Worklet d'animation de Houdini

Optimiser les animations de votre application Web

Résumé:le Worklet d'animation vous permet d'écrire des animations impératives qui s'exécutent à la fréquence d'images native de l'appareil pour une fluidité supplémentaire sans à-coups. Elles rendent vos animations plus résilientes face aux à-coups du thread principal et peuvent être associées au défilement plutôt qu'au temps. Le Worklet d'animation est disponible dans Chrome Canary (derrière l'indicateur "Fonctionnalités expérimentales de la plate-forme Web) et nous prévoyons une phase d'évaluation pour Chrome 71. Vous pouvez commencer à l'utiliser dès aujourd'hui en tant qu'amélioration progressive.

Autre API d'animation ?

En fait non, c'est une extension de ce que nous avons déjà, et à juste titre ! Commençons par le début. Si vous souhaitez animer n'importe quel élément DOM sur le Web aujourd'hui, vous avez le choix entre deux options: Transitions CSS pour les transitions de A à B simples, Animations CSS pour les animations temporelles potentiellement cycliques et plus complexes, et API Web Animations (WAAPI) pour les animations complexes presque arbitrairement. La matrice de prise en charge de WAAPI semble assez sombre, mais elle est en cours d'amélioration. En attendant, il y a un polyfill.

Toutes ces méthodes ont un point en commun : elles sont sans état et basées sur le temps. Mais certains des effets que les développeurs essaient ne sont ni temporels, ni sans état. Par exemple, comme son nom l'indique, le célèbre parallaxe est axé sur le défilement. Implémenter un conteneur de défilement parallaxe performant sur le Web aujourd'hui est étonnamment difficile.

Et qu'en est-il de l'état sans état ? Prenons l'exemple de la barre d'adresse de Chrome sur Android. Si vous faites défiler l'écran vers le bas, vous ne verrez plus le bouton. Toutefois, dès que vous faites défiler la page vers le haut, elle revient, même si vous êtes à mi-chemin du bas de la page. L'animation dépend non seulement de la position de défilement, mais également de la direction de défilement précédente. Il est avec état.

Le style des barres de défilement constitue un autre problème. Ils sont notoirement peu stylisés ou, du moins, ne sont pas assez stylisés. Comment faire pour utiliser un chat nyan comme barre de défilement ? Quelle que soit la technique que vous choisissez, la création d'une barre de défilement personnalisée n'est ni performante, ni facile.

Le fait est que toutes ces choses sont gênantes et difficiles, voire impossibles à implémenter efficacement. La plupart d'entre eux s'appuient sur des événements et/ou requestAnimationFrame, ce qui peut vous maintenir à 60 FPS, même si votre écran peut s'exécuter à 90, 120 FPS ou plus, et utiliser une fraction de votre précieux budget de frame de thread principal.

Le workflow d'animation étend les fonctionnalités de la pile d'animations du Web pour faciliter ce type d'effets. Avant d'entrer dans le vif du sujet, vérifions que nous sommes à jour sur les bases des animations.

Présentation des animations et des timelines

WAAPI et Animation Worklet font un usage intensif des timelines pour vous permettre d'orchestrer des animations et des effets comme vous le souhaitez. Cette section est un bref rappel ou une introduction aux chronologies et à leur fonctionnement avec les animations.

Chaque document contient document.timeline. Elle commence à 0 lorsque le document est créé et compte le nombre de millisecondes écoulées depuis le début de la création du document. Toutes les animations d'un document fonctionnent par rapport à cette timeline.

Pour être plus concret, examinons cet extrait 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();

Lorsque nous appelons animation.play(), l'animation utilise le currentTime de la timeline comme heure de début. Notre animation a un délai de 3 000 ms, ce qui signifie qu'elle commencera (ou deviendra "active") lorsque la timeline atteindra la valeur "startTime".

  • 3 000. 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 + 3 000 + 1 000and the last keyframe atstartTime + 3 000 + 2 000". Le point est le point, les commandes de la timeline où nous nous trouvons dans notre animation !

Une fois que l'animation a atteint la dernière image clé, elle revient à la première image clé et lance l'itération suivante de l'animation. Ce processus se répète trois fois au total depuis que nous avons défini iterations: 3. Si nous voulons que l'animation ne s'arrête jamais, nous écrivons iterations: Number.POSITIVE_INFINITY. Voici le résultat du code ci-dessus.

WAAPI est incroyablement puissant et il existe de nombreuses autres fonctionnalités dans cette API, telles que l'accélération, les décalages de début, la pondération des images clés et le comportement de remplissage, qui élivraient le champ d'application de cet article. Pour en savoir plus, nous vous invitons à lire cet article sur les animations CSS et les astuces CSS.

Écrire un Worklet d'animation

