Worklet dell'animazione di Houdini

Potenzia le animazioni della tua applicazione web

TL;DR: il worklet di animazione ti consente di scrivere animazioni imperative che vengono eseguite alla frequenza fotogrammi nativa del dispositivo, per ottenere uno smoothnessTM senza sforzi rendi le animazioni più resilienti rispetto ai blocchi del thread principale e siano collegabili per scorrere al posto del tempo. Il worklet dell'animazione è disponibile in Chrome Canary (dietro "Funzionalità sperimentali della piattaforma web" ) e stiamo pianificando una prova dell'origine per Chrome 71. Puoi iniziare a utilizzarlo come un miglioramento progressivo oggi.

Un'altra API Animation?

In realtà no, è un'estensione di quello che già abbiamo e con una buona ragione! Cominciamo dall'inizio. Se vuoi animare qualsiasi elemento DOM sul web Oggi hai 2 opzioni e mezzo: transizioni CSS per semplici transizioni 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 ha un aspetto piuttosto tetro, ma è in arrivo. Fino ad allora, c'è polyfill.

Quello che tutti questi metodi hanno in comune è che sono stateless in base al tempo. Ma alcuni effetti che gli sviluppatori stanno provando non sono basata sul tempo né stateless. Ad esempio, il famigerato scorrimento parallasse è, come il nome implica, è basato sullo scorrimento. Oggi, l'implementazione di uno scorrimento parallasse ad alte prestazioni sul web è sorprendentemente difficile.

E per quanto riguarda l'stateless? Pensa alla barra degli indirizzi di Chrome su Android, esempio. Se scorri verso il basso, l'opzione scompare dalla visualizzazione. Ma mentre scorri verso l'alto, torna indietro, anche se sei a metà strada più in basso. L'animazione non dipende solo dalla posizione di scorrimento, ma anche la direzione di scorrimento precedente. È stateful.

Un altro problema è l'applicazione di stili alle barre di scorrimento. Non sono stilizzabili, o addirittura non abbastanza stilizzabili. E se volessi un gatto nyan come barra di scorrimento? Qualunque sia la tecnica scelta, non è possibile creare una barra di scorrimento personalizzata rendimento elevato né facile.

Il punto è che tutte queste cose sono imbarazzanti, difficili o impossibili da da implementare in modo efficiente. La maggior parte si basa su eventi e/o requestAnimationFrame, che potrebbe mantenerti a 60 f/s, anche quando lo schermo è in grado di funzionare a 90 f/s, 120 f/s o superiori e utilizza una frazione del il prezioso budget del frame del thread principale.

Il worklet dell'animazione estende le capacità dello stack di animazioni del web per questo tipo di effetti. Prima di entrare nel dettaglio, assicuriamoci aggiornato sulle nozioni di base delle animazioni.

Un'introduzione ad animazioni e sequenze temporali

WAAPI e Animation Worklet fanno ampio uso di sequenze temporali per consentirti orchestrare animazioni ed effetti nel modo che desideri. Questa sezione riguarda un un rapido 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 è creato e conteggia i millisecondi dall'inizio del documento. Tutti questi le animazioni di un documento funzionano in relazione a questa sequenza temporale.

Per rendere le cose un po' più concreti, 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. La nostra animazione ha un ritardo di 3000 ms, il che significa che l'animazione inizierà (o diventerà "attiva") quando la sequenza temporale raggiunge il valore "startTime".

  • 3000. After that time, the animation engine will animate the given element from the first keyframe (traduttoreX(0)), through all intermediate keyframes (traduttoreX(500 px)) all the way to the last keyframe (traduttoreY(500 px)) in exactly 2000ms, as prescribed by thedurataoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline'stempoattualeisOra inizio+3000 + 1000and the last keyframe atOra inizio+3000 + 2000`. Il punto è che la sequenza temporale controlla il punto in cui ci troviamo nell'animazione.

Una volta raggiunto l'ultimo fotogramma chiave, l'animazione tornerà al primo. fotogramma chiave e avvia l'iterazione successiva dell'animazione. Questo processo ripete tre volte in totale da quando abbiamo impostato iterations: 3. Se volessimo che l'animazione non fermarsi mai, scriveremo iterations: Number.POSITIVE_INFINITY. Ecco il risultato del codice in alto.

