wandres.dev
SCROLL-DRIVEN I · El modelo de timelines

Timeline monotónica frente a timeline de progreso

Las dos clases de fuente de tiempo, en qué se diferencian formalmente, y qué partes del modelo de animación dejan de tener sentido cuando el tiempo puede retroceder.

⏱ 18 min

Las líneas de tiempo se dividen en dos clases, y la diferencia no es de unidad sino de propiedades matemáticas. Una monotónica avanza siempre, no retrocede nunca y no tiene final. Una de progreso está acotada entre un principio y un final, y su valor puede subir, bajar o quedarse quieto según lo que la gobierne. Cambiar de una a otra invalida tres supuestos que estaban repartidos por todo el modelo de animación, y saber cuáles son evita la mitad de las sorpresas del bloque que empieza aquí.

🎯 Al terminar esta lección sabrás
  • Definir monotonía y acotación aplicadas a una línea de tiempo.
  • Explicar por qué una duración en milisegundos deja de tener sentido en una de progreso.
  • Predecir qué le ocurre a iterations: Infinity y a la promesa finished.
  • Reconocer el estado inactivo como situación normal y no como error.

Monotónica: el reloj

Una línea de tiempo monotónica cumple tres propiedades:

Avanza siempre. Entre dos lecturas consecutivas, la segunda es mayor o igual que la primera. Nunca disminuye por sí sola.

No está acotada. No hay un valor máximo ni un concepto de “el final”. Puede seguir creciendo mientras el documento exista.

Su unidad es absoluta. Un milisegundo mide lo mismo al principio que al final, y mide lo mismo que un milisegundo de cualquier otro sitio.

document.timeline es la única monotónica que expone la plataforma, y esas tres propiedades son las que hacen que todo el vocabulario habitual de la animación funcione. “Duración de 400 milisegundos” es una frase con sentido porque el milisegundo es absoluto. “Repetir infinitamente” tiene sentido porque no hay final. “Terminar” tiene sentido porque una vez pasado un instante no se vuelve a él.

De progreso: una posición dentro de un recorrido

Una línea de tiempo de progreso invierte las tres:

Puede retroceder. Su valor sube o baja según lo que la gobierne, y puede quedarse parada indefinidamente.

Está acotada. Tiene un principio y un final definidos, y el valor actual siempre está entre ambos. Por eso se expresa como un porcentaje de 0% a 100% y no en milisegundos.

Su unidad es relativa. El 1% de un recorrido de mil píxeles y el 1% de uno de diez mil no miden lo mismo en ninguna magnitud absoluta. Solo tienen en común representar la misma fracción.

Cualquier magnitud con principio, final y posición actual puede ser una fuente de progreso. La que la plataforma ha adoptado es el desplazamiento de un contenedor, y esa es la materia de los dos niveles siguientes. Aquí interesa lo que se deduce de las propiedades, no de la fuente concreta.

Qué cambia al sustituir la fuente

Lo que deja de tener sentido

La duración en milisegundos. Si el tiempo de la línea es un porcentaje, una duración de 400 milisegundos no puede mapearse a nada. Lo que hace el modelo es interpretar la duración como una fracción del rango: una animación ocupa una porción del recorrido y su duración deja de medir tiempo para medir espacio. En el caso normal, una animación cubre el recorrido entero y su duración es todo el rango.

La repetición infinita. iterations: Infinity sobre un recorrido acotado significaría infinitas repeticiones dentro de un espacio finito, es decir, una frecuencia infinita. La especificación resuelve el sinsentido tratando esa configuración como que la animación no progresa. Si escribes un bucle infinito esperando que se repita mientras te desplazas, no se repite: se queda quieto.

La velocidad. playbackRate multiplica el tiempo, y sobre una fuente de progreso no hay tiempo que multiplicar: el avance lo decide íntegramente la fuente. La propiedad sigue existiendo y sigue afectando a la dirección, pero “el doble de rápido” deja de ser una frase con contenido.

La irreversibilidad del final. Con una fuente que puede retroceder, una animación entra en su fase posterior, vuelve a la activa, vuelve a entrar en la posterior, y así tantas veces como el usuario quiera. La promesa finished se resuelve y se sustituye por otra pendiente cada vez que se sale del estado, y el evento finish se dispara una vez por cada entrada. Cualquier código que asuma “esto ocurre una vez” tiene que reescribirse.

Lo que sigue teniendo sentido, y más que antes

Las fases siguen siendo las tres de siempre: previa, activa y posterior. Y animation-fill-mode pasa de ser una propiedad que se copia por inercia a ser esencial, porque con una fuente de progreso es normalísimo que el usuario deje el recorrido antes del principio o después del final de la ventana activa de una animación. Sin relleno, el elemento vuelve a su estilo base en esas zonas, y eso casi nunca es lo que se quiere.

Los keyframes, el easing por tramo y la composición funcionan idénticamente. El efecto no se entera de nada porque recibe un progreso, y un progreso es un progreso.

El estado inactivo deja de ser un caso raro. Una línea de tiempo de progreso cuya fuente no existe o no tiene recorrido —un contenedor que no se puede desplazar porque su contenido cabe entero— está inactiva, y su currentTime vale null. La animación conectada a ella no aplica nada. Esto ocurre continuamente en un diseño adaptable: la misma página tiene recorrido en un móvil y no lo tiene en un monitor grande. Hay que diseñar para que el estado sin recorrido sea visualmente correcto, no un accidente.

Nivel dios

La distinción monotónica frente a progreso tiene una consecuencia en el tipado de la API que ya asomó al hablar de currentTime: el valor está declarado como CSSNumberish, que es la unión de un número y un CSSNumericValue. Sobre document.timeline recibes un número en milisegundos y la aritmética directa funciona. Sobre una línea de progreso recibes un objeto con value y unit, donde la unidad es el porcentaje, y anim.currentTime - 100 da NaN sin lanzar ninguna excepción. Es el único punto del modelo donde la sustitución de la fuente no es transparente para el código que ya tenías, y es exactamente el sitio donde más código hay escrito, porque leer y escribir currentTime es lo que hace cualquier utilidad de control de reproducción. La regla defensiva para cualquier función genérica que reciba una Animation de fuera es normalizar en la entrada: const ms = typeof t === 'number' ? t : t.value;. Y la observación más útil: si tu código necesita hacer aritmética con el tiempo de una animación cuya fuente no controlas, probablemente lo que quieres no es el tiempo sino el progreso, que effect.getComputedTiming().progress te da siempre como número entre 0 y 1 sea cual sea la fuente. Es una de esas veces en que la API que parece menos directa es la que no se rompe.

El resumen operativo

propiedad monotónica de progreso
dirección solo hacia adelante ambas
acotación sin final de 0% a 100%
unidad milisegundos absolutos porcentaje del rango
duración de una animación tiempo real fracción del rango
iterations: Infinity válido sin efecto
playbackRate velocidad solo sentido
estado inactivo excepcional habitual
entrar en finished una vez tantas como el usuario quiera

Con esta tabla delante, la mayor parte de lo que parece comportamiento arbitrario en una animación dirigida por progreso se vuelve deducible. No es una API distinta: es la misma con una fuente que ha perdido dos propiedades que dábamos por hechas.

⚔️ Reto práctico

Coge una animación cualquiera y responde por escrito, antes de escribir código, qué le pasaría a cada una de sus opciones si su fuente de tiempo pasara a ser de progreso: al delay, al endDelay, al iterations, al fill, al direction y al easing. Comprueba después tus respuestas contra la tabla. Las que falles son exactamente los supuestos implícitos que llevabas dentro sin saberlo.