Worklet dell'animazione di Houdini

Potenzia le animazioni della tua app web

TL;DR: il worklet dell'animazione ti consente di scrivere animazioni imperative che vengono eseguite alla frequenza fotogrammi nativa del dispositivo per quella fluidità extra burrosa e priva di jank-free, rendendo le tue animazioni più resilienti al jank del thread principale e collegabili allo scorrimento invece del tempo. Il worklet dell'animazione è in Chrome Canary (dietro il flag "Funzionalità sperimentali della piattaforma web") e stiamo pianificando una prova dell'origine per Chrome 71. Puoi iniziare a utilizzarla come miglioramento progressivo oggi.

Un'altra API Animation?

In realtà no, è un'estensione di ciò che già abbiamo e con una buona ragione! Iniziamo dall'inizio. Se oggi vuoi animare qualsiasi elemento DOM sul web, hai a disposizione due opzioni e mezzo: Transizioni CSS per le transizioni semplici da A a B, Animazioni CSS per animazioni potenzialmente cicliche e basate sul tempo più complesse e API Web Animations (WAAPI) per animazioni quasi arbitrariamente complesse. La matrice di supporto di WAAPI sembra piuttosto cupa, ma è in arrivo. Fino ad allora, è previsto il polyfill.

Tutti questi metodi hanno in comune il fatto che sono stateless e basati sul tempo. Ma alcuni degli effetti che gli sviluppatori stanno cercando non sono né basati sul tempo, né stateless. Ad esempio, il famigerato scorrimento della parallasse è, come suggerisce il nome, tramite scorrimento. Implementare uno scorrimento parallasse ad alte prestazioni sul web oggi è sorprendentemente difficile.

E per quanto riguarda l'apolidia? Pensa, ad esempio, alla barra degli indirizzi di Chrome su Android. Se scorri verso il basso, l'azione non è più visibile. Ma quando scorri verso l'alto la pagina torna indietro, anche se sei a metà 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. Notoriamente non sono stilizzabili o almeno non sono sufficientemente stilizzabili. E se volessi un gatto nyan come barra di scorrimento? Qualunque sia la tecnica che scegli, creare una barra di scorrimento personalizzata non è né efficace né facile.

Il punto è che tutto questo è complicato e difficile da implementare in modo efficiente. La maggior parte si basa sugli eventi e/o requestAnimationFrame, il che potrebbe farti rimanere a 60 f/s, anche quando lo schermo è in grado di funzionare a 90 f/s, 120 f/s o superiore e utilizza una frazione del budget per il thread principale principale.

Il worklet dell'animazione estende le funzionalità degli stack di animazioni del web per semplificare questi tipi di effetti. Prima di addentrarci, assicurati di essere aggiornati sulle nozioni di base delle animazioni.

Una guida introduttiva su animazioni e sequenze temporali

WAAPI e Animation Worklet fanno ampio uso delle sequenze temporali per orchestrare le animazioni e gli effetti nel modo che preferisci. Questa sezione contiene un breve riepilogo o un'introduzione alle sequenze temporali e al loro funzionamento con le animazioni.

Ogni documento ha document.timeline. Inizia da 0 quando il documento viene creato e conta i millisecondi da quando il documento è stato avviato. Tutte le animazioni di un documento funzionano in base a questa sequenza temporale.

Per rendere le cose un po' più concrete, 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 il valore currentTime della sequenza temporale come ora di inizio. L'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 thedurataoptions. 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 sequenza temporale controlla dove ci troviamo nella nostra animazione.

Una volta raggiunto l'ultimo fotogramma chiave, l'animazione tornerà al primo frame chiave e inizierà l'iterazione successiva dell'animazione. Questo processo si ripete per un totale di 3 volte poiché abbiamo impostato iterations: 3. Se volessimo che l'animazione non si interrompesse mai, scriveresti iterations: Number.POSITIVE_INFINITY. Ecco il risultato del codice riportato sopra.