Maintenant que le concept de chronologie est défini, nous pouvons commencer à examiner le Worklet d'animation et comment il vous permet de manipuler les timelines. L'API Animation Worklet n'est pas seulement basée sur WAAPI, mais constitue, dans le sens du Web étendu, une primitive de niveau inférieur qui explique le fonctionnement de WAAPI. En termes de syntaxe, ils sont incroyablement similaires:

Worklet d'animation API 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 différence réside dans le premier paramètre, qui est le nom du worklet qui pilote cette animation.

Détection de fonctionnalités

Chrome est le premier navigateur à proposer cette fonctionnalité. Vous devez donc vous assurer que votre code ne s'attend pas seulement à la présence de AnimationWorklet. Avant de charger le worklet, nous devons donc vérifier si le navigateur de l'utilisateur est compatible avec AnimationWorklet à l'aide d'une simple vérification:

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

Charger un worklet

Les Worklets sont un nouveau concept introduit par le groupe de travail Houdini afin de faciliter la création et le scaling de nombreuses nouvelles API. Nous couvrirons les détails des Worklets un peu plus tard, mais pour plus de simplicité, vous pouvez les considérer comme des threads légers et bon marché (comme des workers).

Nous devons nous assurer que nous avons chargé un worklet nommé "passthrough" avant de déclarer l'animation:

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

Que se passe-t-il ici ? Nous enregistrons une classe en tant qu'animateur à l'aide de l'appel registerAnimator() d'AnimationWorklet et nous lui donnons le nom "passthrough". Il s'agit du nom que nous avons utilisé dans le constructeur WorkletAnimation() ci-dessus. Une fois l'enregistrement terminé, la promesse renvoyée par addModule() est résolue et nous pouvons commencer à créer des animations à l'aide de ce worklet.

La méthode animate() de notre instance sera appelée pour chaque image que le navigateur souhaite afficher, en transmettant la currentTime de la timeline de l'animation ainsi que l'effet en cours de traitement. Nous n'avons qu'un seul effet, KeyframeEffect, et nous utilisons currentTime pour définir la localTime de l'effet. C'est pourquoi cet animateur est appelé "passthrough". Avec ce code pour le worklet, WAAPI et AnimationWorklet ci-dessus se comportent exactement de la même manière, comme vous pouvez le voir dans la démonstration.

Temps

Le paramètre currentTime de notre méthode animate() est le currentTime de la chronologie que nous avons transmise au constructeur WorkletAnimation(). Dans l'exemple précédent, nous venons de transmettre ce délai à l'effet. Mais comme il s'agit d'un code JavaScript, nous pouvons distorquer le temps 💫

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

Nous prenons la valeur Math.sin() de currentTime et nous remappons cette valeur à la plage [0; 2000], qui est la période pour laquelle notre effet est défini. Maintenant, l'animation est très différente, sans avoir modifié les images clés ni les options de l'animation. Le code du worklet peut être arbitrairement complexe. Il vous permet de définir de manière programmatique l'ordre et l'étendue des effets à exécuter.

Remplacer les options par des options

Vous voudrez peut-être réutiliser un worklet et modifier ses numéros. Pour cette raison, le constructeur WorkletAnimation vous permet de transmettre un objet d'options au 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();

Dans cet exemple, les deux animations sont générées avec le même code, mais avec des options différentes.

Donne-moi l'État de votre région !

Comme indiqué précédemment, les animations avec état sont l'un des principaux problèmes que le workflow d'animation doit résoudre. Les Worklets d'animation sont autorisés à conserver l'état. Cependant, l'une des principales caractéristiques des Worklets est qu'ils peuvent être migrés vers un autre thread ou même être détruits pour économiser des ressources, ce qui détruit également leur état. Pour éviter la perte d'état, le worklet d'animation propose un hook appelé avant la destruction d'un worklet, que vous pouvez utiliser pour renvoyer un objet d'état. Cet objet sera transmis au constructeur lors de la recréation du worklet. Lors de la création initiale, ce paramètre sera 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,
      };
    }
  }
);

Chaque fois que vous actualisez cette démonstration, vous avez 50/50 de chances dans la direction du carré. Si le navigateur devait supprimer le worklet et le migrer vers un autre thread, un autre appel Math.random() serait effectué lors de la création, ce qui pourrait entraîner un changement soudain de direction. Pour éviter que cela ne se produise, nous renvoyons les animations choisies de manière aléatoire en tant qu'state et nous l'utilisons dans le constructeur, s'il est fourni.

Se connecter au continuum espace-temps: ScrollTimeline

Comme indiqué dans la section précédente, AnimationWorklet nous permet de définir de manière programmatique l'impact de l'avance de la timeline sur les effets de l'animation. Mais jusqu'à présent, notre chronologie a toujours été document.timeline, ce qui suit le temps.

