Worklet dell'animazione di Houdini

Potenzia le animazioni della tua web app

TL;DR: Animation Worklet ti consente di scrivere animazioni imperative che vengono eseguite alla frequenza dei fotogrammi nativa del dispositivo per una fluidità extra senza scatti™, rende le tue animazioni più resilienti agli scatti del thread principale e sono collegabili allo scorrimento anziché al tempo. Animation Worklet è disponibile in Chrome Canary (dietro il flag "Funzionalità sperimentali della piattaforma web") e stiamo pianificando una prova dell'origine per Chrome 71. Puoi iniziare a utilizzarlo come miglioramento progressivo oggi stesso.

Un'altra API Animation?

In realtà no, è un'estensione di ciò che abbiamo già e per un buon motivo. Iniziamo dall'inizio. Se oggi vuoi animare un elemento DOM sul web, hai a disposizione due opzioni e mezzo: transizioni CSS per transizioni semplici da A a B, animazioni CSS per animazioni potenzialmente cicliche e più complesse basate sul tempo e API Web Animations (WAAPI) per animazioni quasi arbitrariamente complesse. La matrice di supporto di WAAPI non è molto promettente, ma è in miglioramento. Fino ad allora, è disponibile un polyfill.

Questi metodi hanno in comune il fatto di essere stateless e basati sul tempo. Tuttavia, alcuni degli effetti che gli sviluppatori stanno provando non sono basati sul tempo né stateless. Ad esempio, il famigerato scorrimento parallasse è, come suggerisce il nome, basato sullo scorrimento. Implementare uno scorrimento parallasse performante sul web oggi è sorprendentemente difficile.

E per quanto riguarda la tecnologia stateless? Pensa, ad esempio, alla barra degli indirizzi di Chrome su Android. Se scorri verso il basso, non sarà più visibile. Ma non appena scorri verso l'alto, torna a essere visibile, anche se ti trovi a metà della pagina. L'animazione dipende non solo dalla posizione di scorrimento, ma anche dalla direzione di scorrimento precedente. È stateful.

Un altro problema è lo stile delle barre di scorrimento. Sono notoriamente difficili da stilizzare o almeno non abbastanza. Cosa succede se voglio un nyan cat come barra di scorrimento? Qualunque tecnica tu scelga, creare una barra di scorrimento personalizzata non è né efficiente né semplice.

Il punto è che tutte queste cose sono difficili e quasi impossibili da implementare in modo efficiente. La maggior parte si basa su eventi e/o requestAnimationFrame, che potrebbero mantenere la frequenza a 60 fps, anche quando lo schermo è in grado di funzionare a 90 fps, 120 fps o superiore e utilizzare una frazione del prezioso budget dei frame del thread principale.

Animation Worklet estende le funzionalità dello stack di animazioni del web per semplificare questo tipo di effetti. Prima di iniziare, assicuriamoci di avere le basi delle animazioni.

Introduzione alle animazioni e alle sequenze temporali

WAAPI e Animation Worklet utilizzano ampiamente le sequenze temporali per consentirti di orchestrare animazioni ed effetti nel modo che preferisci. Questa sezione è un rapido ripasso o introduzione alle sequenze temporali e al loro funzionamento con le animazioni.

Ogni documento ha document.timeline. Inizia da 0 quando viene creato il documento e conta i millisecondi trascorsi dalla creazione del documento. Tutte le animazioni di un documento funzionano in relazione a questa sequenza temporale.

Per rendere il tutto un po' più concreto, diamo un'occhiata a questo snippet WAAPI

const animation = new Animation(
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)',
      },
      {
        transform: 'translateX(500px)',
      },
      {
        transform: 'translateY(500px)',
      },
    ],
    {
      delay: 3000,
      duration: 2000,
      iterations: 3,
    }
  ),
  document.timeline
);

animation.play();

Quando chiamiamo animation.play(), l'animazione utilizza currentTime della sequenza temporale come ora di inizio. La nostra animazione ha un ritardo di 3000 ms, il che significa che l'animazione inizierà (o diventerà "attiva") quando la sequenza temporale raggiunge `startTime

  • 3000. After that time, the animation engine will animate the given element from the first keyframe (translateX(0)), through all intermediate keyframes (translateX(500px)) all the way to the last keyframe (translateY(500px)) in exactly 2000ms, as prescribed by thedurationoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline'scurrentTimeisstartTime + 3000 + 1000and the last keyframe atstartTime + 3000 + 2000`. Il punto è che la timeline controlla la posizione dell'animazione.

