Potencia las animaciones de tu app web
TL;DR: Animation Worklet te permite escribir animaciones imperativas que se ejecutan en la velocidad de fotogramas nativa del dispositivo para lograr una fluidez adicional sin tirones™, hacer que tus animaciones sean más resistentes a los tirones del subproceso principal y vincularlas al desplazamiento en lugar del tiempo. El Animation Worklet está disponible en Chrome Canary (detrás de la marca "Experimental Web Platform features") y planeamos una prueba de origen para Chrome 71. Puedes comenzar a usarlo como una mejora progresiva hoy mismo.
¿Otra API de Animation?
En realidad, no. Es una extensión de lo que ya tenemos, y con razón. Comencemos por el principio. Si quieres animar cualquier elemento DOM en la Web hoy en día, tienes 2 opciones y media: transiciones CSS para transiciones simples de A a B, animaciones CSS para animaciones basadas en el tiempo potencialmente cíclicas y más complejas, y la API de Web Animations (WAAPI) para animaciones casi arbitrariamente complejas. La matriz de compatibilidad de la WAAPI se ve bastante sombría, pero está en aumento. Hasta entonces, hay un polyfill.
Lo que todos estos métodos tienen en común es que no guardan estado y se basan en el tiempo. Sin embargo, algunos de los efectos que intentan los desarrolladores no se basan en el tiempo ni son sin estado. Por ejemplo, el famoso desplazamiento de paralaje se basa en el desplazamiento, como su nombre lo indica. Implementar un desplazamiento de paralaje de alto rendimiento en la Web hoy en día es sorprendentemente difícil.
¿Y qué sucede con la falta de estado? Por ejemplo, piensa en la barra de direcciones de Chrome en Android. Si te desplazas hacia abajo, se oculta. Pero en cuanto te desplazas hacia arriba, vuelve a aparecer, incluso si estás a mitad de la página. La animación no solo depende 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 notoriamente difíciles de aplicarles estilos, o al menos no lo suficiente. ¿Qué sucede si quiero un gato nyan como barra de desplazamiento? Cualquiera sea la técnica que elijas, crear una barra de desplazamiento personalizada no es ni fácil ni eficiente.
El punto es que todas estas cosas son incómodas y difíciles, o incluso imposibles, de implementar de manera eficiente. La mayoría de ellos se basan en eventos o en requestAnimationFrame
, lo que podría mantenerte en 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 valioso presupuesto de fotogramas del subproceso principal.
El Worklet de animación extiende las capacidades de la pila de animaciones de la Web para facilitar este tipo de efectos. Antes de comenzar, asegurémonos de que tenemos los conocimientos básicos sobre animaciones.
Introducción a las animaciones y los cronogramas
WAAPI y Animation Worklet usan ampliamente las líneas de tiempo para permitirte orquestar animaciones y efectos de la manera que desees. En esta sección, se ofrece un repaso rápido o una introducción a las líneas de tiempo 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 el documento. Todas las animaciones de un documento funcionan en relación con esta línea de tiempo.
Para que 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 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 la línea de tiempo 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 the
durationoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline's
currentTimeis
startTime + 3000 + 1000and the last keyframe at
startTime + 3000 + 2000". El punto es que la línea de tiempo controla dónde estamos en nuestra animación.
Una vez que la animación alcance el último fotograma clave, volverá al primero y comenzará la siguiente iteración de la animación. Este proceso se repite un total de 3 veces, ya que establecimos iterations: 3
. Si quisiéramos que la animación nunca se detuviera, escribiríamos iterations: Number.POSITIVE_INFINITY
. Este es el resultado del código anterior.
La WAAPI es increíblemente potente y tiene muchas más funciones, como la aceleración, los desplazamientos de inicio, las ponderaciones de fotogramas clave y el comportamiento de relleno, que excederían el alcance de este artículo. Si deseas obtener más información, te recomiendo que leas este artículo sobre las animaciones de CSS en CSS Tricks.
Cómo escribir un worklet de animación
Ahora que comprendemos el concepto de líneas de tiempo, podemos comenzar a analizar el Animation Worklet y cómo te permite modificar las líneas de tiempo. La API de Animation Worklet no solo se basa en la WAAPI, sino que, en el sentido de la Web extensible, es un elemento primitivo de nivel inferior que explica cómo funcionan las WAAPI. En términos de sintaxis, son increíblemente 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 se encuentra en el primer parámetro, que es el nombre del worklet que controla esta animación.
Detección de características
Chrome es el primer navegador en lanzar 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 admite AnimationWorklet
con una verificación simple:
if ('animationWorklet' in CSS) {
// AnimationWorklet is supported!
}
Cómo cargar un worklet
Los worklets son un concepto nuevo que introdujo el grupo de trabajo de Houdini para facilitar la creación y el escalamiento de muchas de las APIs nuevas. Más adelante, explicaremos los detalles de los worklets, pero, por ahora, para simplificar, puedes considerarlos como subprocesos económicos y ligeros (como los trabajadores).
Antes de declarar la animación, debemos asegurarnos de haber cargado un worklet con el nombre "passthrough":
// 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 un objeto Animator 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 devolvió 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
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 establecer el localTime
del efecto, por lo que este objeto Animator se llama "passthrough". Con este código para el worklet, la WAAPI y el AnimationWorklet anteriores 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
de la línea de tiempo que pasamos al constructor WorkletAnimation()
. En el ejemplo anterior, simplemente pasamos 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()
del currentTime
y reasignamos ese valor al rango [0; 2000], que es el rango 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 forma programática qué efectos se reproducen, en qué orden y en qué medida.
Opciones sobre opciones
Es posible que desees reutilizar 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 controlan con el mismo código, pero con diferentes opciones.
¡Dime tu estado local!
Como mencioné antes, uno de los problemas clave que el worklet de animación busca resolver son las animaciones con estado. Los worklets de animación pueden mantener el estado. Sin embargo, una de las características 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, el worklet de animación ofrece un hook que se llama antes de que se destruya un worklet y que puedes usar para devolver 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 un 50% de probabilidades de que el cuadrado gire en una dirección u otra. Si el navegador cerrara el worklet y lo migrara a otro subproceso, 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-tiempo: ScrollTimeline
Como se mostró en la sección anterior, AnimationWorklet nos permite definir de forma programática cómo el avance del cronograma 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 las animaciones con el desplazamiento en lugar del tiempo. Reutilizaremos 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
, creamos un nuevo ScrollTimeline
.
Como tal vez ya lo sepas, ScrollTimeline
no usa el tiempo, sino la posición de desplazamiento de scrollSource
para establecer currentTime
en el worklet. Estar desplazado hasta la parte superior (o izquierda) significa currentTime = 0
, mientras que estar desplazado hasta la parte inferior (o derecha) establece currentTime
en timeRange
. Si desplazas el cuadro en esta demostración, 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 adaptable en mente, siempre debes estar preparado para NaN
como tu currentTime
. A menudo, es conveniente establecer el valor predeterminado en 0.
Vincular animaciones con la posición de desplazamiento es algo que se buscó durante mucho tiempo, pero nunca se logró con este nivel de fidelidad (aparte de soluciones alternativas poco prácticas con CSS3D). Animation Worklet permite implementar estos efectos de una manera sencilla y con un alto rendimiento. Por ejemplo, una demostración de un efecto de desplazamiento de paralaje como este muestra que ahora solo se necesitan un par de líneas para definir una animación controlada 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, especialmente en dispositivos de gama baja. Además, los worklets no están vinculados a un bucle de eventos específico, sino que se pueden mover entre subprocesos según sea necesario. Esto es especialmente importante para AnimationWorklet.
Compositor NSync
Es posible que sepas que ciertas propiedades de CSS son rápidas de animar, mientras que otras no. Algunas propiedades solo necesitan algo 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 (y aquí simplifico mucho) organizar capas y texturas, y luego utilizar la GPU para actualizar la pantalla con la mayor frecuencia posible, idealmente tan rápido como la pantalla pueda actualizarse (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 necesitan 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 en sincronización con el compositor.
Una palmada en la muñeca
Por lo general, solo hay un proceso de compositor que se puede compartir entre 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 la entrada del usuario. Esto se debe evitar 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, se permite que el worklet se "deslice", según la especificación. Se retrasa detrás del compositor, y el compositor puede volver a usar los datos del último fotograma para mantener la velocidad de fotogramas. Visualmente, esto se verá como un jank, pero la gran diferencia es que el navegador sigue respondiendo a la entrada del usuario.
Conclusión
AnimationWorklet tiene muchas facetas y ofrece muchos beneficios para la Web. Los beneficios evidentes son un mayor control sobre las animaciones y nuevas formas de impulsarlas 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 la latencia, a la vez que obtienes acceso a todas las nuevas funciones.
Animation Worklet está en Canary y nuestro objetivo es realizar una prueba de origen con Chrome 71. Esperamos con ansias tus nuevas y excelentes experiencias web, y queremos saber qué podemos mejorar. También hay un polyfill que te brinda la misma API, pero no proporciona el aislamiento del rendimiento.
Ten en cuenta que las transiciones y animaciones CSS siguen siendo opciones válidas y pueden ser mucho más simples para las animaciones básicas. Pero si necesitas algo más sofisticado, AnimationWorklet te ayudará.