wandres.dev
WAAPI I · El método animate

Las dos sintaxis de keyframes de WAAPI

Array de objetos y objeto de arrays no son intercambiables: la segunda permite que cada propiedad tenga su propio número de paradas, igual que en CSS.

⏱ 19 min

WAAPI acepta los keyframes en dos formas que la documentación suele presentar como equivalentes con distinta ergonomía. No lo son. La forma de array obliga a que todas las propiedades compartan los mismos puntos de control; la forma de objeto le da a cada propiedad su propia lista de paradas, repartida de forma independiente. Es exactamente el modelo por propiedad que ya rige en CSS, y es lo que hace que la segunda forma no sea azúcar sino una capacidad distinta.

🎯 Al terminar esta lección sabrás
  • Escribir keyframes en las dos sintaxis y traducir entre ellas.
  • Explicar cómo se reparten los offsets cuando no los declaras.
  • Aprovechar que cada propiedad tenga un número distinto de paradas.
  • Conocer los nombres especiales cssOffset y cssFloat y por qué existen.

Array de objetos: keyframes compartidos

La forma de array reproduce literalmente la estructura de una regla @keyframes: una secuencia de bloques, cada uno con las propiedades que declara en ese punto.

el.animate([
  { opacity: 0, transform: 'translateY(20px)' },
  { opacity: 1, transform: 'translateY(-6px)', offset: 0.7 },
  { opacity: 1, transform: 'translateY(0)' },
], 600);

Los nombres de propiedad van en camelCase (backgroundColor, borderTopLeftRadius) o con su nombre CSS entre comillas ('background-color'). Ambas formas funcionan y se pueden mezclar; la de camelCase es la habitual.

Cada objeto acepta además tres claves reservadas que no son propiedades CSS: offset, easing y composite. offset es un número entre 0 y 1 —no un porcentaje— que fija la posición del keyframe. easing es la curva del tramo que empieza en ese keyframe, igual que dentro de un bloque de CSS. composite decide cómo se combina ese keyframe con el valor subyacente.

Los offsets que no declares se reparten de forma uniforme entre los que sí. Con cuatro keyframes y ninguno con offset, quedan en 0, 1/3, 2/3 y 1. Con offset: 0.7 en el segundo de tres, quedan en 0, 0.7 y 1. La regla es que el primero va a 0, el último a 1, y los huecos se reparten equidistantes entre los offsets fijos que los rodean.

Dos restricciones que lanzan TypeError si las incumples: los offsets deben estar en el rango de 0 a 1, y deben ser no decrecientes. Un offset: 1.2 o una secuencia 0.6 seguida de 0.4 aborta la llamada con una excepción, no con un fallo silencioso. Es una diferencia con CSS, donde un selector de keyframe inválido se descarta sin ruido.

Objeto de arrays: una lista por propiedad

La segunda forma invierte la estructura. En vez de una lista de instantes con propiedades dentro, tienes un objeto de propiedades con una lista de valores cada una.

el.animate({
  opacity:   [0, 1],
  transform: ['translateY(20px)', 'translateY(-6px)', 'translateY(0)'],
}, 600);

Aquí está la diferencia que importa. opacity tiene dos valores, así que sus paradas caen en 0 y 1: sube de forma continua durante toda la animación. transform tiene tres, así que las suyas caen en 0, 0.5 y 1. Cada propiedad se reparte sobre su propio número de valores. No hay ningún keyframe intermedio de opacidad en el 0.5, y no hace falta inventárselo.

Escribir eso mismo con la forma de array obliga a repetir la opacidad en el punto medio, y a decidir qué valor ponerle:

// Equivalente en forma de array: hay que rellenar el hueco a mano
el.animate([
  { opacity: 0,   transform: 'translateY(20px)' },
  { opacity: 0.5, transform: 'translateY(-6px)' },   // valor inventado
  { opacity: 1,   transform: 'translateY(0)' },
], 600);