WAAPI è incredibilmente potente e questa API include molte altre funzionalità, come l'easing, gli offset di avvio, le ponderazioni dei fotogrammi chiave e il comportamento di riempimento che supererebbe l'ambito di questo articolo. Per saperne di più, ti consigliamo di leggere questo articolo sulle animazioni CSS sui trucchi per CSS.

Creazione di un worklet dell'animazione

Ora che il concetto di sequenza temporale è stato risolto, possiamo iniziare a osservare il worklet dell'animazione e a come questo consente di sbagliare. 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 molto simili:

Worklet dell'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 risiede nel primo parametro, ossia il nome del worklet che attiva l'animazione.

Rilevamento delle funzionalità

Chrome è il primo browser a offrire questa funzionalità, quindi è necessario assicurarsi che il codice non si aspetti solo che AnimationWorklet sia presente. Quindi, 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 di Houdini per semplificare la creazione e la scalabilità di molte delle nuove API. I dettagli dei worklet verranno illustrati più avanti, ma per semplicità puoi considerarli come thread economici e leggeri (come i worker) per il momento.

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

// 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 e assegnandogli 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 il worklet.

Il metodo animate() della nostra istanza verrà richiamato per ogni frame che il browser vuole visualizzare, passando il valore currentTime della sequenza temporale dell'animazione e l'effetto attualmente in fase di elaborazione. Abbiamo un solo effetto, KeyframeEffect, e stiamo usando currentTime per impostare l'localTime dell'effetto, ecco perché questo animatore è chiamato "passthrough". Con questo codice per il worklet, WAAPI e AnimationWorklet sopra riportati si comportano esattamente allo stesso modo, come puoi vedere nella demo.

Tempo

Il parametro currentTime del nostro metodo animate() è il valore currentTime della sequenza temporale che abbiamo passato al costruttore WorkletAnimation(). Nell'esempio precedente, lo abbiamo appena trasmesso all'effetto. Ma poiché questo è 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)
      );
    }
  }
);

Stiamo prendendo il Math.sin() di currentTime e rimappando questo valore nell'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 rispetto alle opzioni

Potresti voler riutilizzare un worklet e modificarne i numeri. Per questo motivo il costruttore WorkletAnimation consente di passare un oggetto 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 guidate con lo stesso codice, ma con opzioni diverse.

Dammi la provincia locale!

Come ho accennato prima, uno dei problemi principali che il worklet dell'animazione punta a risolvere è le animazioni stateful. I worklet dell'animazione possono mantenere lo stato. Tuttavia, una delle caratteristiche principali dei worklet è la possibilità di eseguirne la migrazione a un thread diverso o persino di eliminarli per risparmiare risorse, il che ne comporta anche l'eliminazione. Per evitare la perdita di stato, il worklet dell'animazione offre un hook chiamato prima dell'eliminazione di un worklet che puoi utilizzare per restituire un oggetto stato. L'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 una probabilità 50/50 nella direzione in cui il quadrato ruoterà. Se il browser elimina il worklet ed esegue la migrazione in un thread diverso, ci sarà un'altra chiamata Math.random() al momento della creazione, il che potrebbe causare un improvviso cambio di direzione. Per assicurarci che ciò non avvenga, restituiamo le animazioni scelte in modo casuale come state e le utilizziamo nel costruttore, se fornito.

Un tuffo nel continuum spazio-temporale: Scorri la cronologia

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

ScrollTimeline apre nuove possibilità e ti consente di creare animazioni con lo scorrimento anziché 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();

Invece di trasmettere document.timeline, stiamo creando un nuovo ScrollTimeline. Come avrai capito, ScrollTimeline non usa il tempo, ma la posizione di scorrimento di scrollSource per impostare currentTime nel worklet. Se scorri completamente verso l'alto (o verso sinistra), currentTime = 0 viene fatto scorrere completamente verso il basso (o verso destra), mentre currentTime viene impostato su timeRange. Se scorri la casella in questa demo, puoi controllare la posizione della casella rossa.