Una volta raggiunto l'ultimo fotogramma chiave, l'animazione torna al primo fotogramma chiave e inizia l'iterazione successiva. Questo processo si ripete un totale di 3 volte, dato che abbiamo impostato iterations: 3. Se volessimo che l'animazione non si fermasse mai, scriveremmo iterations: Number.POSITIVE_INFINITY. Ecco il risultato del codice precedente.

L'API WAAPI è incredibilmente potente e offre molte altre funzionalità, come l'accelerazione, gli offset iniziali, le ponderazioni dei fotogrammi chiave e il comportamento di riempimento, che andrebbero oltre l'ambito di questo articolo. Se vuoi saperne di più, ti consiglio di leggere questo articolo sulle animazioni CSS su CSS Tricks.

Scrivere un worklet di animazione

Ora che abbiamo compreso il concetto di sequenze temporali, possiamo iniziare a esaminare Animation Worklet e come ti consente di modificare le sequenze temporali. L'API Animation Worklet non si basa solo su WAAPI, ma è, nel senso del web estensibile, una primitiva di livello inferiore che spiega come funziona WAAPI. In termini di sintassi, sono incredibilmente simili:

Worklet di animazione WAAPI
new WorkletAnimation(
  'passthrough',
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)'
      },
      {
        transform: 'translateX(500px)'
      }
    ],
    {
      duration: 2000,
      iterations: Number.POSITIVE_INFINITY
    }
  ),
  document.timeline
).play();
      
        new Animation(

        new KeyframeEffect(
        document.querySelector('#a'),
        [
        {
        transform: 'translateX(0)'
        },
        {
        transform: 'translateX(500px)'
        }
        ],
        {
        duration: 2000,
        iterations: Number.POSITIVE_INFINITY
        }
        ),
        document.timeline
        ).play();
        

La differenza sta nel primo parametro, ovvero il nome del worklet che gestisce questa animazione.

Rilevamento delle funzionalità

Chrome è il primo browser a implementare questa funzionalità, quindi devi assicurarti che il tuo codice non si limiti a prevedere la presenza di AnimationWorklet. Pertanto, prima di caricare il worklet, dobbiamo rilevare se il browser dell'utente supporta AnimationWorklet con un semplice controllo:

if ('animationWorklet' in CSS) {
  // AnimationWorklet is supported!
}

Caricamento di un worklet

I worklet sono un nuovo concetto introdotto dalla task force Houdini per semplificare la creazione e lo scaling di molte delle nuove API. Vedremo i dettagli dei worklet più avanti, ma per semplicità per ora puoi considerarli come thread economici e leggeri (come i worker).

Prima di dichiarare l'animazione, dobbiamo assicurarci di aver caricato un worklet con il nome "passthrough":

// index.html
await CSS.animationWorklet.addModule('passthrough-aw.js');
// ... WorkletAnimation initialization from above ...

// passthrough-aw.js
registerAnimator(
  'passthrough',
  class {
    animate(currentTime, effect) {
      effect.localTime = currentTime;
    }
  }
);

Che cosa accade in questo caso? Stiamo registrando una classe come animatore utilizzando la chiamata registerAnimator() di AnimationWorklet, assegnandole il nome "passthrough". È lo stesso nome che abbiamo utilizzato nel costruttore WorkletAnimation() sopra. Una volta completata la registrazione, la promessa restituita da addModule() verrà risolta e potremo iniziare a creare animazioni utilizzando questo worklet.

Il metodo animate() della nostra istanza verrà chiamato per ogni frame che il browser vuole eseguire il rendering, passando il currentTime della sequenza temporale dell'animazione e l'effetto attualmente in elaborazione. Abbiamo un solo effetto, KeyframeEffect, e utilizziamo currentTime per impostare localTime dell'effetto, motivo per cui questo animatore è chiamato "passthrough". Con questo codice per il worklet, WAAPI e AnimationWorklet sopra si comportano esattamente allo stesso modo, come puoi vedere nella demo.

Ora

Il parametro currentTime del nostro metodo animate() è currentTime della sequenza temporale che abbiamo passato al costruttore WorkletAnimation(). Nell'esempio precedente, abbiamo semplicemente passato l'ora all'effetto. Ma dato che si tratta di codice JavaScript e possiamo distorcere il tempo 💫

