Usar la API de Animaciones Web
La API de Animaciones Web nos permite crear animaciones y controlar su reproducción con JavaScript. Este artículo te pondrá en el buen camino con demostraciones y tutoriales divertidos protagonizados por Alicia en el país de las maravillas.
Conoce la API de Animaciones Web
La API de Animaciones Web abre el motor de animación del navegador a los desarrolladores y a su manipulación con JavaScript. Esta API se diseñó para servir de base tanto a las implementaciones de las animaciones CSS como a las de las transiciones CSS, y deja la puerta abierta a futuros efectos de animación. Es una de las formas más eficientes de animar en la web, ya que permite que el navegador haga sus propias optimizaciones internas sin trucos, imposiciones ni Window.requestAnimationFrame().
Con la API de Animaciones Web, podemos llevar las animaciones interactivas de las hojas de estilo a JavaScript, separando la presentación del comportamiento. Ya no necesitamos depender de técnicas que recargan el DOM, como escribir propiedades CSS y añadir clases a los elementos para controlar la dirección de la reproducción. Y, a diferencia del CSS puro y declarativo, JavaScript también nos permite establecer dinámicamente valores, desde propiedades hasta duraciones. Para crear bibliotecas de animación personalizadas y animaciones interactivas, la API de Animaciones Web puede ser la herramienta perfecta. ¡Veamos lo que puede hacer!
Esta página contiene una serie de ejemplos que usan la API de Animaciones Web, inspirados en Alicia en el país de las maravillas. Estos ejemplos los creó y comparte amablemente Rachel Nabors. La serie completa de ejemplos está disponible en CodePen; aquí presentamos los que son relevantes para nuestra documentación.
Escribir animaciones CSS con la API de Animaciones Web
Una de las formas más conocidas de empezar a aprender la API de Animaciones Web es partir de algo con lo que la mayoría de los desarrolladores web ya han trabajado: las animaciones CSS. Las animaciones CSS tienen una sintaxis conocida que se presta muy bien para hacer demostraciones.
La versión en CSS
Esta es una animación de caída escrita en CSS, que muestra a Alicia cayendo por la madriguera del conejo que lleva al país de las maravillas:
Observa que el fondo se mueve, Alicia gira y su color cambia con un desfase respecto a su giro. En este tutorial nos vamos a centrar solo en Alicia. Puedes ver el código fuente completo haciendo clic en "Play" en el bloque de código. Este es el CSS simplificado que controla la animación de Alicia:
#alice {
animation: alice-tumbling infinite 3s linear;
}
@keyframes alice-tumbling {
0% {
color: black;
transform: rotate(0) translate3d(-50%, -50%, 0);
}
30% {
color: #431236;
}
100% {
color: black;
transform: rotate(360deg) translate3d(-50%, -50%, 0);
}
}
Esto cambia el color de Alicia y la rotación de su transformación a lo largo de 3 segundos, a un ritmo constante (lineal), y se repite de forma infinita. En el bloque @keyframes podemos ver que, al 30 % de cada ciclo (hacia los 0,9 segundos), el color de Alicia cambia de negro a un burdeos intenso, y vuelve al negro al final del ciclo.
Pasarla a JavaScript
Ahora intentemos crear la misma animación con la API de Animaciones Web.
Representar los fotogramas clave
Lo primero que necesitamos es crear un objeto de fotogramas clave que corresponda a nuestro bloque @keyframes de CSS:
const aliceTumbling = [
{ transform: "rotate(0) translate3d(-50%, -50%, 0)", color: "black" },
{ color: "#431236", offset: 0.3 },
{ transform: "rotate(360deg) translate3d(-50%, -50%, 0)", color: "black" },
];
Aquí usamos un array que contiene varios objetos. Cada objeto representa un fotograma clave del CSS original. Sin embargo, a diferencia de CSS, a la API de Animaciones Web no hace falta indicarle explícitamente en qué porcentaje de la animación debe aparecer cada fotograma clave. La API divide automáticamente la animación en partes iguales según el número de fotogramas clave que le des. Esto significa que un objeto de fotogramas clave con tres fotogramas reproducirá el del medio al 50 % de cada ciclo de la animación, salvo que se indique lo contrario.
Cuando queremos establecer explícitamente el desfase de un fotograma clave respecto a los demás, podemos especificar un desfase directamente en el objeto, separado de la declaración por una coma. En el ejemplo anterior, para asegurarnos de que el color de Alicia cambie al 30 % (y no al 50 %), le damos offset: 0.3.
Por ahora, se deben especificar al menos dos fotogramas clave (que representan el estado inicial y el final de la secuencia de animación). Si tu lista de fotogramas clave tiene una sola entrada, Element.animate() puede lanzar un DOMException NotSupportedError en algunos navegadores hasta que se actualicen.
En resumen, los fotogramas clave se distribuyen de forma uniforme de manera predeterminada, salvo que especifiques un desfase en alguno. Práctico, ¿verdad?
Representar las propiedades de temporización
También tendremos que crear un objeto con las propiedades de temporización que corresponda a los valores de la animación de Alicia:
const aliceTiming = {
duration: 3000,
iterations: Infinity,
};
Notarás algunas diferencias con respecto a cómo se representan los valores equivalentes en CSS:
- Para empezar, la duración se indica en milisegundos y no en segundos: 3000, no 3s. Al igual que
setTimeout()yWindow.requestAnimationFrame(), la API de Animaciones Web solo acepta milisegundos. - La otra cosa que notarás es que se usa
iterations, noiteration-count.
Nota:
Hay varias pequeñas diferencias entre la terminología de las animaciones CSS y la de Web Animations. Por ejemplo, Web Animations no usa la cadena "infinite", sino la palabra clave de JavaScript Infinity. Y en lugar de timing-function usamos easing. Aquí no indicamos ningún valor de easing porque, a diferencia de las animaciones CSS, donde la animation-timing-function predeterminada es ease, en la API de Animaciones Web la función de aceleración predeterminada es linear, que es justo lo que queremos.
Juntar las piezas
Ahora es el momento de juntar ambos con el método Element.animate():
document.getElementById("alice").animate(aliceTumbling, aliceTiming);
Y listo: la animación empieza a reproducirse:
El método animate() se puede llamar en cualquier elemento del DOM que se pueda animar con CSS. Y se puede escribir de varias formas. En lugar de crear objetos para los fotogramas clave y las propiedades de temporización, podríamos pasar sus valores directamente, así:
document.getElementById("alice").animate(
[
{ transform: "rotate(0) translate3d(-50%, -50%, 0)", color: "black" },
{ color: "#431236", offset: 0.3 },
{ transform: "rotate(360deg) translate3d(-50%, -50%, 0)", color: "black" },
],
{
duration: 3000,
iterations: Infinity,
},
);
Es más, si solo quisiéramos especificar la duración de la animación y no sus iteraciones (de forma predeterminada, las animaciones se repiten una vez), podríamos pasar solo los milisegundos:
document.getElementById("alice").animate(
[
{ transform: "rotate(0) translate3d(-50%, -50%, 0)", color: "black" },
{ color: "#431236", offset: 0.3 },
{ transform: "rotate(360deg) translate3d(-50%, -50%, 0)", color: "black" },
],
3000,
);
Controlar la reproducción con play(), pause(), reverse() y updatePlaybackRate()
Aunque podemos escribir animaciones CSS con la API de Animaciones Web, donde la API resulta realmente útil es en la manipulación de la reproducción de la animación. La API de Animaciones Web ofrece varios métodos útiles para controlar la reproducción. Veamos cómo pausar y reproducir animaciones en el ejemplo "Sigue al conejo blanco":
En este ejemplo, el conejo blanco tiene una animación que lo hace bajar por una madriguera. Solo se activa cuando el usuario hace clic en él.
Pausar y reproducir animaciones
Podemos animar al conejo con el método animate(), como siempre:
const whiteRabbit = document.getElementById("rabbit");
const rabbitDownAnimation = whiteRabbit.animate(
[{ transform: "translateY(0%)" }, { transform: "translateY(100%)" }],
{ duration: 3000, fill: "forwards" },
);
El método Element.animate() se ejecuta inmediatamente después de llamarlo. Para evitar que el pastel se coma solo antes de que el usuario haya tenido la oportunidad de hacer clic en él, llamamos a Animation.pause() justo después de definirlo, así:
rabbitDownAnimation.pause();
Nota:
También puedes definir rabbitDownAnimation con el constructor Animation(), que no empieza a reproducirse hasta que llamas a play().
Ahora podemos usar el método Animation.play() para ejecutarla cuando queramos. En concreto, queremos vincularla a una acción de clic. Podemos lograrlo así:
whiteRabbit.addEventListener("click", downHeGoes);
whiteRabbit.addEventListener("touchstart", downHeGoes);
function downHeGoes(event) {
whiteRabbit.removeEventListener("click", downHeGoes);
whiteRabbit.removeEventListener("touchstart", downHeGoes);
rabbitDownAnimation.play();
}
Cuando un usuario hace clic o toca al conejo con el dedo, ahora podemos llamar a downHeGoes para reproducir todas las animaciones.
Otros métodos útiles
Además de pausar y reproducir, podemos usar los siguientes métodos de Animation:
Animation.finish()salta al final de la animación.Animation.cancel()interrumpe la animación y elimina sus efectos.Animation.reverse()establece la velocidad de reproducción de la animación (Animation.playbackRate) en un valor negativo para que se reproduzca hacia atrás.
Veamos primero playbackRate: un playbackRate negativo hace que una animación se reproduzca al revés.
En A través del espejo, Alicia viaja a un mundo donde tiene que correr para quedarse en el mismo sitio, ¡y correr el doble de rápido para avanzar! En el ejemplo de la carrera de la Reina Roja, Alicia y la Reina Roja corren para quedarse en el mismo sitio:
Como los niños pequeños se cansan con facilidad, a diferencia de las piezas de ajedrez autómatas, Alicia va cada vez más despacio. Podemos lograrlo aplicando una disminución progresiva al playbackRate de su animación. Usamos updatePlaybackRate() en lugar de establecer playbackRate directamente, porque así la actualización es fluida:
setInterval(() => {
// Asegurarse de que la velocidad de reproducción nunca baje de 0,4
if (redQueenAlice.playbackRate > 0.4) {
redQueenAlice.updatePlaybackRate(redQueenAlice.playbackRate * 0.9);
}
adjustBackgroundPlayback();
}, 1000);
Pero animarlas haciendo clic o tocando la pantalla hace que aceleren, porque se multiplica su playbackRate:
function goFaster() {
// Pero puedes acelerarlas haciendo clic o tocando la pantalla.
redQueenAlice.updatePlaybackRate(redQueenAlice.playbackRate * 1.1);
adjustBackgroundPlayback();
}
document.addEventListener("click", goFaster);
document.addEventListener("touchstart", goFaster);
Los elementos del fondo también tienen un playbackRate que cambia al hacer clic o tocar. Sus velocidades de reproducción se derivan de la de Alicia, como se muestra a continuación. ¿Qué pasa si haces que Alicia y la Reina Roja corran el doble de rápido? ¿Y si dejas que vayan más despacio?
/* ¡Alicia se cansa con mucha facilidad!
Cada pocos segundos, reducimos su velocidad de reproducción para que vayan un poco más lento.
*/
const sceneries = [
foreground1Movement,
foreground2Movement,
background1Movement,
background2Movement,
];
function adjustBackgroundPlayback() {
// Si Alicia y la Reina Roja corren a una velocidad de 0,8–1,2,
// el fondo no se mueve.
// Pero si bajan de 0,8, el fondo se desliza hacia atrás
if (redQueenAlice.playbackRate < 0.8) {
sceneries.forEach((anim) => {
anim.updatePlaybackRate(-redQueenAlice.playbackRate / 2);
});
} else if (redQueenAlice.playbackRate > 1.2) {
sceneries.forEach((anim) => {
anim.updatePlaybackRate(redQueenAlice.playbackRate / 2);
});
} else {
sceneries.forEach((anim) => {
anim.updatePlaybackRate(0);
});
}
}
adjustBackgroundPlayback();
Conservar los estilos de la animación
Al animar elementos, un caso de uso habitual es conservar el estado final de la animación una vez terminada. Un método que a veces se usa para esto es establecer el modo de relleno de la animación en forwards. Sin embargo, no se recomienda usar los modos de relleno para conservar el efecto de una animación de forma indefinida, por dos motivos:
- El navegador tiene que mantener el estado de la animación mientras sigue activa, así que la animación sigue consumiendo recursos aunque ya no esté animando. Ten en cuenta que esto se atenúa en parte porque el navegador elimina automáticamente las animaciones de relleno.
- Los estilos que aplican las animaciones tienen una mayor precedencia en la cascada que los estilos especificados, por lo que puede ser difícil sobrescribirlos cuando hace falta.
Una mejor opción es usar el método Animation.commitStyles(). Este método escribe los valores calculados de los estilos actuales de la animación en el atributo style de su elemento de destino, y a partir de ahí se pueden volver a aplicar estilos al elemento con normalidad.
Eliminación automática de animaciones de relleno
Es posible activar una gran cantidad de animaciones en el mismo elemento. Si son indefinidas (es decir, con relleno hacia delante), esto puede generar una lista de animaciones enorme, lo que podría provocar una fuga de memoria. Por este motivo, los navegadores eliminan automáticamente las animaciones de relleno cuando las reemplazan otras más nuevas, salvo que el desarrollador indique explícitamente que se conserven.
Las animaciones se eliminan cuando se cumplen todas las condiciones siguientes:
- La animación tiene relleno (su
fillesforwardssi se reproduce hacia delante,backwardssi se reproduce hacia atrás, oboth). - La animación ha terminado. (Ten en cuenta que, debido al
fill, seguirá teniendo efecto). - La línea de tiempo de la animación es monótonamente creciente. (Esto siempre se cumple con
DocumentTimeline; otras líneas de tiempo, comoscroll-timeline, pueden avanzar hacia atrás). - La animación no está controlada por un marcado declarativo, como CSS.
- Todos los efectos de estilo del
AnimationEffectde la animación están sobrescritos por otra animación que también cumple todas las condiciones anteriores. (Normalmente, cuando dos animaciones establecen la misma propiedad de estilo del mismo elemento, la creada en último lugar sobrescribe a la otra).
Las cuatro primeras condiciones garantizan que, sin la intervención del código JavaScript, el efecto de la animación nunca cambiará ni terminará. La última condición garantiza que la animación nunca afectará realmente al estilo de ningún elemento, porque ha sido reemplazada por completo.
Cuando la animación se elimina automáticamente, se dispara su evento remove.
Para evitar que el navegador elimine automáticamente las animaciones, llama al método persist() de la animación.
La propiedad replaceState de la animación valdrá removed si la animación se eliminó, persisted si llamaste a persist() en la animación, o active en cualquier otro caso.
Obtener información de las animaciones
Imagina otras formas en las que podríamos usar playbackRate, como mejorar la accesibilidad para los usuarios con trastornos vestibulares permitiéndoles ralentizar las animaciones de todo un sitio. Eso es imposible de hacer con CSS sin recalcular las duraciones en cada regla CSS, pero con la API de Animaciones Web podríamos usar el método Document.getAnimations para recorrer todas las animaciones de la página y reducir a la mitad su playbackRate, así:
document.getAnimations().forEach((animation) => {
animation.updatePlaybackRate(animation.playbackRate * 0.5);
});
¡Con la API de Animaciones Web, basta con cambiar una pequeña propiedad!
Otra cosa difícil de lograr solo con animaciones CSS es crear dependencias de valores proporcionados por otras animaciones. Por ejemplo, en el ejemplo del juego "Alicia crece y encoge", quizás hayas notado algo raro en la duración del pastel:
document.getElementById("eat-me-sprite").animate([], {
duration: aliceChange.effect.getComputedTiming().duration / 2,
});
Para entender lo que pasa aquí, veamos la animación de Alicia:
const aliceChange = document
.getElementById("alice")
.animate(
[
{ transform: "translate(-50%, -50%) scale(.5)" },
{ transform: "translate(-50%, -50%) scale(2)" },
],
{
duration: 8000,
easing: "ease-in-out",
fill: "both",
},
);
La animación de Alicia la hace pasar de la mitad de su tamaño al doble de su tamaño a lo largo de 8 segundos. Después la pausamos:
aliceChange.pause();
Si la hubiéramos dejado pausada al principio de su animación, empezaría con la mitad de su tamaño completo, ¡como si ya se hubiera bebido toda la botella! Queremos colocar el "cabezal de reproducción" de su animación en el medio, para que ya vaya por la mitad. Podríamos hacerlo estableciendo su Animation.currentTime en 4 segundos, así:
aliceChange.currentTime = 4000;
Pero, mientras trabajamos en esta animación, es posible que cambiemos mucho la duración de la animación de Alicia. ¿No sería mejor establecer su currentTime de forma dinámica, para no tener que hacer dos actualizaciones a la vez? De hecho, podemos hacerlo haciendo referencia a la propiedad Animation.effect de aliceChange, que devuelve un objeto con todos los detalles de los efectos activos sobre Alicia:
aliceChange.currentTime = aliceChange.effect.getComputedTiming().duration / 2;
effect nos permite acceder a los fotogramas clave y las propiedades de temporización de la animación: aliceChange.effect.getComputedTiming() apunta al objeto de temporización de Alicia, que contiene su duration. Podemos dividir su duración a la mitad para obtener el punto medio de la línea de tiempo de su animación y darle así su altura normal. ¡Ahora podemos invertir y reproducir su animación en cualquier dirección para hacerla más pequeña o más grande!
Y podemos hacer lo mismo al establecer las duraciones del pastel y de la botella:
const drinking = document
.getElementById("liquid")
.animate([{ height: "100%" }, { height: "0" }], {
fill: "forwards",
duration: aliceChange.effect.getComputedTiming().duration / 2,
});
drinking.pause();
Ahora las tres animaciones están vinculadas a una sola duración, que podemos cambiar fácilmente desde un único lugar.
También podemos usar la API de Animaciones Web para averiguar el tiempo actual de la animación. El juego termina cuando se acaba el pastel o se vacía la botella. La escena final que ven los jugadores depende de lo avanzada que estaba Alicia en su animación: si creció demasiado y ya no cabe por la puerta diminuta, o si se hizo demasiado pequeña y no alcanza la llave para abrirla. Podemos saber si está en el extremo grande o pequeño de su animación obteniendo el currentTime de su animación y dividiéndolo por su activeDuration:
const endGame = () => {
// obtener la posición del cabezal de reproducción de la línea de tiempo de Alicia
const alicePlayhead = aliceChange.currentTime;
const aliceTimeline = aliceChange.effect.getComputedTiming().activeDuration;
// detener la animación de Alicia y las demás
stopPlayingAlice();
// según el tercio en el que caiga
const aliceHeight = alicePlayhead / aliceTimeline;
if (aliceHeight <= 0.333) {
// ¡Alicia se hizo más pequeña!
// …
} else if (aliceHeight >= 0.666) {
// ¡Alicia se hizo más grande!
// …
} else {
// Alicia no cambió de forma significativa
// …
}
};
Callbacks y promesas
Las animaciones y las transiciones CSS tienen sus propios detectores de eventos, y también es posible usarlos con la API de Animaciones Web:
onfinishes el manejador del eventofinishy se puede activar manualmente confinish().oncanceles el manejador del eventocancely se puede activar concancel().
Aquí establecemos los callbacks del pastel, la botella y Alicia para que llamen a la función endGame:
// Cuando se acaba el pastel o la botella
nommingCake.onfinish = endGame;
drinking.onfinish = endGame;
// Alicia llega al final de su animación
aliceChange.onfinish = endGame;
Mejor aún, la API de Animaciones Web también ofrece una promesa finished que se resuelve cuando la animación termina, o se rechaza si se cancela.
Conclusión
Estas son las funciones básicas de la API de Animaciones Web. ¡A estas alturas ya deberías estar listo para "bajar por la madriguera del conejo" de las animaciones en el navegador y para escribir tus propios experimentos de animación!
Véase también
- La serie completa de demostraciones de Alicia en el país de las maravillas en CodePen, para que juegues con ellas, hagas un fork y las compartas.
- Animating like you just don't care with Element.animate (2016). Explica el origen de la API de Animaciones Web y por qué es más eficiente que otros métodos de animación web.