Geef de animaties van uw webapp een boost
TL;DR: Met Animation Worklet kun je essentiële animaties schrijven die draaien op de native framerate van het apparaat voor die extra soepele, soepele, soepele weergave™. Je animaties zijn beter bestand tegen storingen in de hoofdthread en je kunt ze koppelen aan scrollen in plaats van aan tijd. Animation Worklet is beschikbaar in Chrome Canary (achter de vlag "Experimentele functies voor het webplatform") en we plannen een Origin-proefversie voor Chrome 71. Je kunt het vandaag nog gebruiken als een progressieve verbetering.
Nog een animatie-API?
Eigenlijk niet, 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 het web wilt animeren, heb je 2,5 opties: CSS-overgangen voor eenvoudige A-naar-B-overgangen, CSS-animaties voor potentieel cyclische, complexere tijdsgebonden animaties en Web Animations API (WAAPI) voor bijna willekeurig complexe animaties. De ondersteuningsmatrix van WAAPI ziet er somber uit, maar het is in opkomst. Tot die tijd is er een polyfill .
Wat al deze methoden gemeen hebben, is dat ze stateloos en tijdsafhankelijk zijn. Maar sommige effecten die ontwikkelaars proberen te bereiken, zijn noch tijdsafhankelijk noch stateloos. De beruchte parallax-scroller is bijvoorbeeld, zoals de naam al aangeeft, scroll-gestuurd. Het implementeren van een performante parallax-scroller op het web is tegenwoordig verrassend moeilijk.
En hoe zit het met stateloosheid? Denk bijvoorbeeld aan de adresbalk van Chrome op Android. Als je naar beneden scrolt, verdwijnt hij uit beeld. Maar zodra je omhoog scrolt, komt hij weer terug, zelfs als je halverwege de pagina bent. De animatie is niet alleen afhankelijk van de scrollpositie, maar ook van je vorige scrollrichting. Het is stateful .
Een ander probleem is de styling van schuifbalken. Ze zijn notoir onstylebaar – of in ieder geval niet stylebaar genoeg. Wat als ik een nyankat als schuifbalk wil? Welke techniek je ook kiest, het bouwen van een aangepaste schuifbalk is noch performant, noch eenvoudig .
Het punt is dat al deze dingen lastig en moeilijk tot onmogelijk efficiënt te implementeren zijn. De meeste ervan zijn afhankelijk van events en/of requestAnimationFrame
, wat je op 60 fps kan houden, zelfs als je scherm 90 fps, 120 fps of hoger aankan en slechts een fractie van je kostbare hoofdframebudget gebruikt.
Animation Worklet breidt de mogelijkheden van de animatiestack op het web uit om dit soort effecten eenvoudiger te maken. Voordat we erin duiken, moeten we ervoor zorgen dat we de basisbeginselen van animaties kennen.
Een inleiding tot animaties en tijdlijnen
WAAPI en Animation Worklet maken uitgebreid gebruik van tijdlijnen, zodat u animaties en effecten naar wens kunt orkestreren. Deze sectie is een korte opfriscursus of introductie tot tijdlijnen en hoe ze werken met animaties.
Elk document heeft document.timeline
. Deze begint bij 0 wanneer het document wordt aangemaakt en telt de milliseconden sinds het document is ontstaan. Alle animaties van een document werken relatief ten opzichte van deze tijdlijn.
Om het wat concreter te maken, laten we 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 + 1000is
and the last keyframe at
startTime + 3000 + 2000`. Het punt is dat de tijdlijn bepaalt waar we ons in onze animatie bevinden!
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, aangezien we iterations: 3
Als we willen dat de animatie nooit stopt, schrijven we iterations: Number.POSITIVE_INFINITY
. Hier is het resultaat van de bovenstaande code.
WAAPI is ongelooflijk krachtig en er zitten nog veel meer functies in deze API, zoals easing, start offsets, keyframe wegingen en fill behavior, die de reikwijdte van dit artikel te buiten zouden gaan. Wil je meer weten? Lees dan dit artikel over CSS-animaties op CSS Tricks.
Een animatieworklet schrijven
Nu we het concept van tijdlijnen onder de knie hebben, kunnen we Animation Worklet bekijken en hoe je ermee kunt spelen! De Animation Worklet API is niet alleen gebaseerd op WAAPI, maar is – in de zin van het extensible web – een primitieve versie op een lager niveau die uitlegt hoe WAAPI werkt. Qua syntaxis lijken ze enorm op elkaar:
Animatie-werkblad | 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: de naam van het worklet dat deze animatie aanstuurt.
Functiedetectie
Chrome is de eerste browser die deze functie biedt, dus zorg ervoor dat je code niet alleen AnimationWorklet
verwacht. Voordat we de worklet laden, moeten we daarom met een eenvoudige controle controleren of de browser van de gebruiker AnimationWorklet
ondersteunt:
if ('animationWorklet' in CSS) {
// AnimationWorklet is supported!
}
Een worklet laden
Worklets zijn een nieuw concept dat door de Houdini-taskforce is geïntroduceerd om veel van de nieuwe API's eenvoudiger te bouwen en te schalen. We zullen later wat uitgebreider op de details van worklets ingaan, maar voor de eenvoud kun je ze voorlopig beschouwen als goedkope en lichtgewicht threads (zoals workers).
We moeten ervoor zorgen dat we een worklet met de naam "passthrough" hebben geladen, 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 en geven deze de naam "passthrough". Het is dezelfde naam die we hierboven in de WorkletAnimation()
constructor hebben gebruikt. Zodra de registratie is voltooid, wordt de promise die door addModule()
wordt geretourneerd, verwerkt en kunnen we beginnen met het maken van animaties met die worklet.
De animate()
-methode van onze instantie wordt aangeroepen voor elk frame dat de browser wil renderen, 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 exact hetzelfde, zoals u kunt zien in de demo .
Tijd
De parameter currentTime
van onze animate()
-methode is de currentTime
van de tijdlijn die we hebben doorgegeven aan de constructor WorkletAnimation()
. In het vorige voorbeeld hebben we die tijd gewoon doorgegeven aan het effect. Maar omdat 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 de currentTime
en wijzen die waarde opnieuw toe aan het bereik [0; 2000], het tijdsbereik waarvoor ons effect is gedefinieerd. De animatie ziet er nu heel anders uit , zonder dat de keyframes of de animatieopties zijn gewijzigd. De workletcode kan willekeurig complex zijn en stelt je in staat om programmatisch te definiëren welke effecten in welke volgorde en in welke mate worden afgespeeld.
Opties over opties
Mogelijk wilt u een worklet hergebruiken en de nummers ervan wijzigen. Daarom kunt u met de constructor WorkletAnimation een optiesobject 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 mij uw lokale staat!
Zoals ik al eerder aangaf, is een van de belangrijkste problemen die een animatieworklet probeert op te lossen, stateful animaties. Animatieworklets mogen een state behouden. Een van de belangrijkste kenmerken van worklets is echter dat ze naar een andere thread kunnen worden gemigreerd of zelfs vernietigd om resources te besparen, wat ook hun state zou vernietigen. Om stateverlies te voorkomen, biedt een animatieworklet een hook die wordt aangeroepen voordat een worklet wordt vernietigd. Deze hook kunt u gebruiken om een stateobject te retourneren. Dat object wordt doorgegeven aan de constructor wanneer de worklet opnieuw wordt aangemaakt. Bij de eerste aanmaak is die parameter 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,
};
}
}
);
Elke keer dat u deze demo vernieuwt, is de kans 50% in welke richting het vierkant zal draaien. Als de browser de worklet zou afbreken en naar een andere thread zou migreren, zou er bij het aanmaken opnieuw een Math.random()
-aanroep plaatsvinden, wat een plotselinge richtingsverandering zou kunnen veroorzaken. Om ervoor te zorgen dat dit niet gebeurt, retourneren we de willekeurig gekozen richting van de animatie als status en gebruiken deze in de constructor, indien aanwezig.
Aansluiten op het ruimte-tijdcontinuüm: ScrollTimeline
Zoals de vorige sectie heeft laten zien, stelt AnimationWorklet ons in staat om programmatisch te definiëren hoe het doorlopen van de tijdlijn de effecten van de animatie beïnvloedt. Maar tot nu toe was onze tijdlijn altijd document.timeline
, wat de tijd bijhoudt.
ScrollTimeline
opent nieuwe mogelijkheden en stelt je in staat om animaties aan te sturen met scrollen in plaats van tijd. We gaan onze allereerste "passthrough" worklet 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
aan. Je raadt het al: ScrollTimeline
gebruikt geen tijd, maar de scrollpositie van de scrollSource
om de currentTime
in het worklet in te stellen. Helemaal naar boven (of links) scrollen betekent currentTime = 0
, terwijl helemaal naar beneden (of rechts) scrollen currentTime
op timeRange
zet. Als je het vak in deze demo scrollt, kun je de positie van het rode vak bepalen.
Als je een ScrollTimeline
maakt met een element dat niet scrollt, is de currentTime
van de tijdlijn NaN
. Dus, vooral met het oog op responsive design, moet je altijd voorbereid zijn op NaN
als currentTime
. Het is vaak verstandig om standaard de waarde 0 te gebruiken.
Het koppelen van animaties aan scrollpositie is iets waar al lang naar wordt gezocht, maar het is nooit echt op dit niveau van nauwkeurigheid bereikt (afgezien van wat onhandige oplossingen met CSS3D). Met Animation Worklet kunnen deze effecten eenvoudig worden geïmplementeerd en tegelijkertijd zeer performant zijn. Bijvoorbeeld: een parallax-scrolleffect zoals deze demo laat zien dat het nu slechts een paar regels kost 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 event loop, 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 te animeren zijn, terwijl andere dat niet zijn. Sommige eigenschappen vereisen alleen wat werk op de GPU om geanimeerd te worden, terwijl andere de browser dwingen om het hele document opnieuw op te maken.
In Chrome (net als in veel andere browsers) hebben we een proces genaamd de compositor. Deze heeft als taak – en ik vereenvoudig het hier sterk – om lagen en texturen te ordenen en vervolgens de GPU te gebruiken om het scherm zo regelmatig mogelijk te updaten, idealiter zo snel als het scherm kan updaten (meestal 60 Hz). Afhankelijk van welke CSS-eigenschappen worden geanimeerd, hoeft de browser mogelijk alleen de compositor zijn werk te laten doen, terwijl andere eigenschappen de lay-out moeten uitvoeren, een bewerking die alleen de hoofdthread kan uitvoeren. Afhankelijk van welke eigenschappen u wilt animeren, wordt uw animatieworklet gekoppeld aan de hoofdthread of uitgevoerd in een aparte thread die synchroon loopt met de compositor.
Een tik op de pols
Er is meestal maar één compositorproces dat mogelijk over meerdere tabbladen wordt gedeeld, omdat de GPU een zeer belaste bron is. Als de compositor op de een of andere manier geblokkeerd raakt, loopt de hele browser vast en reageert niet meer op gebruikersinvoer. Dit moet koste wat kost worden vermeden. Dus wat gebeurt er als uw worklet de gegevens die de compositor nodig heeft niet op tijd kan leveren om het frame te renderen?
Als dit gebeurt, mag de worklet – volgens de specificatie – "uitglijden". Hij loopt achter op de compositor, en de compositor mag de data van het laatste frame hergebruiken om de framesnelheid hoog te houden. Visueel ziet dit eruit als jank, maar het grote verschil is dat de browser nog steeds reageert op gebruikersinvoer.
Conclusie
AnimationWorklet heeft veel facetten en biedt talloze voordelen voor het web. De voor de hand liggende voordelen zijn meer controle over animaties en nieuwe manieren om animaties aan te sturen en zo een nieuw niveau van visuele getrouwheid naar het web te brengen. Maar het API-ontwerp maakt je app ook beter bestand tegen haperingen en biedt tegelijkertijd toegang tot alle nieuwe mogelijkheden.
Animation Worklet is beschikbaar in Canary en we streven naar een Origin-proefversie met Chrome 71. We kijken reikhalzend uit naar jullie fantastische nieuwe webervaringen en horen graag wat we kunnen verbeteren. Er is ook een polyfill die dezelfde API biedt, maar niet de prestatie-isolatie.
Houd er rekening mee dat CSS-overgangen en CSS-animaties nog steeds goede opties zijn en veel eenvoudiger kunnen zijn voor basisanimaties. Maar als je het echt uitgebreid wilt doen, staat AnimationWorklet voor je klaar!