Se crei un elemento ScrollTimeline con un elemento che non scorre, il valore currentTime della sequenza temporale sarà NaN. Quindi, soprattutto se hai in mente il design reattivo, dovresti sempre prepararti a utilizzare NaN come currentTime. Spesso è possibile impostare il valore predefinito 0.

Il collegamento delle animazioni alla posizione di scorrimento è ricercato a lungo, ma non è mai stato raggiunto davvero a questo livello di fedeltà (a parte le soluzioni alternative ingannevoli con CSS3D). Con il worklet dell'animazione, puoi implementare questi effetti in modo semplice e con prestazioni elevate. Ad esempio, un effetto di scorrimento parallasse come questa demo mostra che ora sono necessarie solo un paio di righe per definire un'animazione con scorrimento.

Uno sguardo alle caratteristiche

Worklet

I worklet sono contesti JavaScript con un ambito isolato e una superficie API molto ridotta. La piccola piattaforma API consente un'ottimizzazione più aggressiva dal browser, in particolare sui dispositivi di fascia bassa. Inoltre, i worklet non sono associati a un loop di eventi specifico, ma possono essere spostati tra i thread in base alle esigenze. Questo è particolarmente importante per AnimationWorklet.

Compositor NSync

Forse sai già che certe proprietà CSS si animano velocemente e altre no. Alcune proprietà hanno solo bisogno di un po' di lavoro sulla GPU per animarle, mentre altre forzano il browser a modificare il layout dell'intero documento.

In Chrome (come in molti altri browser) abbiamo un processo chiamato compositor, il cui compito è quello di semplificare l'organizzazione dei livelli e delle trame, per poi utilizzare la GPU per aggiornare lo schermo il più regolarmente possibile, idealmente il più velocemente che lo schermo si aggiorni (in genere a 60 Hz). A seconda delle proprietà CSS da animare, il browser potrebbe dover semplicemente fare lavorare il compositore, mentre le altre proprietà devono eseguire il layout, un'operazione che può essere eseguita solo dal thread principale. A seconda delle proprietà che intendi animare, il worklet dell'animazione sarà associato al thread principale o verrà eseguito in un thread separato in sincronizzazione con il compositor.

Schiaffeggia il polso

Di solito esiste un solo processo di composizione potenzialmente condiviso tra più schede, dato che la GPU è una risorsa molto competitiva. Se il compositor si blocca, l'intero browser si arresta in modo anomalo e non risponde all'input dell'utente. Ciò deve essere evitato a tutti i costi. Quindi cosa succede se il tuo worklet non può fornire i dati di cui il compositore ha bisogno in tempo per il rendering del frame?

In questo caso, il worklet è consentito, secondo le specifiche, a "sfilarsi". Non rimane che il compositore, che può riutilizzare i dati dell'ultimo frame per mantenere elevata la frequenza frame. A livello visivo, sembrerà jank, ma la grande differenza è che il browser è ancora reattivo all'input dell'utente.

Conclusione

Esistono molti facet in AnimationWorklet e i vantaggi che apporta sul web. I vantaggi evidenti sono un maggiore controllo sulle animazioni e nuovi modi per gestirle, in modo da portare sul web un nuovo livello di fedeltà visiva. Tuttavia, la progettazione delle API ti consente anche di rendere la tua app più resiliente a jank e di accedere contemporaneamente a tutte le nuove funzionalità.

Il worklet dell'animazione è in versione Canary e il nostro obiettivo è una prova dell'origine con Chrome 71. Siamo impazienti di ricevere le tue nuove esperienze sul web e di sapere cosa possiamo migliorare. È disponibile anche un polyfill che fornisce la stessa API, ma non fornisce l'isolamento delle prestazioni.

Tieni presente che le transizioni CSS e le animazioni CSS sono comunque opzioni valide e possono essere molto più semplici per le animazioni di base. Ma se hai bisogno di un po' di fantasia, AnimationWorklet ti copre le spalle.