Houdini's animatieworklet

Geef de animaties van uw webapp een boost

TL;DR: Met Animation Worklet kunt u dwingende animaties schrijven die worden uitgevoerd op de eigen framesnelheid van het apparaat voor die extra boterachtige jank-free smoothness™, uw animaties veerkrachtiger maken tegen hoofdlijnen en kunnen worden gekoppeld aan scrollen in plaats van aan tijd. Animation Worklet bevindt zich in Chrome Canary (achter de vlag 'Experimentele webplatformfuncties') en we plannen een Origin-proefversie voor Chrome 71. Je kunt het vandaag nog als een progressieve verbetering gaan gebruiken.

Nog een animatie-API?

Eigenlijk nee, het is een uitbreiding van wat we al hebben, en met goede reden! Laten we bij het begin beginnen. Als je vandaag de dag een DOM-element op internet wilt animeren, heb je 2 ½ keuze: CSS-overgangen voor eenvoudige overgangen van A naar B, CSS-animaties voor potentieel cyclische, complexere, op tijd gebaseerde animaties en Web Animations API (WAAPI) voor vrijwel willekeurige complexe animaties. De ondersteuningsmatrix van WAAPI ziet er behoorlijk somber uit, maar is in de lift. Tot die tijd is er een polyfill .

Wat al deze methoden gemeen hebben, is dat ze staatloos en tijdgestuurd zijn. Maar sommige van de effecten die ontwikkelaars proberen, zijn noch tijdgestuurd, noch staatloos. De beruchte parallax-scroller is bijvoorbeeld, zoals de naam al aangeeft, scroll-aangedreven. Het implementeren van een performante parallax-scroller op internet is tegenwoordig verrassend moeilijk.

En hoe zit het met staatloosheid? Denk bijvoorbeeld aan de adresbalk van Chrome op Android. Als je naar beneden scrollt, scrollt het uit beeld. Maar zodra je naar boven scrolt, komt het terug, zelfs als je halverwege de pagina bent. De animatie is niet alleen afhankelijk van de scrollpositie, maar ook van uw vorige scrollrichting. Het is statelijk .

Een ander probleem is de vormgeving van schuifbalken. Ze zijn notoir onstylebaar – of in ieder geval niet stijlbaar genoeg. Wat moet ik doen als ik een Nyan-kat als schuifbalk wil? Welke techniek u ook kiest, het bouwen van een aangepaste schuifbalk is niet performant en ook niet eenvoudig .

Het punt is dat al deze dingen lastig en moeilijk tot onmogelijk zijn om efficiënt te implementeren. De meeste daarvan zijn afhankelijk van evenementen en/of requestAnimationFrame , waardoor u mogelijk op 60 fps blijft, zelfs als uw scherm op 90 fps, 120 fps of hoger kan draaien en een fractie van uw kostbare hoofdframebudget gebruikt.

Animation Worklet breidt de mogelijkheden van de animatiestapel van het web uit om dit soort effecten eenvoudiger te maken. Voordat we erin duiken, moeten we ervoor zorgen dat we op de hoogte zijn van de basisbeginselen van animaties.

Een inleiding over animaties en tijdlijnen

WAAPI en Animation Worklet maken uitgebreid gebruik van tijdlijnen, zodat u animaties en effecten kunt orkestreren op de manier die u wilt. Dit gedeelte is een snelle opfriscursus of introductie tot tijdlijnen en hoe ze werken met animaties.

Elk document heeft document.timeline . Het begint bij 0 wanneer het document wordt gemaakt en telt de milliseconden sinds het document begon te bestaan. Alle animaties van een document werken relatief ten opzichte van deze tijdlijn.

Laten we, om de zaken wat concreter te maken, eens kijken naar dit WAAPI-fragment

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();

Wanneer we animation.play() aanroepen, gebruikt de animatie de currentTime van de tijdlijn als starttijd. Onze animatie heeft een vertraging van 3000 ms, wat betekent dat de animatie start (of "actief" wordt) wanneer de tijdlijn `startTime bereikt

  • 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 the options. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline's startTime + 3000 + 1000 is and the last keyframe at startTime + 3000 + 2000`. Het punt is dat de tijdlijn bepaalt waar we ons bevinden in onze animatie!

Zodra de animatie het laatste keyframe heeft bereikt, springt deze terug naar het eerste keyframe en start de volgende iteratie van de animatie. Dit proces herhaalt zich in totaal drie keer sinds we iterations: 3 . Als we wilden dat de animatie nooit zou stoppen, zouden we iterations: Number.POSITIVE_INFINITY . Hier is het resultaat van de bovenstaande code.

WAAPI is ongelooflijk krachtig en er zijn nog veel meer functies in deze API, zoals versoepeling, start-offsets, keyframe-wegingen en opvulgedrag, die de reikwijdte van dit artikel zouden verpesten. Als je meer wilt weten, raad ik je aan dit artikel over CSS-animaties op CSS-trucs te lezen.

Een animatiewerklet schrijven

