Worklet de animación de Houdini

Potencia las animaciones de tu app web

Resumen: Animation Worklet te permite escribir animaciones imperativas que se ejecutan a la velocidad de fotogramas nativa del dispositivo para obtener una fluidez sin interrupciones™, hacen que tus animaciones sean más resistentes a las interrupciones del subproceso principal y se pueden vincular al desplazamiento en lugar del tiempo. Animation Worklet está en Chrome Canary (detrás de la marca "Funciones experimentales de la plataforma web") y estamos planeando una prueba de origen para Chrome 71. Puedes comenzar a usarlo como mejora progresiva hoy mismo.

¿Otra API de Animation?

En realidad, no. Es una extensión de lo que ya tenemos, y con razón. Empecemos por el principio. Si quieres animar cualquier elemento DOM en la Web hoy en día, tienes 2 ½ opciones: transiciones de CSS para transiciones simples de A a B, animaciones de CSS para animaciones potencialmente cíclicas y más complejas basadas en el tiempo, y la API de Web Animations (WAAPI) para animaciones casi arbitrariamente complejas. La matriz de compatibilidad de WAAPI parece bastante sombría, pero está mejorando. Hasta entonces, hay un polyfill.

Lo que tienen en común todos estos métodos es que son sin estado y basados en el tiempo. Sin embargo, algunos de los efectos que los desarrolladores están probando no son ni orientados al tiempo ni sin estado. Por ejemplo, el famoso control deslizante de paralaje es, como su nombre lo indica, controlado por desplazamiento. Implementar un control deslizante de paralaje de alto rendimiento en la Web hoy en día es sorprendentemente difícil.

¿Y qué sucede con la falta de estado? Piensa en la barra de direcciones de Chrome en Android, por ejemplo. Si te desplazas hacia abajo, desaparecerá de la vista. Sin embargo, en cuanto te desplazas hacia arriba, vuelve a aparecer, incluso si estás a mitad de esa página. La animación depende no solo de la posición de desplazamiento, sino también de la dirección de desplazamiento anterior. Tiene estado.

Otro problema es aplicar diseño a las barras de desplazamiento. Son notoriamente difíciles de modificar, o al menos no lo suficiente. ¿Qué sucede si quiero un gato nyan como barra de desplazamiento? Independientemente de la técnica que elijas, compilar una barra de desplazamiento personalizada no es ni óptima ni fácil.

El punto es que todas estas cosas son incómodas y difíciles de implementar de manera eficiente. La mayoría de ellos dependen de eventos o requestAnimationFrame, que pueden mantenerte a 60 fps, incluso cuando la pantalla es capaz de ejecutarse a 90 fps, 120 fps o más, y usar una fracción de tu preciado presupuesto de fotogramas del subproceso principal.

Animation Worklet extiende las capacidades de la pila de animaciones de la Web para facilitar este tipo de efectos. Antes de comenzar, asegúrate de estar al tanto de los conceptos básicos de las animaciones.

Introducción a las animaciones y los cronogramas

WAAPI y Animation Worklet usan mucho los cronogramas para permitirte orquestar animaciones y efectos de la forma que desees. Esta sección es un repaso rápido o una introducción a los cronogramas y cómo funcionan con las animaciones.

Cada documento tiene document.timeline. Comienza en 0 cuando se crea el documento y cuenta los milisegundos desde que comenzó a existir. Todas las animaciones de un documento funcionan en relación con este cronograma.

Para que todo sea un poco más concreto, veamos este fragmento de 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();

Cuando llamamos a animation.play(), la animación usa el currentTime de la línea de tiempo como su hora de inicio. Nuestra animación tiene un retraso de 3000 ms, lo que significa que la animación comenzará (o se volverá "activa") cuando el cronograma alcance "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 thedurationoptions. 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`. El punto es que el cronograma controla dónde estamos en nuestra animación.

Una vez que la animación llegue al último fotograma clave, saltará al primero y comenzará la siguiente iteración de la animación. Este proceso se repite un total de 3 veces desde que configuramos iterations: 3. Si quisiéramos que la animación nunca se detenga, escribiríamos iterations: Number.POSITIVE_INFINITY. Este es el resultado del código anterior.

WAAPI es increíblemente potente y tiene muchas más funciones, como la atenuación, los desfases de inicio, las ponderaciones de fotogramas clave y el comportamiento de relleno, que excederían el alcance de este artículo. Si quieres obtener más información, te recomiendo que leas este artículo sobre animaciones de CSS en CSS Tricks.

