Orquestar la complejidad con la API de animaciones web
Publicado: 2022-03-10No hay término medio entre transiciones simples y animaciones complejas. O está bien con lo que proporcionan las transiciones y animaciones CSS o de repente necesita todo el poder que puede obtener. Web Animations API le brinda muchas herramientas para trabajar con animaciones. Pero hay que saber manejarlos. Este artículo lo guiará a través de los principales puntos y técnicas que pueden ayudarlo a lidiar con animaciones complejas mientras se mantiene flexible.
Antes de sumergirnos en el artículo, es vital que esté familiarizado con los conceptos básicos de la API de animaciones web y JavaScript. Para que quede claro y evitar la distracción del problema en cuestión, los ejemplos de código proporcionados son sencillos. No habrá nada más complejo que funciones y objetos. Como buenos puntos de entrada a las animaciones en sí, sugeriría MDN como referencia general, la excelente serie de Daniel C. Wilson y CSS Animations vs Web Animations API de Ollie Williams. No analizaremos las formas de definir los efectos y ajustarlos para lograr el resultado que desea. Este artículo asume que tiene sus animaciones definidas y necesita ideas y técnicas para manejarlas.
Comenzamos con una descripción general de las interfaces y para qué sirven. Luego veremos el tiempo y los niveles de control para definir qué, cuándo y por cuánto tiempo. Después de eso, aprenderemos cómo tratar varias animaciones como una sola envolviéndolas en objetos. Ese sería un buen comienzo para usar la API de animaciones web.
Interfaces
La API de animaciones web nos brinda una nueva dimensión de control. Antes de eso, las transiciones y animaciones de CSS, si bien brindaban una forma poderosa de definir efectos, todavía tenían un único punto de actuación . Como un interruptor de luz, estaba encendido o apagado. Podrías jugar con retrasos y funciones de aceleración para crear efectos bastante complejos. Aún así, en cierto punto, se vuelve engorroso y difícil de trabajar.
Web Animations API convierte este único punto de actuación en un control total sobre la reproducción . El interruptor de luz se convierte en un interruptor de atenuación con un control deslizante. Si lo desea, puede convertirlo en todo un hogar inteligente, porque además del control de reproducción, ahora puede definir y cambiar los efectos en tiempo de ejecución. Ahora puede adaptar los efectos al contexto o puede implementar un editor de animaciones con vista previa en tiempo real.
Comenzamos con la interfaz de Animación. Para obtener un objeto de animación, podemos usar el método Element.animate
. Le das fotogramas clave y opciones y reproduce tu animación inmediatamente. Lo que también hace es devolver una instancia de objeto de Animation
. Su propósito es controlar la reproducción.
Piense en ello como un reproductor de casetes , si recuerda estos. Soy consciente de que algunos de los lectores pueden no estar familiarizados con lo que es. Es inevitable que cualquier intento de aplicar conceptos del mundo real para describir cosas informáticas abstractas fracase rápidamente. Pero deje que le asegure a usted, un lector que no conoce el placer de rebobinar una cinta con un lápiz, que las personas que saben lo que es un reproductor de casetes estarán aún más confundidas al final de este artículo.
Imagina una caja. Tiene una ranura donde va el casete y tiene botones para reproducir, detener y rebobinar. Eso es lo que es la instancia de la interfaz de animación: un cuadro que contiene una animación definida y proporciona formas de interactuar con su reproducción. Le das algo para jugar y te devuelve los controles.
Los controles que obtiene son convenientemente similares a los que obtiene de los elementos de audio y video. Son métodos de reproducción y pausa , y la propiedad de tiempo actual . Con esos tres controles, puede construir cualquier cosa en lo que respecta a la reproducción.
El casete en sí es un paquete que contiene una referencia al elemento que se anima, la definición de efectos y opciones que incluyen temporización, entre otras cosas. Y eso es lo que es KeyframeEffect
. Nuestra cinta de casete es algo que contiene todas las grabaciones e información sobre la duración de las grabaciones. Dejaré que la imaginación de la audiencia mayor haga coincidir todas esas propiedades con los componentes de un casete físico. Lo que te mostraré es cómo se ve en el código.
Cuando creas una animación a través de Element.animate
, estás usando un atajo que hace tres cosas. Crea una instancia de KeyframeEffect
. Se instala en una nueva instancia de Animation
. Inmediatamente comienza a reproducirlo.
const animation = element.animate(keyframes, options);
Desglosémoslo y veamos el código equivalente que hace lo mismo.
const animation = new Animation( // (2) new KeyframeEffect(element, keyframes, options) // (1) ); animation.play(); (3)
Obtenga el casete (1), colóquelo en un reproductor (2), luego presione el botón Reproducir (3).
El punto de saber cómo funciona detrás de escena es poder separar la definición de fotogramas clave y decidir cuándo reproducirlo. Cuando tiene muchas animaciones para coordinar, puede ser útil reunirlas todas primero para saber que están listas para jugar. Generarlos sobre la marcha y esperar que comiencen a reproducirse en el momento adecuado no es algo a lo que querrías aspirar. Es demasiado fácil romper el efecto deseado arrastrando unos pocos fotogramas. En el caso de una secuencia larga, la resistencia se acumula, lo que da como resultado una experiencia nada convincente.
Sincronización
Como en la comedia, el tiempo lo es todo en las animaciones. Para hacer que un efecto funcione, para lograr una cierta sensación, debe poder ajustar la forma en que cambian las propiedades. Hay dos niveles de tiempo que puede controlar en la API de animaciones web.
En el nivel de las propiedades individuales, hemos offset
. Offset le da control sobre el tiempo de una sola propiedad . Al darle un valor de cero a uno, define cuándo se activa cada efecto. Cuando se omite, es igual a cero.
Es posible que recuerde de @keyframes
en CSS cómo puede usar porcentajes en lugar de from
/ to
. Eso es lo que es la offset
pero dividida por cien. El valor de offset
es una parte de la duración de una sola iteración .
El offset
le permite organizar fotogramas clave dentro de un KeyframeEffect
. Al ser un desplazamiento de número relativo, se asegura de que, independientemente de la duración o la velocidad de reproducción, todos los fotogramas clave comiencen en el mismo momento entre sí.
Como dijimos anteriormente, el offset
es una parte de la duración . Ahora quiero que evites mis errores y la pérdida de tiempo en esto. Es importante comprender que la duración de la animación no es lo mismo que la duración total de una animación. Por lo general, son los mismos y eso es lo que podría confundirte, y lo que definitivamente me confundió a mí.
La duración es la cantidad de tiempo en milisegundos que tarda una iteración en finalizar. Será igual a la duración total por defecto. Una vez que agrega un retraso o aumenta el número de iteraciones en una animación, la duración deja de indicarle el número que desea saber. Eso es importante de entender para usarlo a tu favor.
Cuando necesite coordinar la reproducción de un fotograma clave dentro de un contexto más amplio, como la reproducción de medios, debe usar las opciones de tiempo. La duración total de la animación desde el inicio hasta el evento "terminado" en la siguiente ecuación:
delay + (iterations × duration) + end delay
Puedes verlo en acción en la siguiente demostración:
Lo que esto nos permite hacer es alinear varias animaciones dentro del contexto de medios de duración fija. Manteniendo intacta la duración deseada de la animación, puede "rellenarla" con delay
al principio y delayEnd
el final al final para incrustarlo en un contexto con una duración más larga. Si lo piensa, la delay
en este sentido actuaría como lo hace el desplazamiento en los fotogramas clave. Solo recuerde que el retraso se establece en milisegundos, por lo que es posible que desee convertirlo a un valor relativo.
Una opción de tiempo más que ayudaría a alinear la animación es iterationStart
. Establece la posición inicial de una iteración. Tome la demostración de la bola de billar. Al ajustar el control deslizante iterationStart
, puede establecer la posición inicial de la bola y la rotación, por ejemplo, puede configurarlo para que comience a saltar desde el centro de la pantalla y hacer que el número sea directo en la cámara en el último cuadro.
Controle varios como uno
Cuando trabajé en el editor de animación para una aplicación de presentación, tuve que organizar varias animaciones para un solo elemento en una línea de tiempo. Mi primer intento fue usar el offset
para poner mi animación en el punto de inicio correcto en una línea de tiempo.
Rápidamente resultó ser la forma incorrecta de usar offset
. En términos de esta animación en movimiento de la interfaz de usuario en particular en la línea de tiempo, la intención era cambiar su posición inicial sin cambiar la duración de la animación. Con el offset
, eso significaba que necesitaba cambiar varias cosas, el offset
en sí mismo y también cambiar el offset
de la propiedad de cierre para asegurarme de que la duración no cambie. La solución resultó ser demasiado compleja para comprenderla.
El segundo problema vino con la propiedad transform
. Debido al hecho de que puede representar varios cambios característicos de un elemento, puede ser complicado hacer que haga lo que desea. En caso de que desee cambiar esas propiedades independientemente unas de otras, podría volverse aún más difícil. La función de cambio de escala influye en todas las funciones que le siguen. He aquí por qué sucede eso.
La propiedad Transform puede tomar varias funciones en una secuencia como valor. Dependiendo del orden de la función, el resultado cambia. Toma scale
y translate
. A veces es útil definir la translate
en porcentaje, lo que significa relativo al tamaño de un elemento. Digamos que quieres que una pelota salte exactamente a tres diámetros de altura. Ahora, dependiendo de dónde coloque la función de escala, antes o después de la translate
, el resultado cambia de tres alturas del tamaño original o escalado.
Es un rasgo importante de la propiedad de transform
. Lo necesitas para lograr una transformación bastante compleja. Pero cuando necesita que esas transformaciones sean distintas e independientes de otras transformaciones de un elemento, se interpone en su camino.
Hay casos en los que no puede poner todos los efectos en una propiedad de transform
. Puede llegar a ser demasiado con bastante rapidez. Especialmente si sus fotogramas clave provienen de diferentes lugares, necesitaría tener una fusión muy compleja de una cadena transformada . Difícilmente podría confiar en un mecanismo automático porque la lógica no es sencilla. Además, podría ser difícil entender qué esperar. Para simplificar esto y mantener la flexibilidad, necesitamos separarlos en diferentes canales.
Una solución es envolver nuestros elementos en div
s que cada uno podría animarse por separado, por ejemplo, un div para posicionar en el lienzo, otro para escalar y un tercero para rotar. De esa manera, no solo simplifica enormemente la definición de animaciones, sino que también abre la posibilidad de definir diferentes orígenes de transformación cuando corresponda.
Puede parecer que las cosas se salen de control con ese truco. Que estamos multiplicando el número de problemas que teníamos antes. De hecho, cuando encontré este truco por primera vez, lo descarté por ser demasiado. Pensé que podría asegurarme de que mi propiedad de transform
se compile a partir de todas las piezas en el orden correcto en una sola pieza. Se necesitó una función de transform
más para hacer que las cosas fueran demasiado complejas de administrar y ciertas cosas imposibles de hacer. Mi compilador de cadenas de propiedades de transform
comenzó a tomar cada vez más tiempo para hacerlo bien, así que me di por vencido.
Resultó que controlar la reproducción de varias animaciones no es tan difícil como parece inicialmente. ¿Recuerdas la analogía del reproductor de cintas de casete del principio? ¿Qué pasaría si pudiera usar su propio reproductor que admita cualquier cantidad de casetes? Más que eso, puede agregar tantos botones como desee en ese reproductor.
La única diferencia entre llamar a play
en una sola animación y una serie de animaciones es que necesita iterar. Aquí está el código que puede usar para cualquier método de instancias de Animation
:
// To play just call play on all of them animations.forEach((animation) => animation.play());
Usaremos esto para crear todo tipo de funciones para nuestro reproductor.
Vamos a crear ese cuadro que contendrá las animaciones y reproducirlas. Puede crear esos cuadros de cualquier manera que sea adecuada. Para que te quede claro, te mostraré un ejemplo de cómo hacerlo con una función y un objeto. La función createPlayer
toma una serie de animaciones que se reproducirán de forma sincronizada. Devuelve un objeto con un solo método de play
.
function createPlayer(animations) { return Object.freeze({ play: function () { animations.forEach((animation) => animation.play()); } }); }
Eso es suficiente para que lo sepas para comenzar a expandir la funcionalidad. currentTime
métodos de pausa y hora actual.
function createPlayer(animations) { return Object.freeze({ play: function () { animations.forEach((animation) => animation.play()); }, pause: function () { animations.forEach((animation) => animation.pause()); }, currentTime: function (time = 0) { animations.forEach((animation) => animation.currentTime = time); } }); }
El createPlayer
con esos tres métodos le brinda suficiente control para orquestar cualquier cantidad de animaciones . Pero llevémoslo un poco más lejos. Hagámoslo para que nuestro reproductor no solo pueda tomar cualquier cantidad de casetes, sino también otros reproductores.
Como vimos anteriormente, la interfaz de Animation
es similar a las interfaces de medios. Usando esa similitud podrías poner todo tipo de cosas en tu reproductor. Para adaptarnos a eso, modifiquemos el método currentTime
para que funcione tanto con objetos de animación como con objetos que provienen de createPlayer
.
function currentTime(time = 0) { animations.forEach(function (animation) { if (typeof animation.currentTime === "function") { animation.currentTime(time); } else { animation.currentTime = time; } }); }
El reproductor que acabamos de crear es lo que le permitirá ocultar la complejidad de varios div
s para canales de animaciones de un solo elemento. Esos elementos podrían agruparse en una escena. Y cada escena podría ser parte de algo más grande. Todo eso se podría hacer con esta técnica.
Para demostrar la demostración de tiempo, dividí todas las animaciones en tres jugadores. El primero es controlar la reproducción de la vista previa a la derecha. El segundo combina la animación de salto de todos los contornos de las bolas a la izquierda y de la vista previa.
Finalmente, el tercero es un jugador que combinó animaciones de posición de las bolas en un contenedor izquierdo. Ese jugador permite que las bolas se extiendan en una demostración continua de la animación con unos 60 fotogramas por segundo.
Conclusión
Las interfaces web como Web Animations API nos exponen ciertas cosas que los navegadores hicieron todo el tiempo. Los navegadores saben cómo renderizar rápido pasando el trabajo a la GPU. Con Web Animations API, tenemos control sobre ella. Aunque ese control pueda parecer un poco extraño o confuso, no significa que usarlo también deba ser confuso. Con una comprensión del tiempo y el control de la reproducción, tiene herramientas para adaptar esa API a sus necesidades. Debería poder definir qué tan complejo debe ser.
Otras lecturas
- "Técnicas prácticas en el diseño de animación", Sarah Drasner
- “Diseño con movimiento reducido para sensibilidades de movimiento”, Val Head
- “Una interfaz de usuario de voz alternativa a los asistentes de voz”, Ottomatias Peura
- "Diseñar mejores consejos sobre herramientas para interfaces de usuario móviles", Eric Olive