function remap(minIn, maxIn, minOut, maxOut, v) {
  return ((v - minIn) / (maxIn - minIn)) * (maxOut - minOut) + minOut;
}
registerAnimator(
  'sin',
  class {
    animate(currentTime, effect) {
      effect.localTime = remap(
        -1,
        1,
        0,
        2000,
        Math.sin((currentTime * 2 * Math.PI) / 2000)
      );
    }
  }
);

Prendiamo il Math.sin() del currentTime e rimappiamo questo valore all'intervallo [0; 2000], che è l'intervallo di tempo per cui è definito il nostro effetto. Ora l'animazione ha un aspetto molto diverso, senza aver modificato i fotogrammi chiave o le opzioni dell'animazione. Il codice del worklet può essere arbitrariamente complesso e ti consente di definire in modo programmatico quali effetti vengono riprodotti in quale ordine e in quale misura.

Opzioni su opzioni

Potresti voler riutilizzare un worklet e modificarne i numeri. Per questo motivo, il costruttore WorkletAnimation ti consente di passare un oggetto di opzioni al worklet:

registerAnimator(
  'factor',
  class {
    constructor(options = {}) {
      this.factor = options.factor || 1;
    }
    animate(currentTime, effect) {
      effect.localTime = currentTime * this.factor;
    }
  }
);

new WorkletAnimation(
  'factor',
  new KeyframeEffect(
    document.querySelector('#b'),
    [
      /* ... same keyframes as before ... */
    ],
    {
      duration: 2000,
      iterations: Number.POSITIVE_INFINITY,
    }
  ),
  document.timeline,
  {factor: 0.5}
).play();

In questo esempio, entrambe le animazioni sono gestite con lo stesso codice, ma con opzioni diverse.

Dimmi il tuo stato locale.

Come ho accennato in precedenza, uno dei problemi principali che l'animation worklet mira a risolvere sono le animazioni con stato. I worklet di animazione possono mantenere lo stato. Tuttavia, una delle funzionalità principali dei worklet è che possono essere migrati in un thread diverso o persino distrutti per risparmiare risorse, il che distruggerebbe anche il loro stato. Per evitare la perdita di stato, l'animation worklet offre un hook chiamato prima che un worklet venga eliminato, che puoi utilizzare per restituire un oggetto di stato. Questo oggetto verrà passato al costruttore quando il worklet viene ricreato. Al momento della creazione iniziale, questo parametro sarà undefined.

registerAnimator(
  'randomspin',
  class {
    constructor(options = {}, state = {}) {
      this.direction = state.direction || (Math.random() > 0.5 ? 1 : -1);
    }
    animate(currentTime, effect) {
      // Some math to make sure that `localTime` is always > 0.
      effect.localTime = 2000 + this.direction * (currentTime % 2000);
    }
    destroy() {
      return {
        direction: this.direction,
      };
    }
  }
);

Ogni volta che aggiorni questa demo, hai il 50% di possibilità che il quadrato ruoti in una direzione o nell'altra. Se il browser dovesse smontare il worklet e migrarlo in un altro thread, si verificherebbe un'altra chiamata Math.random() alla creazione, che potrebbe causare un cambio improvviso di direzione. Per evitare che ciò accada, restituiamo la direzione scelta in modo casuale per le animazioni come state e la utilizziamo nel costruttore, se fornita.

Agganciarsi al continuum spazio-temporale: ScrollTimeline

Come mostrato nella sezione precedente, AnimationWorklet ci consente di definire a livello di programmazione in che modo l'avanzamento della sequenza temporale influisce sugli effetti dell'animazione. Finora, però, la nostra cronologia è sempre stata document.timeline, che tiene traccia del tempo.

ScrollTimeline apre nuove possibilità e ti consente di controllare le animazioni con lo scorrimento anziché con il tempo. Riutilizzeremo il nostro primo worklet "passthrough" per questa demo:

new WorkletAnimation(
  'passthrough',
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)',
      },
      {
        transform: 'translateX(500px)',
      },
    ],
    {
      duration: 2000,
      fill: 'both',
    }
  ),
  new ScrollTimeline({
    scrollSource: document.querySelector('main'),
    orientation: 'vertical', // "horizontal" or "vertical".
    timeRange: 2000,
  })
).play();