Cómo escribir un worklet de animación

Ahora que tenemos el concepto de cronogramas, podemos comenzar a analizar la Worklet de animación y cómo te permite manipular los cronogramas. La API de Animation Worklet no solo se basa en WAAPI, sino que, en el sentido de la Web extensible, es una primitiva de nivel inferior que explica cómo funciona WAAPI. En términos de sintaxis, son muy similares:

Worklet de animación 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 diferencia está en el primer parámetro, que es el nombre del worklet que controla esta animación.

Detección de atributos

Chrome es el primer navegador en lanzar esta función, por lo que debes asegurarte de que tu código no espere que AnimationWorklet esté allí. Por lo tanto, antes de cargar el worklet, debemos detectar si el navegador del usuario es compatible con AnimationWorklet con una verificación simple:

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

Cómo cargar una tarea

Los worklets son un concepto nuevo que presentó el equipo de tareas de Houdini para facilitar la compilación y escalamiento de muchas de las APIs nuevas. Hablaremos de los detalles de los worklets un poco más adelante, pero, por ahora, puedes pensar en ellos como subprocesos baratos y ligeros (como los trabajadores).

Debemos asegurarnos de haber cargado una worklet con el nombre “passthrough” antes de declarar la animación:

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

¿Qué está pasando aquí? Registramos una clase como animador con la llamada registerAnimator() de AnimationWorklet y le asignamos el nombre "passthrough". Es el mismo nombre que usamos en el constructor WorkletAnimation() anterior. Una vez que se complete el registro, se resolverá la promesa que devuelve addModule() y podremos comenzar a crear animaciones con esa worklet.

Se llamará al método animate() de nuestra instancia para cada fotograma que el navegador quiera renderizar y se pasará el currentTime de la línea de tiempo de la animación, así como el efecto que se está procesando actualmente. Solo tenemos un efecto, el KeyframeEffect, y usamos currentTime para configurar el localTime del efecto, por lo que este animador se llama “transferencia”. Con este código para la worklet, WAAPI y AnimationWorklet se comportan exactamente de la misma manera, como puedes ver en la demostración.

Hora

El parámetro currentTime de nuestro método animate() es el currentTime del cronograma que pasamos al constructor WorkletAnimation(). En el ejemplo anterior, solo pasamos ese tiempo al efecto. Pero, como este es código JavaScript, podemos distorcionar el tiempo 💫

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

Tomamos el Math.sin() de currentTime y reasignamos ese valor al rango [0; 2000], que es el intervalo de tiempo para el que se define nuestro efecto. Ahora la animación se ve muy diferente, sin haber cambiado los fotogramas clave ni las opciones de la animación. El código de la worklet puede ser arbitrariamente complejo y te permite definir de forma programática qué efectos se reproducen en qué orden y en qué medida.

Opciones sobre opciones

Es posible que desees volver a usar una tarea y cambiar sus números. Por este motivo, el constructor WorkletAnimation te permite pasar un objeto de opciones a la 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();

En este ejemplo, ambas animaciones se controlan con el mismo código, pero con opciones diferentes.

Dame tu estado local.

Como lo mencioné antes, uno de los problemas clave que la worklet de animación intenta resolver son las animaciones con estado. Los worklets de animación pueden mantener el estado. Sin embargo, una de las funciones principales de los worklets es que se pueden migrar a un subproceso diferente o incluso destruirse para ahorrar recursos, lo que también destruiría su estado. Para evitar la pérdida de estado, la worklet de animación ofrece un hook que se llama antes de que se destruya una worklet que puedes usar para mostrar un objeto de estado. Ese objeto se pasará al constructor cuando se vuelva a crear la worklet. En la creación inicial, ese parámetro será 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,
      };
    }
  }
);

Cada vez que actualizas esta demostración, tienes una probabilidad de 50/50 de que el cuadrado gire en una dirección o en la otra. Si el navegador desmontara la worklet y la migrara a un subproceso diferente, habría otra llamada a Math.random() en la creación, lo que podría causar un cambio repentino de dirección. Para asegurarnos de que eso no suceda, devolvemos la dirección elegida al azar de las animaciones como estado y la usamos en el constructor, si se proporciona.

Cómo conectarse al continuo espacio-temporal: ScrollTimeline

Como se mostró en la sección anterior, AnimationWorklet nos permite definir de forma programática cómo el avance de la línea de tiempo afecta los efectos de la animación. Pero hasta ahora, nuestro cronograma siempre fue document.timeline, que hace un seguimiento del tiempo.

