Worklet de animación de Houdini

Potencia las animaciones de tus aplicaciones web

TL;DR: El Worklet de animación te permite escribir animaciones imperativas que se ejecutan a la velocidad de fotogramas nativa del dispositivo para lograr una fluidez adicional sin bloqueosTM, hacen que tus animaciones sean más resistentes contra los bloqueos del subproceso principal y se pueden vincular para desplazarse en lugar de tiempo. Worklet de animación está en Chrome Canary (detrás de la marca "Funciones de la plataforma web experimental") y planeamos realizar una prueba de origen para Chrome 71. Puedes comenzar a usarla como una mejora progresiva hoy mismo.

¿Otra API de Animation?

En realidad, no, es una extensión de lo que ya tenemos, y con buen motivo. Empecemos por el principio. Si deseas animar cualquier elemento del DOM en la Web en este momento, tienes 2 1⁄2 opciones: Transiciones CSS para transiciones simples de A a B, Animaciones CSS para animaciones potencialmente cíclicas y más complejas basadas en el tiempo, y API de Web Animations (WAAPI) para animaciones casi arbitrariamente complejas. La matriz de compatibilidad de WAAPI es bastante sombría, pero está en ascenso. Hasta entonces, existe un polyfill.

Lo que todos estos métodos tienen en común es que no tienen estado y están basados en el tiempo. Sin embargo, algunos de los efectos que los desarrolladores intentan no están impulsados por el tiempo ni sin estado. Por ejemplo, el infame desplazador de paralaje es, como su nombre lo indica, controlado por desplazamientos. Hoy en día, implementar un desplazador de paralaje de alto rendimiento en la Web es sorprendentemente difícil.

¿Y qué pasa 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. Pero apenas te desplazas hacia arriba, vuelve, incluso si estás en la mitad de la página. La animación depende no solo de la posición de desplazamiento, sino también de la dirección de desplazamiento anterior. Es con estado.

Otro problema es el diseño de las barras de desplazamiento. Son notablemente poco estilo o, al menos, poco estilo. ¿Qué sucede si quiero un gato nyan como mi barra de desplazamiento? Independientemente de la técnica que elijas, compilar una barra de desplazamiento personalizada no es eficaz ni fácil.

La idea es que todas estas cosas son incómodas, difíciles o imposibles de implementar de manera eficiente. La mayoría de ellos dependen de eventos o requestAnimationFrame, que podrían mantenerlos en 60 FPS, incluso cuando la pantalla puede ejecutarse a 90 FPS, 120 FPS o más, y usar una fracción de tu valioso presupuesto de fotogramas del subproceso principal.

Worklet de Animation extiende las capacidades de la pila de animaciones de la Web para facilitar este tipo de efectos. Antes de comenzar, asegurémonos de que estamos actualizados los conceptos básicos de las animaciones.

Un manual básico sobre animaciones y cronogramas

WAAPI y el Worklet de Animation hacen un uso extensivo de los cronogramas para permitirte organizar las animaciones y los efectos de la manera 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 el documento comenzó a existir. Todas las animaciones de un documento funcionan en relación con este cronograma.

Para ser un poco más concretos, 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 del cronograma como hora de inicio. Nuestra animación tiene un retraso de 3,000 ms, lo que significa que la animación comenzará (o se volverá "activa") cuando el cronograma llegue a "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 theduraciónoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline'scurrentTimeisstartTime + 3,000 + 1000and the last keyframe atstartTime + 3,000 + 2000`. El punto es que el cronograma controla dónde estamos en nuestra animación.

Una vez que la animación alcanzó el último fotograma clave, regresará al primer fotograma clave 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 detuviera, escribiríamos iterations: Number.POSITIVE_INFINITY. A continuación, se muestra el resultado del código anterior.

WAAPI es increíblemente potente, y esta API cuenta con muchas más funciones, como la aceleración, los desplazamientos de inicio, el peso de fotogramas clave y el comportamiento de relleno, que superarían el alcance de este artículo. Si deseas obtener más información, te recomendamos que leas este artículo sobre animaciones de CSS en trucos de CSS.

Cómo escribir un Worklet de animación

Ahora que tenemos el concepto de las líneas de tiempo, podemos comenzar a ver el Worklet de Animation y cómo te permite modificar los plazos. 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 el funcionamiento de 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 impulsa esta animación.

Detección de funciones

Chrome es el primer navegador en implementar esta función, por lo que debes asegurarte de que tu código no solo 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 un worklet

Los Worklets son un concepto nuevo ingresado por el grupo de trabajo de Houdini que facilita la compilación y el escalamiento de muchas de las nuevas APIs. Analizaremos los detalles de los Worklets un poco más adelante, pero, por cuestiones de simplicidad, puedes considerarlos como subprocesos económicos y ligeros (como los trabajadores) por el momento.

Debemos asegurarnos de haber cargado un 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í? Registraremos una clase como animador con la llamada registerAnimator() de AnimationWorklet y le daremos 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 muestra addModule() y podremos comenzar a crear animaciones con ese worklet.

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

