Introduzione ai filtri personalizzati (noti anche come Shader CSS)

Paul Lewis

I filtri personalizzati, o shader CSS come venivano chiamati in precedenza, ti consentono di utilizzare la potenza degli shader di WebGL con i contenuti DOM. Poiché nell'implementazione attuale gli shader utilizzati sono praticamente gli stessi di WebGL, devi fare un passo indietro e comprendere un po' di terminologia 3D e la pipeline di grafica.

Ho incluso una versione registrata di una presentazione che ho tenuto di recente a LondonJS. Nel video viene fornita una panoramica della terminologia 3D che devi conoscere, i diversi tipi di variabili che incontrerai e come iniziare a utilizzare i filtri personalizzati oggi stesso. Ti consigliamo inoltre di scaricare le diapositive per poter provare le demo.

Introduzione agli shader

In precedenza ho scritto un'introduzione agli shader che ti fornirà una buona panoramica di cosa sono gli shader e di come puoi utilizzarli dal punto di vista di WebGL. Se non hai mai utilizzato gli shader, ti consigliamo di leggere questo articolo prima di procedere oltre, perché molti dei concetti e del linguaggio dei filtri personalizzati si basano sulla terminologia degli shader WebGL esistente.

Detto questo, abilitiamo i filtri personalizzati e continuiamo.

Attivazione dei filtri personalizzati

I filtri personalizzati sono disponibili sia in Chrome che in Canary, nonché in Chrome per Android. Basta andare su about:flags e cercare "Shader CSS", attivarli e riavviare il browser. Ora è tutto pronto.

La sintassi

I filtri personalizzati ampliano l'insieme di filtri che puoi già applicare, ad esempio blur o sepia, agli elementi DOM. Eric Bidelman ha scritto un ottimo strumento di test per questo tipo di problemi, che ti consiglio di provare.

Per applicare un filtro personalizzato a un elemento DOM, utilizza la seguente sintassi:

.customShader {
    -webkit-filter:

    custom(
        url(vertexshader.vert)
        mix(url(fragment.frag) normal source-atop),

    /* Row, columns - the vertices are made automatically */
    4 5,

    /* We set uniforms; we can't set attributes */
    time 0)
}

Da questo codice puoi vedere che dichiariamo gli shader vertex e fragment, il numero di righe e colonne in cui vogliamo suddividere l'elemento DOM e le eventuali uniformi da passare.

Un'ultima cosa da sottolineare è che utilizziamo la funzione mix() intorno al frammento shader con una modalità di miscela (normal) e una modalità composita (source-atop). Diamo un'occhiata al frammento shader stesso per capire perché abbiamo bisogno di una funzione mix().

Pixel Pushing

Se hai dimestichezza con gli shader di WebGL, noterai che in Filtri personalizzati le cose sono leggermente diverse. Ad esempio, non creiamo le texture utilizzate dal nostro shader di frammento per riempire i pixel. I contenuti DOM a cui è applicato il filtro vengono mappati automaticamente a una trama, il che significa due cose:

  1. Per motivi di sicurezza, non possiamo eseguire query sui singoli valori di colore dei pixel della trama del DOM
  2. Non impostiamo (almeno nelle implementazioni attuali) il colore del pixel finale, ovvero gl_FragColor è off limits. Si presume piuttosto che tu voglia eseguire il rendering dei contenuti DOM e che tu possa manipolare i relativi pixel indirettamente tramite css_ColorMatrix e css_MixColor.

Ciò significa che il nostro Hello World di shader di frammenti ha un aspetto più simile al seguente:

void main() {
    css_ColorMatrix = mat4(1.0, 0.0, 0.0, 0.0,
                            0.0, 1.0, 0.0, 0.0,
                            0.0, 0.0, 1.0, 0.0,
                            0.0, 0.0, 0.0, 1.0);

    css_MixColor = vec4(0.0, 0.0, 0.0, 0.0);

    // umm, where did gl_FragColor go?
}

Ogni pixel dei contenuti DOM viene moltiplicato per css_ColorMatrix, che nel caso precedente non fa nulla perché è la matrice di identità e non modifica nessuno dei valori RGBA. Se volessimo, ad esempio, mantenere solo i valori rossi, useremmo un css_ColorMatrix come questo:

// keep only red and alpha
css_ColorMatrix = mat4(1.0, 0.0, 0.0, 0.0,
                        0.0, 0.0, 0.0, 0.0,
                        0.0, 0.0, 0.0, 0.0,
                        0.0, 0.0, 0.0, 1.0);

Spero che tu possa notare che, moltiplicando i valori dei pixel 4D (RGBA) per la matrice, ottieni un valore del pixel manipolato dall'altro lato e, in questo caso, uno che azzera i componenti verde e blu.

css_MixColor viene utilizzato principalmente come colore di base da combinare con i contenuti DOM. La miscelazione viene eseguita tramite le modalità di fusione che conosci dai pacchetti di arte: overlay, schermo, sfumatura colore, luce dura e così via.

Esistono molti modi in cui queste due variabili possono manipolare i pixel. Ti consigliamo di consultare la specifica degli effetti filtro per comprendere meglio il modo in cui interagiscono le modalità di miscelazione e composizione.

Creazione di vertici

In WebGL ci assumiamo la piena responsabilità della creazione dei punti 3D della nostra mesh, ma in Filtri personalizzati devi solo specificare il numero di righe e colonne che ti interessano e il browser suddividerà automaticamente i contenuti DOM in una serie di triangoli:

Creazione di vertici
Un'immagine suddivisa in righe e colonne

Ognuno di questi vertici viene poi passato al nostro vertex shader per la manipolazione, il che significa che possiamo iniziare a spostarli nello spazio 3D in base alle nostre esigenze. A breve potrai creare effetti davvero fantastici.

Un effetto accordion
Un'immagine deformata da un effetto accordion

Animazione con shader

L'aggiunta di animazioni agli shader li rende divertenti e coinvolgenti. Per farlo, utilizza semplicemente una transizione (o animazione) nel CSS per aggiornare i valori uniformi:

.shader {
    /* transition on the filter property */
    -webkit-transition: -webkit-filter 2500ms ease-out;

    -webkit-filter: custom(
    url(vshader.vert)
    mix(url(fshader.frag) normal source-atop),
    1 1,
    time 0);
}

    .shader:hover {
    -webkit-filter: custom(
    url(vshader.vert)
    mix(url(fshader.frag) normal source-atop),
    1 1,
    time 1);
}

Quindi, nel codice riportato sopra, tieni presente che il tempo passerà da 0 a 1 durante la transizione. All'interno dello shader possiamo dichiarare l'uniforme time e utilizzare il relativo valore corrente:

    uniform float time;

uniform mat4 u_projectionMatrix;
attribute vec4 a_position;

void main() {
    // copy a_position to position - attributes are read only!
    vec4 position = a_position;

    // use our time uniform from the CSS declaration
    position.x += time;

    gl_Position = u_projectionMatrix * position;
}

Inizia a giocare.

I filtri personalizzati sono molto divertenti e gli effetti incredibili che puoi creare sono difficili (e in alcuni casi impossibili) da realizzare senza di essi. È ancora presto per dare un giudizio definitivo e le cose stanno cambiando molto, ma perché non provare? Aggiungere queste funzionalità ai tuoi progetti può dare un tocco di spettacolo in più.

Risorse aggiuntive