WAAPI è incredibilmente potente e include molte altre funzionalità, ad esempio easing, offset iniziali, ponderazioni dei fotogrammi chiave e comportamento di riempimento che nell'ambito di questo articolo. Per saperne di più, ti consigliamo di leggere questo articolo sulle animazioni CSS sui trucchi CSS.

Scrittura di un worklet di animazione

Ora che abbiamo delineato il concetto di sequenza temporale, possiamo iniziare a osservare il worklet dell'animazione e come ti permette di confondere le cronologie. Animazione L'API Worklet non si basa solo su WAAPI, ma è, nel senso del web estendibile, una primitiva di livello inferiore che spiega come funziona WAAPI. In termini di sintassi, sono estremamente 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 è nel primo parametro, che è il nome del worklet che alimenta questa animazione.

Rilevamento delle caratteristiche

Chrome è il primo browser a offrire questa funzionalità, quindi devi assicurarti il codice non solo si aspetta che AnimationWorklet sia lì. Quindi, prima di caricare worklet, dobbiamo rilevare se il browser dell'utente supporta AnimationWorklet con un semplice segno di spunta:

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

Caricamento di un worklet in corso...

I worklet sono un nuovo concetto introdotto dalla task force di Houdini per rendere creare e scalare le nuove API. Tratteremo i dettagli dei worklet un po' più avanti, ma per semplicità si può considerare come economici e i thread leggeri (come i worker).

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 un corso come animatore utilizzando Chiamata registerAnimator() di AnimationWorklet con il nome "passthrough". È lo stesso nome che abbiamo usato nel costruttore WorkletAnimation() sopra. Una volta registrazione completata, la promessa restituita da addModule() si risolverà possiamo iniziare a creare animazioni usando questo worklet.

Il metodo animate() della nostra istanza viene chiamato per ogni frame il browser vuole eseguire il rendering, trasmettendo il currentTime della sequenza temporale dell'animazione nonché l'effetto attualmente in fase di elaborazione. Ne abbiamo solo uno , KeyframeEffect e stiamo usando currentTime per impostare localTime, per questo motivo l'animatore è chiamato "passthrough". Con questo codice per il worklet, WAAPI e AnimationWorklet sopra si comportano esattamente come puoi vedere demo.

Ora

Il parametro currentTime del nostro metodo animate() è il currentTime del della sequenza temporale che abbiamo passato al costruttore WorkletAnimation(). Nella precedente esempio, abbiamo appena superato il periodo di tempo. Ma poiché si tratta di codice JavaScript e possiamo distortare 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 valore Math.sin() di currentTime e rimappiamo questo valore a l'intervallo [0; 2000], ovvero l'intervallo di tempo per il quale è stato definito l'effetto. Adesso l'animazione ha un aspetto molto diverso, senza ha modificato i fotogrammi chiave o le opzioni dell'animazione. Il codice del worklet può essere arbitrariamente complesso e 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 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 il tuo stato!

Come accennato in precedenza, uno dei principali problemi del worklet dell'animazione è con le animazioni stateful. I worklet di animazione possono rimanere nello stato. Tuttavia, delle caratteristiche principali dei worklet è che possono essere migrati il thread o addirittura distruggersi per salvare risorse, distruggendo così anche i loro stato. Per evitare la perdita di stato, il worklet di animazione offre un hook che viene chiamato prima che venga eliminato un worklet, che puoi utilizzare per restituire uno stato . Questo oggetto verrà passato al costruttore quando il worklet viene ricreato. Al momento della creazione iniziale, il 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 un punteggio di 50/50 probabilità nella direzione in cui il quadrato ruota. Se il browser venisse rimosso del worklet e migrarlo in un thread diverso, c'è un altro Chiamata di Math.random() alla creazione, che potrebbe causare un cambiamento improvviso delle . Per assicurarci che non avvenga, restituiamo le animazioni direzione scelta casualmente come state e utilizzala nel costruttore, se disponibile.

Lanciati nel continuum spazio-tempo: Scorri la cronologia

Come abbiamo mostrato nella sezione precedente, AnimationWorklet ci consente a definire in modo programmatico in che modo l'avanzamento della sequenza temporale influisce sugli effetti l'animazione. Ma finora le nostre tempistiche sono sempre state document.timeline e, di conseguenza, tiene traccia del tempo.