ScrollTimeline ouvre de nouvelles possibilités et vous permet de générer des animations en faisant défiler la page plutôt qu'en fonction du temps. Nous allons réutiliser notre tout premier worklet "passthrough" pour cette démonstration:

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

Au lieu de transmettre document.timeline, nous créons un objet ScrollTimeline. Vous l'avez peut-être deviné, ScrollTimeline n'utilise pas le temps, mais la position de défilement de scrollSource pour définir currentTime dans le worklet. Un défilement jusqu'en haut (ou à gauche) signifie currentTime = 0, tandis qu'un défilement jusqu'en bas (ou à droite) définit currentTime sur timeRange. Si vous faites défiler le cadre dans cette démonstration, vous pouvez contrôler la position du cadre rouge.

Si vous créez une ScrollTimeline avec un élément qui ne défile pas, l'élément currentTime de la chronologie sera NaN. Ainsi, en particulier dans le domaine du responsive design, vous devez toujours vous préparer à utiliser NaN comme currentTime. Il est souvent logique de définir une valeur de 0 par défaut.

L'association d'animations à la position de défilement est un élément qui a été recherché depuis longtemps, mais qui n'a jamais été réellement obtenu à ce niveau de fidélité (à l'exception des solutions complexes avec CSS3D). Le Worklet d'animation permet d'implémenter ces effets de manière simple tout en étant très performants. Par exemple, un effet de défilement parallaxe, comme dans cette démonstration, montre qu'il suffit désormais de quelques lignes pour définir une animation liée au défilement.

dans le détail

Worklets

Les Worklets sont des contextes JavaScript avec un champ d'application isolé et une surface d'API très petite. La petite surface de l'API permet une optimisation plus agressive du navigateur, en particulier sur les appareils bas de gamme. De plus, les Worklets ne sont pas liés à une boucle d'événements spécifique, mais peuvent être déplacés entre les threads si nécessaire. Ceci est particulièrement important pour AnimationWorklet.

NSync du compositeur

Vous savez peut-être que certaines propriétés CSS sont rapides à animer, d'autres non. Certaines propriétés nécessitent simplement du travail sur le GPU pour être animées, tandis que d'autres obligent le navigateur à remettre en page l'ensemble du document.

Dans Chrome (comme dans de nombreux autres navigateurs), nous avons un processus appelé compositeur, dont le rôle est, et je le simplifie ici, d'organiser les couches et les textures, puis d'utiliser le GPU pour mettre à jour l'écran aussi régulièrement que possible, idéalement aussi vite que possible (généralement 60 Hz). En fonction des propriétés CSS qui sont animées, le navigateur devra peut-être simplement faire fonctionner le compositeur, tandis que d'autres propriétés devront exécuter la mise en page, une opération que seul le thread principal peut faire. Selon les propriétés que vous prévoyez d'animer, votre worklet d'animation sera lié au thread principal ou exécuté dans un thread distinct synchronisé avec le compositeur.

Gifler au poignet

Il n'y a généralement qu'un seul processus compositeur potentiellement partagé entre plusieurs onglets, car le GPU est une ressource très conflictuelle. Si le compositeur est bloqué d'une manière ou d'une autre, l'ensemble du navigateur s'interrompt et ne répond plus aux entrées utilisateur. Cela doit être évité à tout prix. Que se passe-t-il si votre worklet ne peut pas fournir les données dont le compositeur a besoin à temps pour le rendu du frame ?

Dans ce cas, le worklet est autorisé, conformément aux spécifications, à "glisser". Elle prend du retard sur le compositeur, qui est autorisé à réutiliser les données de la dernière image pour maintenir la fréquence d'images. Visuellement, cela ressemblera à des à-coups, mais la grande différence est que le navigateur réagit toujours aux entrées utilisateur.

Conclusion

AnimationWorklet présente de nombreux aspects et les avantages qu'il apporte au Web. Les avantages évidents sont un meilleur contrôle des animations et de nouvelles façons de les générer afin d'apporter un nouveau niveau de fidélité visuelle sur le Web. Toutefois, la conception des API vous permet également de rendre votre application plus résistante aux à-coups tout en ayant accès à tous les nouveaux avantages.

Le Worklet d'animation est en version Canary et nous prévoyons une phase d'évaluation avec Chrome 71. Nous attendons avec impatience vos nouvelles expériences Web exceptionnelles et vos suggestions d'améliorations. Il existe également un polyfill qui vous donne la même API, mais n'offre pas d'isolation des performances.

N'oubliez pas que les transitions CSS et les animations CSS restent des options valides et peuvent être beaucoup plus simples pour les animations de base. Mais si vous avez besoin de vous laisser porter, AnimationWorklet est là pour vous aider !