Tiempo

El parámetro currentTime de nuestro método animate() es el currentTime del cronograma que pasamos al constructor WorkletAnimation(). En el ejemplo anterior, acabamos de pasar ese tiempo al efecto. Pero como se trata de código JavaScript, podemos distorsionar 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 volvemos a asignar 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 del worklet puede ser arbitrariamente complejo y te permite definir de manera programática qué efectos se reproducen, en qué orden y en qué medida.

Opciones en lugar de Opciones

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

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

Indica tu estado local.

Como indiqué antes, uno de los problemas clave del worklet de animación con el objetivo de resolver son las animaciones con estado. Los worklets de animación pueden conservar el estado. Sin embargo, una de las funciones principales de los workletes es que se pueden migrar a otro subproceso o incluso destruirlos para ahorrar recursos, lo que también destruiría su estado. Para evitar la pérdida de estado, el worklet de animación ofrece un hook que se llama antes de que se destruya uno que puedes usar para mostrar un objeto de estado. Ese objeto se pasará al constructor cuando se vuelva a crear el 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 actualices esta demostración, tendrás 50/50 de probabilidades de en qué dirección girará el cuadrado. Si el navegador eliminara el worklet y lo migrara a un subproceso diferente, habría otra llamada a Math.random() durante la creación, lo que podría causar un cambio repentino de dirección. Para asegurarnos de que eso no suceda, mostramos las animaciones elegidas al azar como state y las usamos en el constructor, si se proporciona.

Conexión con el continuo espacio-tiempo: ScrollTimeline

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

ScrollTimeline abre nuevas posibilidades y te permite impulsar animaciones con desplazamiento en lugar de tiempo. Vamos a reutilizar nuestro primer 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, crearemos un nuevo ScrollTimeline. Es posible que lo hayas adivinado: ScrollTimeline no usa el tiempo, sino la posición de desplazamiento de scrollSource para establecer currentTime en el worklet. El desplazamiento hacia la parte superior (o la izquierda) significa currentTime = 0, mientras que el desplazamiento hasta la parte inferior (o la derecha) establece currentTime en timeRange. Si te desplazas por el cuadro de esta demostración, puedes controlar la posición del cuadro rojo.

Si creas un ScrollTimeline con un elemento que no se desplaza, el currentTime del cronograma será NaN. Por lo tanto, en especial con el diseño responsivo, siempre debes estar preparado para que NaN sea tu currentTime. A menudo, se puede establecer un valor predeterminado en 0.

La vinculación de animaciones con la posición de desplazamiento es algo que se busca desde hace tiempo, pero que nunca se logró en este nivel de fidelidad (aparte de las soluciones alternativas poco comunes con CSS3D). El Worklet de la animación permite implementar estos efectos de una manera sencilla con un alto rendimiento. Por ejemplo, un efecto de desplazamiento con paralaje como esta demostración muestra que ahora solo se necesitan algunas líneas para definir una animación basada en desplazamientos.

Detrás de escena

Worklets

Los Worklets son contextos de JavaScript con un alcance aislado y una superficie de API muy pequeña. La plataforma de API pequeña 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 evento específico, pero se pueden mover entre subprocesos según sea necesario. Esto es muy importante para AnimationWorklet.

NSync del compositor

Es posible que sepas que ciertas propiedades de CSS se animan rápido, mientras que otras no. Algunas propiedades solo requieren que se realice una animación en la GPU, mientras que otras obligan al navegador a rediseñar todo el documento.

En Chrome (como en muchos otros navegadores), tenemos un proceso llamado compositor, cuyo trabajo es (y lo simplificaré mucho aquí) para 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 se pueda actualizar (por lo general, 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 planees animar, tu worklet de animación se vinculará al subproceso principal o se ejecutará en un subproceso independiente sincronizado con el compositor.

Dar una cachetada en la muñeca

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

Si esto sucede, se permite, según las especificaciones, el worklet. Se encuentra detrás del compositor, y este puede reutilizar los datos del último fotograma para mantener la velocidad de fotogramas alta. Visualmente, se verá como un bloqueo, pero la gran diferencia es que el navegador sigue siendo responsivo a las entradas del usuario.

.

Conclusión

AnimationWorklet tiene muchas facetas y los beneficios que brinda a la Web. Los beneficios obvios son un mayor control sobre las animaciones y nuevas formas de controlarlas para brindar 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 y, al mismo tiempo, obtener acceso a todas las ventajas nuevas.

Animation Worklet está en Canary y queremos realizar una prueba de origen con Chrome 71. Esperamos con ansias tus nuevas experiencias web increíbles y conocer los aspectos que podemos mejorar. También existe un polyfill que proporciona la misma API, pero no el aislamiento de rendimiento.

Ten en cuenta que las transiciones y las animaciones de CSS siguen siendo opciones válidas y pueden ser mucho más sencillas para las animaciones básicas. Pero si necesitas algo más sofisticado, AnimationWorklet está para ayudarte.