ScrollTimeline offre nuove possibilità e ti consente di creare animazioni con lo scorrimento invece che con il tempo. Riutilizzeremo il primo "passthrough" worklet per questo 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é superare document.timeline, stiamo creando un nuovo ScrollTimeline. Potresti aver indovinato, ScrollTimeline non usa il tempo, ma Posizione di scorrimento di scrollSource per impostare currentTime nel worklet. Essere fatto di scorrere fino in alto (o a sinistra) significa currentTime = 0, mentre scorre fino in fondo (o verso destra) imposta currentTime su timeRange. Se scorri la casella demo, puoi controllare la posizione della casella rossa.

Se crei un elemento ScrollTimeline con un elemento che non scorre, il valore di currentTime della sequenza temporale sarà NaN. Soprattutto con il responsive design Tieni presente che dovresti sempre essere pronto per NaN come currentTime. Spesso da 0, per impostazione predefinita.

È da molto tempo che si cerca di collegare le animazioni alla posizione di scorrimento, ma non è mai stato raggiunto a questo livello di fedeltà (a parte gli attacchi hacky soluzioni alternative con CSS3D). Il worklet dell'animazione consente di in modo semplice e con un rendimento elevato. Ad esempio: un effetto di scorrimento parallasse come questo demo mostra che sono necessarie solo un paio di righe per definire un'animazione basata sullo scorrimento.

dietro le quinte

Worklet

I worklet sono contesti JavaScript con un ambito isolato e un'API molto piccola superficie. La piccola superficie dell'API consente un'ottimizzazione più aggressiva da parte solo sui dispositivi di fascia bassa. Inoltre, i worklet non sono legati per un loop di eventi specifico, ma possono essere spostati da un thread all'altro in base alle esigenze. Questo è particolarmente importante per AnimationWorklet.

NSync compositore

Come saprai, alcune proprietà CSS si animano rapidamente, mentre altre sono . Alcune proprietà richiedono solo un po' di lavoro sulla GPU per essere animata, mentre altre forza il re-layout dell'intero documento.

In Chrome (come in molti altri browser) abbiamo un processo chiamato compositore, il cui compito è (e qui semplifichiamo moltissimo) organizzare gli strati texture e quindi utilizzare la GPU per aggiornare lo schermo nel modo più regolare possibile, idealmente alla velocità di aggiornamento dello schermo (in genere 60 Hz). In base a quale Le proprietà CSS vengono animate, il browser potrebbe avere bisogno solo di di un compositore, mentre le altre proprietà devono eseguire il layout, eseguibile solo dal thread principale. In base alle proprietà che prevedono l'animazione, il worklet dell'animazione sarà associato al riquadro o eseguirlo in un thread separato sincronizzato con il compositore.

Schiaffo sul polso

Di solito esiste un solo processo di composizione che viene potenzialmente condiviso più schede, in quanto la GPU è una risorsa molto contenuta. Se il compositore ottiene in qualche modo bloccato, l'intero browser si blocca e non risponde l'input dell'utente. Questo deve essere evitato a tutti i costi. Quindi cosa succede se le tue il worklet non può fornire i dati di cui il compositore ha bisogno in tempo per il frame eseguire il rendering?

In questo caso, il worklet può "slittare", in base alla specifica. Rimane indietro il compositore può riutilizzare i dati dell'ultimo frame mantenere alta la frequenza fotogrammi. Visivamente, sarà simile a un jank, ma la grande è che il browser risponde ancora all'input dell'utente.

Conclusione

AnimationWorklet offre molti aspetti e i vantaggi che apporta sul web. I vantaggi evidenti sono un maggiore controllo sulle animazioni e nuovi modi di guidare per portare un nuovo livello di fedeltà visiva nel web. Ma le API consente anche di rendere la tua app più resiliente ai blocchi accesso a tutte le nuove virtù contemporaneamente.

Il worklet di animazione è in versione canary e puntiamo a una prova dell'origine con Chrome 71. Siamo impazienti di ricevere nuove esperienze e opinioni sul web su ciò che possiamo migliorare. C'è 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 valide e può essere molto più semplice per le animazioni di base. Ma se devi andare fantasia, AnimationWorklet ti copre le spalle!