currentTime y startTime: los dos relojes
La ecuación que relaciona el tiempo de la animación con el de su línea de tiempo, cómo usarla para buscar un instante, y por qué startTime es la forma correcta de sincronizar varias animaciones.
Una animación tiene dos tiempos y no son intercambiables. currentTime es el tiempo de la animación: cuánto lleva reproducido, medido en su propia escala. startTime es el instante de la línea de tiempo en el que la reproducción quedó anclada. Uno vive dentro de la animación, el otro fuera, y una ecuación de una línea los relaciona. Con esa ecuación en la cabeza se resuelven la búsqueda de instantes, la sincronización de grupos y el arrastre manual sin recurrir a ningún truco.
- Escribir la ecuación que relaciona
currentTime,startTimey el tiempo de la línea. - Buscar un instante concreto de la animación asignando
currentTime. - Sincronizar varias animaciones fijando un
startTimecomún. - Programar una animación para el pasado o para el futuro sin usar
delay.
La ecuación
currentTime = (timeline.currentTime - startTime) * playbackRate
De aquí sale todo. timeline.currentTime es el reloj del documento, que avanza solo. startTime es el ancla: el valor que tenía ese reloj cuando la animación se consideró iniciada. La diferencia entre ambos es el tiempo transcurrido, y playbackRate lo escala.
Los dos extremos de la ecuación se pueden escribir, y escribir uno recalcula el otro para que la igualdad se mantenga. Asignar currentTime mueve el ancla; asignar startTime mueve el tiempo de la animación. Son dos formas de decir lo mismo con distinto punto de vista.
const anim = el.animate(kf, 1000);
await anim.ready;
anim.currentTime; // ~16, avanzando
document.timeline.currentTime; // el reloj del documento, en ms
anim.startTime; // el ancla
Cuando la animación está pausada, startTime vale null y la ecuación se suspende: el tiempo actual queda retenido en un valor fijo que no depende ya del reloj. Al reanudar, el motor calcula un startTime nuevo para que la ecuación vuelva a producir el mismo currentTime. Por eso pausar y reanudar no salta.
En 'idle', ambos valen null: no hay reproducción y no hay ancla.
Buscar un instante
Asignar currentTime es la operación de búsqueda. Funciona en cualquier estado y no cambia el estado: si la animación estaba pausada sigue pausada en el punto nuevo, y si estaba corriendo sigue corriendo desde ahí.
anim.currentTime = 0; // al principio
anim.currentTime = anim.effect.getComputedTiming().activeDuration / 2; // a la mitad
El valor está en milisegundos e incluye el retardo. Con delay: 200, poner currentTime = 100 deja la animación dentro de su fase previa, y lo que se vea dependerá del relleno. Es un detalle que se olvida al construir un control deslizante: la barra debe recorrer de cero a endTime, no de cero a duration.
El control deslizante completo, con las tres piezas necesarias:
const anim = el.animate(kf, { duration: 3000, easing: 'linear', fill: 'both' });
anim.pause();
const slider = document.querySelector('input[type=range]');
const total = anim.effect.getComputedTiming().endTime;
slider.min = 0;
slider.max = total;
slider.addEventListener('input', () => { anim.currentTime = Number(slider.value); });
pause() primero, porque si no la animación seguiría avanzando entre eventos y el control pelearía contra ella. fill: 'both' para que los extremos sigan aplicándose. Y easing: 'linear' porque en un control manual la curva de temporización distorsionaría la correspondencia entre la posición del pulgar y el progreso visual.
Asignar currentTime por encima de endTime no es un error: la animación pasa a su fase posterior y playState cambia a 'finished', con lo que se dispara el evento correspondiente. Asignar un valor negativo la lleva a la fase previa.
startTime sincroniza
Aquí está el uso que justifica que startTime sea escribible. Cuando quieres que varias animaciones vayan exactamente al mismo compás, crearlas en la misma vuelta del bucle de eventos no es suficiente: cada una resuelve su instante de inicio por su cuenta, y si entre medias ocurre un cálculo de estilo o una de ellas se acelera en el compositor y otra no, pueden quedar desfasadas por un frame.
La forma determinista es anclarlas todas al mismo instante:
const animaciones = [...document.querySelectorAll('.barra')].map((el) =>
el.animate(
{ transform: ['scaleY(0.3)', 'scaleY(1)', 'scaleY(0.3)'] },
{ duration: 1200, iterations: Infinity, easing: 'ease-in-out' }
)
);
await Promise.all(animaciones.map((a) => a.ready));
const ancla = animaciones[0].startTime;
animaciones.forEach((a, i) => { a.startTime = ancla - i * 150; });
Las dos últimas líneas hacen dos cosas a la vez. Igualan el ancla de todas —quedan sincronizadas al milisegundo— y aplican un desfase controlado restando a cada una un múltiplo del paso. Restar del startTime equivale exactamente a un retardo negativo: la animación queda anclada más atrás, así que su tiempo actual es mayor y va más avanzada.
Es la traducción exacta de la técnica del retardo negativo de CSS, con la ventaja de que aquí el desfase se puede cambiar después sin recrear nada.
Programar para el futuro
Sumar al startTime en vez de restar coloca el ancla en el futuro, y la animación queda esperando:
const anim = el.animate(kf, 600);
await anim.ready;
anim.startTime = document.timeline.currentTime + 1000; // empieza dentro de 1s
Esto es equivalente a delay: 1000, con una diferencia importante: el retardo forma parte de la definición del efecto y no se puede cambiar sin tocarlo, mientras que startTime se puede reasignar en cualquier momento. Una animación programada para dentro de un segundo se puede adelantar, retrasar o disparar de inmediato con una asignación.
// El usuario ha interactuado: adelanta lo que estaba programado
anim.startTime = document.timeline.currentTime;
Es la base de cualquier sistema de coreografía en el que el orden de los eventos no se conoce de antemano: en vez de encadenar temporizadores que hay que cancelar cuando algo cambia, programas anclas y las mueves.
currentTime y startTime están declarados como CSSNumberish en la especificación, no como double. Sobre la línea de tiempo del documento devuelven un número en milisegundos y no notas la diferencia, pero el tipo está así porque sobre otras líneas de tiempo el valor no es un tiempo: es un porcentaje, y llega como un objeto CSSUnitValue con value y unit. La consecuencia es que cualquier código que haga aritmética directa con anim.currentTime es correcto hoy sobre la línea del documento y se rompe sin aviso el día que esa misma animación se conecte a otra fuente de progreso, porque objeto - 100 da NaN. El código defensivo es feo pero de una línea: const t = anim.currentTime; const ms = typeof t === 'number' ? t : t.value;. No hace falta escribirlo en una animación que sabes que vive en el documento, pero sí en cualquier utilidad genérica que reciba una Animation de fuera, porque ahí no controlas de dónde saca el tiempo. Es la primera grieta visible de un modelo que ya no asume que el tiempo sea tiempo.
Leer el progreso normalizado
Para dibujar una barra de progreso o alimentar otro cálculo, lo que suele hacer falta no es el tiempo sino la fracción. La forma correcta la da el propio efecto:
const t = anim.effect.getComputedTiming();
t.progress; // fraccion dentro de la iteracion actual, 0 a 1, o null
t.currentIteration; // indice de iteracion, o null
progress ya tiene aplicada la función de temporización y la dirección, así que refleja el progreso visual, no el temporal. Con easing: 'ease-in' a mitad de la duración, progress no vale 0.5 sino bastante menos. Para una barra que acompañe al tiempo y no al movimiento, calcula la fracción a mano dividiendo currentTime entre endTime; para sincronizar otro efecto con el que se está viendo, usa progress.
Los dos valen null cuando la animación está fuera de su fase activa y no hay relleno, que es la forma que tiene la API de decir “este efecto no está aportando nada ahora mismo”.
Crea tres animaciones idénticas sobre tres elementos y no toques sus tiempos. Registra en consola a.startTime de cada una tras await a.ready y comprueba si coinciden. Después provoca trabajo en el hilo principal entre las tres creaciones con un bucle de cincuenta milisegundos y vuelve a comprobarlo. Por último iguala los tres startTime y verifica que ahora sí van al compás.