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 planificando 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. Comencemos 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 infame desplazador con paralaje, como su nombre lo indica, se basa en el 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. Pero en cuanto te desplaces hacia arriba, volverá a aparecer, incluso si estás a mitad de camino en 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 el diseño de 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 cuestiones son incómodas y difíciles o imposibles 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, asegurémonos de estar actualizados los conceptos básicos de las animaciones.
Un manual básico sobre animaciones y 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 el documento 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 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 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 una 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 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 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 el worklet, la WAAPI y el AnimationWorklet anterior 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 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 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
Se recomienda reutilizar un worklet 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 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 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. Sin embargo, hasta ahora, nuestro cronograma siempre ha sido document.timeline
, que realiza 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. Si se desplaza hasta la parte superior (o hacia la izquierda) significa currentTime = 0
, mientras que el desplazamiento hasta la parte inferior (o derecha) establece currentTime
en timeRange
. Si te desplazas por el cuadro de esta demostración, puedes controlar su posición.
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). Animation Worklet permite implementar estos efectos de una manera sencilla 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 superficie de API pequeña 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, 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 trabajo en la GPU para ser animadas, mientras que otras fuerzan al navegador a rediseñar todo el documento.
En Chrome (al igual que en muchos otros navegadores), tenemos un proceso llamado compositor, cuyo trabajo es (y simplificaré mucho) para organizar las capas y texturas, y luego usar la GPU para actualizar la pantalla con la mayor frecuencia posible, idealmente tan rápido como se pueda actualizar la pantalla (generalmente 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 y, al mismo tiempo, obtener acceso a todas las ventajas nuevas.
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 las animaciones básicas. Pero si necesitas algo más elaborado, AnimationWorklet te ayudará.