En este caso el 0.5 coincide con lo que la interpolación habría producido de todos modos, así que da igual. Pero en cuanto una de las dos propiedades lleva una curva propia, o en cuanto los offsets dejan de ser uniformes, el valor a rellenar deja de ser trivial y la forma de objeto es la única que expresa la intención sin mentir.

Esta forma admite también las tres claves reservadas, pero como arrays:

el.animate({
  transform: ['scale(0.9)', 'scale(1.08)', 'scale(1)'],
  offset:    [0, 0.6],          // el ultimo se completa a 1
  easing:    ['ease-out', 'ease-in-out'],
}, 500);

Una advertencia sobre offset en esta forma: los offsets se aplican a la lista de keyframes resultante por índice, y esa lista es la unión de las paradas de todas las propiedades. Si dos propiedades tienen distinto número de valores, la correspondencia entre tu array de offsets y los keyframes reales deja de ser evidente. La regla práctica es usar offset explícito solo cuando todas las propiedades tengan la misma longitud, y en cuanto necesites offsets distintos por propiedad, partir en dos animaciones.

Un solo keyframe y el resto implícito

Igual que en CSS, WAAPI sintetiza los extremos que faltan a partir del valor computado del elemento. Con un único keyframe, ese keyframe es el destino y el origen se toma del estado actual:

// Se desvanece desde su opacidad actual, sea cual sea
el.animate({ opacity: 0 }, { duration: 300, fill: 'forwards' });

// Equivalente explicito, si supieras la opacidad de partida
el.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 300, fill: 'forwards' });

La primera versión es mejor cuando el estado de partida no lo controlas: un elemento que quizá ya estaba a medio desvanecer, o cuya opacidad depende de una clase. Es la forma de escribir una animación de salida reutilizable que no da un salto al empezar.

El mismo mecanismo funciona con la forma de array declarando solo el último keyframe, y con la forma de objeto usando null como primer valor: { opacity: [null, 0] } toma el origen del estilo computado de forma explícita.

Nivel dios

Dos propiedades de CSS no se pueden escribir con su nombre camelCase en un keyframe porque colisionan con las claves reservadas del propio objeto. La propiedad CSS offset —la del posicionamiento por ruta— se escribe cssOffset, porque offset ya significa la posición del keyframe. Y float se escribe cssFloat, por la misma colisión histórica que ya existía en element.style. Están así en la especificación, con nombre propio, y no hay alternativa: { offset: 'path("M 0 0 L 100 0") 50%' } no anima nada, se interpreta como una posición de keyframe inválida y lanza TypeError. Es el tipo de detalle que cuesta media hora de depuración cuando alguien intenta animar un recorrido desde JavaScript. La forma con guiones tampoco salva: 'offset' entre comillas es la misma clave. cssOffset es la única puerta.

Cuál usar

La forma de objeto gana cuando las propiedades tienen distinto número de paradas, cuando estás generando los valores desde un array de datos, o cuando quieres leer la animación de un vistazo como “esta propiedad va de aquí a aquí”.

// Generado: una parada por elemento de datos
const alturas = datos.map((d) => `scaleY(${d / maximo})`);
barra.animate({ transform: alturas }, { duration: 1200, easing: 'linear' });

La forma de array gana cuando los instantes tienen significado propio —hay un momento en el 30% donde varias cosas pasan a la vez— y cuando necesitas easing o composite distintos por keyframe con una correspondencia clara. También es la única que produce una lectura inequívoca cuando hay offsets no uniformes.

En una base de código conviene elegir una por defecto y desviarse a propósito. La de array es la que más se parece a @keyframes, así que es la que menos fricción produce en un equipo que viene del CSS.

⚔️ Reto práctico

Escribe la misma animación en las dos formas: opacity de 0 a 1 y transform con cuatro paradas de un rebote. Después añade easing: 'steps(4)' al primer keyframe en la forma de array y comprueba que la opacidad también avanza a saltos. Luego hazlo en la forma de objeto y comprueba lo mismo. Deduce de ahí por qué easing por keyframe no es realmente por propiedad en WAAPI, al contrario que en CSS.