ScrollTimeline abre nuevas posibilidades y te permite controlar animaciones con desplazamiento en lugar de tiempo. Volveremos a usar nuestra primera worklet de “transferencia” para esta demostración:

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

En lugar de pasar document.timeline, creamos un ScrollTimeline nuevo. Como lo habrás adivinado, ScrollTimeline no usa el tiempo, sino la posición de desplazamiento de scrollSource para establecer currentTime en la worklet. Desplazarse hasta la parte superior (o izquierda) significa currentTime = 0, mientras que desplazarse hasta la parte inferior (o derecha) establece currentTime en timeRange. Si desplazas el cuadro en esta demo, puedes controlar la posición del cuadro rojo.

Si creas un ScrollTimeline con un elemento que no se desplaza, el currentTime de la línea de tiempo será NaN. Por lo tanto, especialmente con el diseño responsivo en mente, siempre debes estar preparado para NaN como tu currentTime. A menudo, es conveniente establecer un valor predeterminado de 0.

Vincular animaciones con la posición de desplazamiento es algo que se buscó durante mucho tiempo, pero que nunca se logró con este nivel de fidelidad (aparte de soluciones alternativas hacky con CSS3D). La Worklet de animación permite implementar estos efectos de una manera directa y, al mismo tiempo, con un alto rendimiento. Por ejemplo, un efecto de desplazamiento de paralaje como esta demo muestra que ahora solo se necesitan un par de líneas para definir una animación impulsada por el desplazamiento.

Detrás de escena

Worklets

Los worklets son contextos de JavaScript con un alcance aislado y una superficie de API muy pequeña. La pequeña superficie de la API permite una optimización más agresiva desde el navegador, en especial en dispositivos de gama baja. Además, los worklets no están vinculados a un bucle de eventos específico, pero se pueden mover entre subprocesos según sea necesario. Esto es particolarmente importante para AnimationWorklet.

NSync del compositor

Es posible que sepas que ciertas propiedades de CSS son rápidas de animar, mientras que otras no. Algunas propiedades solo necesitan un poco de trabajo en la GPU para animarse, mientras que otras obligan al navegador a volver a diseñar todo el documento.

En Chrome (como en muchos otros navegadores), tenemos un proceso llamado compositor, cuyo trabajo es organizar las capas y las texturas y, luego, usar la GPU para actualizar la pantalla con la mayor frecuencia posible, idealmente, lo más rápido que pueda (por lo general, a 60 Hz). Según las propiedades de CSS que se animen, es posible que el navegador solo necesite que el compositor haga su trabajo, mientras que otras propiedades deben ejecutar el diseño, que es una operación que solo puede realizar el subproceso principal. Según las propiedades que planeas animar, tu worklet de animación se vinculará al subproceso principal o se ejecutará en un subproceso independiente sincronizado con el compositor.

Golpea la muñeca

Por lo general, solo hay un proceso de compositor que se comparte potencialmente en varias pestañas, ya que la GPU es un recurso muy disputado. Si el compositor se bloquea de alguna manera, todo el navegador se detiene y deja de responder a las entradas del usuario. Esto debe evitarse a toda costa. Entonces, ¿qué sucede si tu worklet no puede entregar los datos que necesita el compositor a tiempo para que se renderice el fotograma?

Si esto sucede, la worklet puede "deslizarse" según las especificaciones. Se queda atrás del compositor, y este puede volver a usar los datos del último fotograma para mantener la velocidad de fotogramas alta. Visualmente, esto se verá como un bloqueo, pero la gran diferencia es que el navegador aún responde a las entradas del usuario.

Conclusión

AnimationWorklet tiene muchas facetas y beneficios para la Web. Los beneficios obvios son un mayor control sobre las animaciones y nuevas formas de generar animaciones para llevar un nuevo nivel de fidelidad visual a la Web. Sin embargo, el diseño de las APIs también te permite hacer que tu app sea más resistente a los bloqueos mientras obtienes acceso a todas las funciones nuevas al mismo tiempo.

Animation Worklet está en Canary y nuestro objetivo es realizar una prueba de origen con Chrome 71. Esperamos con ansias tus nuevas experiencias web y saber qué podemos mejorar. También hay un polyfill que te brinda la misma API, pero no proporciona el aislamiento de rendimiento.

Ten en cuenta que las transiciones y animaciones de CSS siguen siendo opciones válidas y pueden ser mucho más simples para animaciones básicas. Pero si necesitas algo más elaborado, AnimationWorklet te ayudará.