El objeto de opciones de temporización
Las once claves del segundo argumento, la fórmula de la duración activa y del tiempo final, y las dos opciones que no tienen ningún equivalente en CSS.
El segundo argumento de animate() es un superconjunto estricto de las ocho propiedades de animación de CSS. Ocho de sus claves son traducción directa; las otras tres no existen en la sintaxis declarativa y ninguna es decorativa. endDelay y iterationStart cambian la forma de la línea temporal de maneras que el CSS no puede describir, y id es lo que hace manejable un documento con decenas de animaciones vivas.
- Traducir cada opción de temporización a su propiedad CSS equivalente.
- Calcular la duración activa y el tiempo final de una animación.
- Distinguir
iterationStartde un retardo negativo. - Usar
endDelaypara secuenciar sin encadenar promesas.
Las ocho traducciones directas
el.animate(keyframes, {
duration: 600, // animation-duration, en milisegundos
delay: 120, // animation-delay
easing: 'ease-in-out', // animation-timing-function
iterations: 3, // animation-iteration-count
direction: 'alternate', // animation-direction
fill: 'backwards', // animation-fill-mode
composite: 'replace', // animation-composition
iterationComposite: 'replace',
});
duration se expresa en milisegundos como número, no como cadena con unidad: 600, no '600ms'. Acepta también la cadena 'auto', que sobre la línea de tiempo del documento se comporta como cero. Y hay un atajo que se usa constantemente: si en vez del objeto pasas un número, se interpreta como la duración.
el.animate(keyframes, 600); // equivale a { duration: 600 }
iterations acepta Infinity, el valor de JavaScript, no la cadena 'infinite'. Es un error frecuente al traducir desde CSS y no da excepción: 'infinite' no es un número, se convierte a NaN, y el resultado es una animación que no se reproduce.
easing en el objeto de opciones es la curva por defecto de todos los tramos, exactamente igual que animation-timing-function en la regla del elemento. La que se declara dentro de un keyframe la sobrescribe para su tramo. Acepta cualquier valor válido de CSS, incluidos steps(), cubic-bezier() y linear() con paradas.
fill admite un valor extra que CSS no tiene: 'auto', que es el valor inicial y se comporta como 'none' para un KeyframeEffect. Su existencia tiene que ver con otros tipos de efecto y en la práctica puedes ignorarlo.
La aritmética de la línea temporal
Cuatro magnitudes, y conviene tenerlas claras porque son las que devuelve getComputedTiming() y las que aparecen en el panel de animaciones:
duracion activa = duration * iterations
tiempo final = delay + duracion activa + endDelay
La duración activa es el tiempo durante el cual el efecto produce valores. El tiempo final es el instante en que la animación pasa a considerarse terminada y se resuelve su promesa finished. Entre el final de la fase activa y el tiempo final hay un hueco, y ese hueco es endDelay.
const anim = el.animate(kf, { duration: 400, iterations: 2, delay: 100, endDelay: 250 });
const t = anim.effect.getComputedTiming();
console.log(t.activeDuration); // 800
console.log(t.endTime); // 1150
console.log(t.progress); // progreso dentro de la iteracion actual, o null
console.log(t.currentIteration);
getComputedTiming() es la forma correcta de leer estos valores: resuelve 'auto', aplica los valores por defecto y devuelve números concretos. Leer anim.effect.getTiming() devuelve lo que tú pasaste, sin resolver, y no sirve para calcular nada.
Las tres opciones que no traducen ninguna propiedad CSS
Las tres que quedan no tienen contrapartida declarativa, y ninguna es decorativa: cada una expresa algo que en una hoja de estilo no se puede decir.
endDelay: el hueco después del final
endDelay es un retardo posterior a la fase activa. Durante ese intervalo la animación ya no produce valores nuevos —está en su fase posterior, y lo que se vea lo decide fill— pero todavía no está terminada: playState sigue siendo 'running' y finished no se ha resuelto.
Su utilidad es secuenciar. Cuando encadenas animaciones esperando la promesa de cada una, endDelay te permite meter la pausa dentro de la propia animación en vez de en un setTimeout intercalado:
async function secuencia(el) {
await el.animate({ transform: ['scale(1)', 'scale(1.2)'] },
{ duration: 200, endDelay: 400, fill: 'forwards' }).finished;
// aqui ya han pasado 600ms, y el elemento lleva 400 congelado en scale(1.2)
await el.animate({ transform: ['scale(1.2)', 'scale(1)'] },
{ duration: 200, fill: 'forwards' }).finished;
}
La ventaja sobre un setTimeout no es estética: la pausa forma parte de la animación, así que se pausa cuando pausas la animación, se acelera cuando cambias su playbackRate y se cancela cuando la cancelas. Un setTimeout intercalado no sabe nada de nada de eso, y una secuencia construida con temporizadores se desincroniza en cuanto el usuario interactúa.
endDelay también acepta valores negativos, que recortan la fase activa por el final: la animación se da por terminada antes de completar sus iteraciones. Es raro y casi siempre expresa peor lo mismo que un iterations fraccionario.
iterationStart no es un retardo negativo
iterationStart desplaza el punto de partida dentro de la iteración, expresado como fracción. Con iterationStart: 0.25, la animación empieza en el 25% de su recorrido de keyframes.
Suena a retardo negativo, pero hay una diferencia que lo cambia todo: la duración activa no se acorta. Un retardo negativo recorta el principio de la ventana activa, así que la animación termina antes. iterationStart deja la ventana intacta y lo que hace es rotar la fase:
// Retardo negativo: dura 750ms y termina en el 100%
el.animate(kf, { duration: 1000, delay: -250 });
// iterationStart: dura 1000ms, va del 25% al 100% y sigue del 0% al 25%
el.animate(kf, { duration: 1000, iterationStart: 0.25 });
Con iterations: 1 e iterationStart: 0.25, la animación recorre el progreso local de 0.25 a 1.25, es decir: del 25% al 100% y vuelta a empezar del 0% al 25%. Con una animación cíclica —una rotación, un degradado que fluye— eso es exactamente lo que quieres: una vuelta completa empezando por otro punto. Con un retardo negativo obtendrías tres cuartos de vuelta.
Es la opción que hace posible desfasar elementos en un bucle sin que ninguno termine antes que los demás, y es la razón por la que existe. En CSS no hay forma de expresarlo.
id parece la opción más trivial del objeto y es la que más problemas ahorra en una base de código real. Sirve para recuperar tus animaciones desde getAnimations() sin llevar tú un registro, y por tanto para todo el ciclo de vida: cancelar la anterior antes de lanzar la siguiente, comprobar si ya hay una en marcha, o depurar qué está pisando qué. El patrón que resuelve el noventa por ciento de los bugs de animaciones superpuestas es este, y cabe en cuatro líneas: for (const a of el.getAnimations()) if (a.id === 'entrada') a.cancel(); antes de crear la nueva con ese mismo id. Sin id, la alternativa es guardar referencias en un WeakMap indexado por elemento, que funciona igual de bien hasta que la animación la crea otro módulo. Y hay un detalle que casi nadie conoce: las animaciones CSS también tienen id, aunque siempre vale cadena vacía, así que filtrar por un id no vacío es una forma fiable de quedarte solo con las tuyas y no cancelar transiciones ajenas por accidente.
pseudoElement
La última clave, pseudoElement, dirige el efecto a un pseudo-elemento del objetivo:
boton.animate(
{ transform: ['scaleX(0)', 'scaleX(1)'] },
{ duration: 300, pseudoElement: '::before' }
);
Acepta '::before', '::after', '::marker' y otros pseudo-elementos animables, con la sintaxis de dos puntos dobles. Es la única forma de animar un pseudo-elemento desde JavaScript, porque los pseudo-elementos no existen en el DOM y no tienen un nodo sobre el que llamar a animate().
Las animaciones creadas así aparecen en elemento.getAnimations() del elemento anfitrión, y su effect.pseudoElement te dice cuál es. Si estás recorriendo animaciones para cancelarlas, ese campo es lo que distingue la del ::before de la del propio elemento.
Anima una rotación completa con duration: 4000, iterations: Infinity sobre cuatro elementos, dándole a cada uno un iterationStart de 0, 0.25, 0.5 y 0.75. Comprueba que quedan perfectamente repartidos y que los cuatro giran a la vez. Después intenta el mismo reparto con retardos negativos de 0, -1000, -2000 y -3000 y verifica que el resultado visual es idéntico solo porque las iteraciones son infinitas: pon iterations: 2 en ambas versiones y observa cómo la de retardos negativos termina escalonada y la de iterationStart termina a la vez.