Anziché trasmettere document.timeline, stiamo creando un nuovo ScrollTimeline. Come avrai intuito, ScrollTimeline non utilizza il tempo, ma la posizione di scorrimento di scrollSource per impostare currentTime nel worklet. Se scorri fino in alto (o a sinistra), il valore è currentTime = 0, mentre se scorri fino in basso (o a destra), il valore di currentTime è timeRange. Se scorri la casella in questa demo, puoi controllare la posizione della casella rossa.

Se crei un ScrollTimeline con un elemento che non scorre, il currentTime della cronologia sarà NaN. Quindi, soprattutto tenendo presente il design adattabile, devi sempre prepararti per NaN come currentTime. Spesso è sensato impostare un valore predefinito di 0.

Il collegamento delle animazioni alla posizione di scorrimento è un obiettivo a lungo termine, ma non è mai stato raggiunto a questo livello di fedeltà (a parte soluzioni alternative con CSS3D). Animation Worklet consente di implementare questi effetti in modo semplice e con prestazioni elevate. Ad esempio: un effetto di scorrimento parallasse come questa demo mostra che ora bastano poche righe per definire un'animazione basata sullo scorrimento.

dietro le quinte

Worklet

I worklet sono contesti JavaScript con un ambito isolato e una superficie API molto piccola. La piccola superficie dell'API consente un'ottimizzazione più aggressiva da parte del browser, soprattutto sui dispositivi di fascia bassa. Inoltre, i worklet non sono vincolati a un ciclo di eventi specifico, ma possono essere spostati tra i thread in base alle necessità. Ciò è particolarmente importante per AnimationWorklet.

Compositor NSync

Potresti sapere che alcune proprietà CSS sono veloci da animare, mentre altre no. Alcune proprietà richiedono solo un po' di lavoro sulla GPU per essere animate, mentre altre forzano il browser a riorganizzare l'intero documento.

In Chrome (come in molti altri browser) abbiamo un processo chiamato compositor, il cui compito è, e qui sto semplificando molto, quello di disporre i livelli e le texture e poi utilizzare la GPU per aggiornare lo schermo il più regolarmente possibile, idealmente alla velocità di aggiornamento dello schermo (in genere 60 Hz). A seconda delle proprietà CSS animate, il browser potrebbe aver bisogno solo del compositore, mentre altre proprietà devono eseguire il layout, un'operazione che può essere eseguita solo dal thread principale. A seconda delle proprietà che prevedi di animare, il worklet di animazione verrà associato al thread principale o verrà eseguito in un thread separato in sincronia con il compositore.

Slap on the wrist

In genere esiste un solo processo di composizione potenzialmente condiviso tra più schede, poiché la GPU è una risorsa molto contesa. Se il compositore viene bloccato in qualche modo, l'intero browser si blocca e non risponde all'input dell'utente. Questo deve essere evitato a tutti i costi. Quindi, cosa succede se il worklet non riesce a fornire in tempo i dati necessari al compositore per il rendering del frame?

In questo caso, il worklet può "slittare" come da specifiche. Rimane indietro rispetto al compositore, che può riutilizzare i dati dell'ultimo frame per mantenere la frequenza dei fotogrammi. A livello visivo, questo problema si presenta come un'interruzione, ma la grande differenza è che il browser continua a rispondere all'input dell'utente.

Conclusione

AnimationWorklet ha molte sfaccettature e offre numerosi vantaggi per il web. I vantaggi più evidenti sono un maggiore controllo sulle animazioni e nuovi modi per animarle, portando un nuovo livello di fedeltà visiva sul web. Tuttavia, la progettazione delle API consente anche di rendere la tua app più resiliente ai problemi di scattosità, ottenendo al contempo l'accesso a tutte le nuove funzionalità.

Animation Worklet è in Canary e puntiamo a una Origin Trial con Chrome 71. Attendiamo con ansia le tue nuove esperienze web e i tuoi suggerimenti su cosa possiamo migliorare. Esiste anche un polyfill che offre la stessa API, ma non fornisce l'isolamento delle prestazioni.

Tieni presente che le transizioni CSS e le animazioni CSS sono ancora opzioni valide e possono essere molto più semplici per le animazioni di base. Ma se vuoi qualcosa di più elaborato, AnimationWorklet ti copre le spalle.