Nu we het concept van tijdlijnen kennen, kunnen we gaan kijken naar Animation Worklet en hoe je hiermee met tijdlijnen kunt rommelen! De Animation Worklet API is niet alleen gebaseerd op WAAPI, maar is – in de zin van het uitbreidbare web – een primitief op een lager niveau dat uitlegt hoe WAAPI functioneert. Qua syntaxis lijken ze ongelooflijk op elkaar:

Animatiewerkje 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();
        

Het verschil zit in de eerste parameter, namelijk de naam van het werklet dat deze animatie aanstuurt.

Functiedetectie

Chrome is de eerste browser die deze functie levert, dus u moet ervoor zorgen dat uw code niet alleen maar verwacht dat AnimationWorklet aanwezig is. Dus voordat we de werklet laden, moeten we met een eenvoudige controle detecteren of de browser van de gebruiker ondersteuning biedt voor AnimationWorklet :

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

Een werklet laden

Worklets zijn een nieuw concept geïntroduceerd door de Houdini-taskforce om veel van de nieuwe API's eenvoudiger te bouwen en te schalen. We zullen later wat dieper ingaan op de details van worklets, maar voor de eenvoud kun je ze voorlopig beschouwen als goedkope en lichtgewicht draden (zoals werkers).

We moeten ervoor zorgen dat we een werklet hebben geladen met de naam "passthrough", voordat we de animatie declareren:

// 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;
    }
  }
);

Wat gebeurt hier? We registreren een klasse als animator met behulp van de registerAnimator() aanroep van AnimationWorklet, waardoor deze de naam "passthrough" krijgt. Het is dezelfde naam die we gebruikten in de WorkletAnimation() -constructor hierboven. Zodra de registratie is voltooid, wordt de door addModule() geretourneerde belofte opgelost en kunnen we beginnen met het maken van animaties met behulp van die werklet.

De animate() -methode van onze instantie wordt aangeroepen voor elk frame dat de browser wil weergeven, waarbij de currentTime van de tijdlijn van de animatie wordt doorgegeven, evenals het effect dat momenteel wordt verwerkt. We hebben maar één effect, het KeyframeEffect , en we gebruiken currentTime om de localTime van het effect in te stellen, vandaar dat deze animator "passthrough" wordt genoemd. Met deze code voor de worklet gedragen de WAAPI en de AnimationWorklet hierboven zich precies hetzelfde, zoals je kunt zien in de demo .

Tijd

De currentTime parameter van onze animate() methode is de currentTime van de tijdlijn die we hebben doorgegeven aan de WorkletAnimation() constructor. In het vorige voorbeeld hebben we die tijd gewoon doorgegeven aan het effect. Maar aangezien dit JavaScript-code is, kunnen we de tijd vervormen 💫

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)
      );
    }
  }
);

We nemen de Math.sin() van currentTime en wijzen die waarde opnieuw toe aan het bereik [0; 2000], het tijdsbereik waarvoor ons effect is gedefinieerd. Nu ziet de animatie er heel anders uit , zonder dat de keyframes of de animatie-opties zijn gewijzigd. De werkletcode kan willekeurig complex zijn en stelt u in staat programmatisch te definiëren welke effecten in welke volgorde en in welke mate worden afgespeeld.

Opties boven opties

Mogelijk wilt u een werklet opnieuw gebruiken en de nummers ervan wijzigen. Om deze reden kunt u met de WorkletAnimation-constructor een optieobject aan de worklet doorgeven:

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 dit voorbeeld worden beide animaties aangestuurd met dezelfde code, maar met verschillende opties.

Geef me je lokale staat!

Zoals ik al eerder heb aangegeven, is een van de belangrijkste problemen die een animatieworklet wil oplossen, stateful animaties. Animatieworklets mogen de status behouden. Een van de belangrijkste kenmerken van worklets is echter dat ze naar een andere thread kunnen worden gemigreerd of zelfs kunnen worden vernietigd om hulpbronnen te sparen, wat ook hun staat zou vernietigen. Om statusverlies te voorkomen, biedt het animatiewerklet een hook die wordt aangeroepen voordat een werklet wordt vernietigd en die u kunt gebruiken om een ​​statusobject te retourneren. Dat object wordt doorgegeven aan de constructor wanneer de werklet opnieuw wordt gemaakt. Bij de eerste aanmaak zal die parameter undefined zijn.

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,
      };
    }
  }
);

Elke keer dat je deze demo ververst, heb je een kans van 50/50 in welke richting het vierkant draait. Als de browser de werklet zou afbreken en naar een andere thread zou migreren, zou er bij het maken nog een Math.random() aanroep plaatsvinden, wat een plotselinge verandering van richting zou kunnen veroorzaken. Om ervoor te zorgen dat dit niet gebeurt, retourneren we de willekeurig gekozen richting van de animaties als status en gebruiken we deze in de constructor, indien aanwezig.

Aansluiten bij het ruimte-tijd continuüm: ScrollTimeline

Zoals de vorige sectie heeft laten zien, stelt AnimationWorklet ons in staat programmatisch te definiëren hoe het vooruitschuiven van de tijdlijn de effecten van de animatie beïnvloedt. Maar tot nu toe was onze tijdlijn altijd document.timeline , dat de tijd bijhoudt.

