playbackRate: velocidad, marcha atrás y el cero
El multiplicador que escala toda la línea temporal, qué significa exactamente un valor negativo o cero, y por qué existe updatePlaybackRate además del asignador.
playbackRate es un multiplicador sobre el tiempo, y por eso afecta a todo: al retardo, a la duración, al retardo final y a la dirección. Un valor de 2 no acelera “la animación”, acelera su reloj. De ahí salen los dos casos límite interesantes: con valor negativo el reloj corre hacia atrás y la animación termina cuando llega al principio; con valor cero el reloj se detiene sin que la animación esté pausada, que es un estado distinto y más útil de lo que parece.
- Explicar sobre qué magnitudes actúa exactamente
playbackRate. - Diferenciar
playbackRate = 0depause(). - Reproducir hacia atrás y saber cuándo termina la animación.
- Elegir entre asignar
playbackRatey llamar aupdatePlaybackRate().
Un multiplicador sobre el reloj
Vuelve a la ecuación de la lección anterior: currentTime = (timeline.currentTime - startTime) * playbackRate. El factor está fuera de todo, así que escala la línea temporal completa. Con playbackRate = 2, una animación con delay: 500 y duration: 1000 tarda 250 milisegundos en empezar y 500 en completarse.
const anim = el.animate(kf, { duration: 2000, delay: 400 });
anim.playbackRate = 4; // empieza a los 100ms y dura 500ms de reloj real
Esto lo distingue de reducir duration, que solo afecta a la fase activa. Si estás construyendo un modo de depuración a cámara lenta, playbackRate es lo correcto precisamente porque respeta las proporciones: los retardos se estiran igual que las duraciones y la coreografía se mantiene.
Asignar playbackRate conserva el tiempo actual. La animación no salta a otro punto: sigue donde estaba y a partir de ahí avanza a otra velocidad. El motor reajusta startTime para que la ecuación siga cuadrando.
Cero no es pausa
Con playbackRate = 0 el tiempo actual deja de avanzar, pero playState sigue valiendo 'running'. La animación está corriendo; lo que pasa es que su reloj no se mueve.
La diferencia con pause() es observable en tres sitios. Primero, en playState, que es lo que consulta cualquier código que decida si algo está en marcha. Segundo, en startTime, que con velocidad cero sigue teniendo un valor mientras que al pausar pasa a null. Y tercero, en finish(), que con velocidad cero lanza InvalidStateError porque no hay una dirección hacia la que terminar.
¿Para qué sirve entonces? Para el control manual. Cuando quieres que la animación sea función de otra cosa —la posición de un control deslizante, el desplazamiento de un contenedor, la posición del puntero— lo que necesitas es que no avance sola y que puedas escribir su tiempo:
const anim = el.animate(kf, { duration: 1000, easing: 'linear', fill: 'both' });
anim.playbackRate = 0;
contenedor.addEventListener('pointermove', (e) => {
const r = contenedor.getBoundingClientRect();
const p = Math.min(1, Math.max(0, (e.clientX - r.left) / r.width));
anim.currentTime = p * 1000;
});
Se puede hacer igual con pause(), y en la mayoría de los casos da lo mismo. La ventaja de la velocidad cero es que devolver el control automático es una sola asignación —anim.playbackRate = 1— sin llamar a play() y sin arriesgarse al rebobinado automático que play() aplica si la animación llegó al final.
Marcha atrás
Con velocidad negativa el tiempo actual decrece. La animación se considera terminada cuando llega a cero, no cuando llega al final:
anim.playbackRate = -1;
anim.play();
await anim.finished; // se cumple cuando currentTime llega a 0
anim.playState; // 'finished'
anim.currentTime; // 0
Ese “terminada en cero” es coherente con el modelo pero descoloca la primera vez: una animación finalizada puede estar mostrando su primer keyframe. Y con fill: 'backwards' el elemento se queda ahí, congelado en el estado inicial, con la animación en estado 'finished'.
reverse() es exactamente playbackRate *= -1 seguido de play(). Escribirlo a mano tiene sentido cuando quieres fijar la dirección de forma absoluta en vez de alternarla: playbackRate = -1 siempre cierra, mientras que reverse() cierra o abre según cómo estuviera.
Una animación con velocidad negativa y iterations: Infinity no termina nunca, igual que con velocidad positiva. Y finish() sobre una animación infinita con velocidad negativa sí funciona, porque el destino hacia atrás es el tiempo cero, que es finito; solo lanza excepción con velocidad positiva, donde el destino es infinito.
Por qué existe updatePlaybackRate
Animation.updatePlaybackRate(v) hace lo mismo que asignar la propiedad, con una diferencia: aplica el cambio de forma asíncrona y sincronizada.
Cuando una animación se está ejecutando en el compositor, su reloj vive en otro hilo. Asignar playbackRate directamente obliga al hilo principal a calcular en ese instante qué tiempo actual corresponde, y ese cálculo usa el estado que el hilo principal conoce, que puede llevar un frame de retraso respecto al que está pintando de verdad. El resultado es un salto visible de uno o dos frames, pequeño pero perceptible en una animación lenta.
// Cambio inmediato, puede saltar en animaciones aceleradas
anim.playbackRate = 0.25;
// Cambio sincronizado, sin salto
anim.updatePlaybackRate(0.25);
await anim.ready; // se cumple cuando el cambio ha entrado en vigor
anim.playbackRate; // 0.25
updatePlaybackRate() fija una velocidad pendiente que entra en vigor en el siguiente ciclo de actualización, momento en el que el motor ajusta el tiempo de inicio para que no haya discontinuidad. Mientras tanto, anim.pending vale true y anim.playbackRate sigue devolviendo el valor antiguo.
La regla: para un cambio que el usuario va a ver mientras algo se mueve, usa updatePlaybackRate(). Para preparar una animación antes de reproducirla, la asignación directa es más simple y no hay nada que sincronizar.
El uso de playbackRate que más partido saca a la propiedad no es acelerar ni invertir: es igualar duraciones sin tocar los efectos. Cuando compones una interfaz a partir de animaciones que vienen de sitios distintos —una del sistema de diseño, otra de un componente de terceros, otra tuya— cada una trae su propia duración, y hacer que un grupo termine a la vez exigiría reescribirlas todas. Con playbackRate no: calculas la duración objetivo y ajustas el multiplicador de cada una. a.playbackRate = a.effect.getComputedTiming().endTime / objetivo deja a todas terminando en el mismo instante, respetando sus curvas y sus retardos internos proporcionalmente. Es la misma idea que aplican los editores de vídeo al cambiar la velocidad de un clip para que encaje en un hueco, y no tiene ningún equivalente en CSS, donde igualar duraciones significa editar cada regla. El detalle que lo hace robusto es usar endTime y no duration: así los retardos y los retardos finales también se escalan y la proporción interna de cada animación se conserva intacta.
Velocidad global
Como todas las animaciones del documento son accesibles, el multiplicador se puede aplicar a todo a la vez. Es el modo de depuración que trae el panel de animaciones de las herramientas de desarrollo, y cabe en dos líneas:
function velocidadGlobal(v) {
for (const a of document.getAnimations()) a.updatePlaybackRate(v);
}
velocidadGlobal(0.1); // todo a camara lenta
velocidadGlobal(1); // normal
Funciona sobre animaciones CSS, transiciones CSS y animaciones de JavaScript por igual, porque las tres son objetos Animation. Es la forma más rápida de estudiar una animación ajena que pasa demasiado deprisa para verla, y merece la pena tenerla en un marcador o en un atajo del entorno de desarrollo.
La limitación es que solo afecta a lo que ya existe: las animaciones creadas después vuelven a velocidad 1. Para un modo persistente hay que reaplicarlo, y el sitio razonable para hacerlo es un requestAnimationFrame que compruebe si hay animaciones nuevas, o directamente reaplicarlo tras cada interacción.
Coge una animación de tres segundos con easing: 'ease-in-out' y ponla a playbackRate = 0.05. Observa la curva con detenimiento: verás la aceleración y el frenado con una claridad imposible a velocidad normal. Después prueba anim.playbackRate = 0.05 frente a anim.updatePlaybackRate(0.05) estando la animación en marcha y busca el salto de un frame en el primero.