ScrollTimeline opent nieuwe mogelijkheden en stelt u in staat animaties aan te sturen met scrollen in plaats van met tijd. We gaan onze allereerste "passthrough" -werklet hergebruiken voor deze 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();

In plaats van document.timeline door te geven, maken we een nieuwe ScrollTimeline . Je raadt het misschien al: ScrollTimeline gebruikt geen tijd, maar de scrollpositie van de scrollSource om de currentTime in de worklet in te stellen. Helemaal naar boven (of naar links) scrollen betekent currentTime = 0 , terwijl helemaal naar beneden (of naar rechts) scrollen currentTime instelt op timeRange . Als u in deze demo door het vakje bladert, kunt u de positie van het rode vakje bepalen.

Als u een ScrollTimeline maakt met een element dat niet scrollt, is de currentTime van de tijdlijn NaN . Dus vooral met responsief ontwerp in gedachten moet u altijd voorbereid zijn op NaN als uw currentTime . Het is vaak verstandig om standaard een waarde van 0 in te stellen.

Het koppelen van animaties aan scrollpositie is iets waar al lang naar wordt gezocht, maar dat op dit niveau van betrouwbaarheid nooit echt is bereikt (afgezien van hacky-oplossingen met CSS3D). Met Animation Worklet kunnen deze effecten op een eenvoudige manier worden geïmplementeerd en tegelijkertijd zeer performant zijn. Een parallax-scrolleffect zoals deze demo laat bijvoorbeeld zien dat er nu slechts een paar regels nodig zijn om een ​​scroll-gestuurde animatie te definiëren.

Onder de motorkap

Werkjes

Worklets zijn JavaScript-contexten met een geïsoleerde scope en een zeer klein API-oppervlak. Het kleine API-oppervlak maakt agressievere optimalisatie vanuit de browser mogelijk, vooral op low-end apparaten. Bovendien zijn worklets niet gebonden aan een specifieke gebeurtenislus, maar kunnen ze indien nodig tussen threads worden verplaatst. Dit is vooral belangrijk voor AnimationWorklet.

Compositor NSync

Je weet misschien dat bepaalde CSS-eigenschappen snel kunnen worden geanimeerd, terwijl andere dat niet zijn. Sommige eigenschappen hebben alleen wat werk aan de GPU nodig om geanimeerd te worden, terwijl andere de browser dwingen het hele document opnieuw op te maken.

In Chrome hebben we (net als in veel andere browsers) een proces genaamd de compositor, wiens taak het is (en ik vereenvoudig dit hier heel erg) om lagen en texturen te rangschikken en vervolgens de GPU te gebruiken om het scherm zo regelmatig mogelijk bij te werken. idealiter zo snel als het scherm kan updaten (meestal 60 Hz). Afhankelijk van welke CSS-eigenschappen worden geanimeerd, hoeft de browser mogelijk alleen maar de compositor zijn werk te laten doen, terwijl andere eigenschappen de lay-out moeten uitvoeren, wat een bewerking is die alleen de hoofdthread kan uitvoeren. Afhankelijk van de eigenschappen die u wilt animeren, wordt uw animatiewerklet gebonden aan de hoofdthread of wordt deze in een aparte thread uitgevoerd, synchroon met de compositor.

Tik op de vingers

Er is meestal maar één compositorproces dat mogelijk over meerdere tabbladen wordt gedeeld, omdat de GPU een zeer omstreden hulpmiddel is. Als de compositor op de een of andere manier geblokkeerd raakt, komt de hele browser tot stilstand en reageert niet meer op gebruikersinvoer. Dit moet koste wat het kost worden vermeden. Dus wat gebeurt er als uw werklet niet op tijd de gegevens kan leveren die de compositor nodig heeft om het frame te kunnen weergeven?

Als dit gebeurt, mag het werklet – per specificatie – ‘slippen’. Het raakt achter op de compositor en de compositor mag de gegevens van het laatste frame hergebruiken om de framesnelheid hoog te houden. Visueel lijkt dit op jank, maar het grote verschil is dat de browser nog steeds reageert op gebruikersinvoer.

Conclusie

AnimationWorklet en de voordelen die het met zich meebrengt voor het internet hebben veel facetten. De voor de hand liggende voordelen zijn meer controle over animaties en nieuwe manieren om animaties aan te sturen om een ​​nieuw niveau van visuele betrouwbaarheid op internet te brengen. Maar dankzij het ontwerp van de API's kunt u uw app ook beter bestand maken tegen janken, terwijl u tegelijkertijd toegang krijgt tot al het nieuwe goed.

Animation Worklet is beschikbaar in Canary en we streven naar een Origin-proefversie met Chrome 71. We kijken reikhalzend uit naar je geweldige nieuwe webervaringen en horen wat we kunnen verbeteren. Er is ook een polyfill die u dezelfde API geeft, maar niet de prestatie-isolatie biedt.

Houd er rekening mee dat CSS-overgangen en CSS-animaties nog steeds geldige opties zijn en veel eenvoudiger kunnen zijn voor basisanimaties. Maar als je zin hebt in luxe, staat AnimationWorklet voor je klaar!