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

Construir una fuente de progreso a mano

Con lo que ya sabes de WAAPI puedes montar el modelo completo: una animación pausada cuyo tiempo lo escribe otra cosa. Sirve como puente conceptual y enseña exactamente qué queda por delegar.

⏱ 18 min

La forma más rápida de entender el modelo de líneas de tiempo de progreso es construir una con las piezas que ya tienes. Una animación pausada es exactamente una función de progreso a valores esperando a que alguien le diga en qué punto está; escribirle el currentTime desde una fuente externa completa el modelo. El resultado funciona hoy en todos los navegadores, enseña sin ambigüedad qué mitad del problema es tuya y cuál es del motor, y deja claro qué es lo único que la plataforma tiene que aportar para que el patrón deje de necesitar código.

🎯 Al terminar esta lección sabrás
  • Convertir una animación en una función pura de progreso con pause().
  • Alimentar su currentTime desde una fuente externa acotada.
  • Comprobar que el easing, el relleno y la composición siguen funcionando igual.
  • Enumerar exactamente qué partes de esta construcción sobran con soporte nativo.

Una animación pausada es una función

const barra = document.querySelector('.progreso');

const efecto = barra.animate(
  { transform: ['scaleX(0)', 'scaleX(1)'] },
  { duration: 1000, easing: 'linear', fill: 'both' }
);
efecto.pause();
efecto.currentTime = 0;

En ese estado, la animación no avanza sola y su valor depende únicamente de currentTime. La duración de mil milisegundos ha dejado de medir tiempo: es una escala arbitraria que hemos elegido para que el rango de 0 a 1000 represente cómodamente el recorrido completo. Podría ser 1 o 100000; lo único que importa es que sea el mismo número que usemos al escribir.

Con duration: 1000, escribir currentTime = 420 sitúa el efecto en el 42%. Eso es todo lo que hay: una función del progreso, con la interpolación, la curva y el relleno que ya sabes usar.

Conectarle una fuente

Ahora falta lo otro: algo acotado que produzca una fracción. El recorrido de desplazamiento del documento sirve:

const ESCALA = 1000;
let recorrido = 0;

function medir() {
  recorrido = document.documentElement.scrollHeight - innerHeight;
}

function actualizar() {
  if (recorrido <= 0) { efecto.currentTime = 0; return; }
  const p = Math.min(1, Math.max(0, scrollY / recorrido));
  efecto.currentTime = p * ESCALA;
}

medir();
actualizar();
addEventListener('resize', () => { medir(); actualizar(); }, { passive: true });
addEventListener('scroll', actualizar, { passive: true });

Funciona, y funciona bien. La barra sigue al desplazamiento, respeta la curva que hayas puesto, y si en vez de una escala lineal declaras keyframes con offsets y easing por tramo, el resultado los aplica todos correctamente. Todo lo que aprendiste sobre efectos vale aquí sin ninguna traducción.

Fíjate en lo que no ha hecho falta cambiar: ni un solo keyframe, ni una opción del efecto, ni la forma de expresar el movimiento. Lo único que se ha sustituido es de dónde sale el tiempo.

Todo lo demás sigue igual

Merece la pena comprobar explícitamente que las piezas de los niveles anteriores no se rompen, porque es la demostración de que la abstracción es sólida.

El easing por tramo funciona: escribe una curva distinta en cada keyframe y verás que la barra acelera y frena en los puntos que hayas marcado, según lo desplazado y no según el tiempo.

El relleno es más importante que nunca. Con fill: 'both', la barra muestra su valor inicial arriba del todo y el final abajo del todo. Sin relleno, en los extremos el elemento vuelve a su estilo de cascada y verás un salto en cuanto el progreso se salga de la ventana activa.

La composición funciona: una segunda animación con composite: 'add' sobre la misma propiedad se suma a esta, y puedes tener un efecto dirigido por el desplazamiento y otro dirigido por el tiempo componiéndose sobre el mismo transform.

El inventario funciona: la animación aparece en document.getAnimations() con playState a 'paused', se puede inspeccionar en el panel de animaciones y se puede cancelar.

El estado inactivo hay que gestionarlo tú, y es lo que hace la guarda de recorrido <= 0. Si el documento no tiene recorrido, no hay fracción que calcular, y sin esa guarda tendrías una división por cero que produce Infinity y un currentTime inválido.

Lo que sobra con soporte nativo

Puesto el patrón encima de la mesa, se ve con precisión qué parte es esencial y qué parte es andamiaje. Lo esencial son cuatro líneas: el efecto, su duración como escala, y la conexión con una fuente. El andamiaje es todo lo demás:

  • El escuchador de desplazamiento.
  • El escuchador de redimensionado y la medida cacheada.
  • El cálculo de la fracción y su acotación entre 0 y 1.
  • La guarda del recorrido nulo.
  • La conversión de fracción a la escala arbitraria.
  • El pause() y el currentTime inicial.

Seis piezas, ninguna relacionada con el efecto. Y tres defectos que no se pueden corregir desde aquí: el retraso de un frame por el viaje entre hilos, el trabajo por frame en el hilo principal, y que la escala de mil milisegundos es un número inventado que no significa nada y que hay que mantener sincronizado entre dos sitios del código.

Lo que aporta el soporte nativo es exactamente eso y nada más: una fuente de progreso de primera clase que se asigna a anim.timeline, con lo que el andamiaje entero desaparece, la escala deja de ser inventada porque el rango es el propio recorrido, y el cálculo ocurre donde el desplazamiento ya se conoce. El efecto no cambia ni una letra.

Nivel dios

Esta construcción manual no es solo un ejercicio didáctico: es la estrategia de reserva correcta para navegadores sin soporte nativo, y merece escribirse con esa intención. Las animaciones dirigidas por el desplazamiento están implementadas en Chromium y en Safari, y Firefox no las ha implementado a fecha de hoy, así que un efecto que dependa de ellas necesita una alternativa si el diseño lo requiere de verdad. El patrón que conviene adoptar es escribir el efecto una sola vez como una animación pausada, y decidir solo la fuente: si la plataforma ofrece una fuente de progreso nativa, se la asignas y no escribes ni un escuchador; si no, montas el andamiaje de esta lección. La parte cara del código —los keyframes, las curvas, la coreografía, los ajustes finos— se escribe una vez y sirve para ambos caminos, porque el efecto es independiente de la fuente. Quien no ve esta separación acaba con dos implementaciones del mismo efecto, una declarativa y otra imperativa, que divergen a la tercera modificación. Es el mismo error que escribir dos veces una consulta de medios. La abstracción que este nivel te ha enseñado no sirve solo para usar la API nueva: sirve para estructurar el código de forma que la API nueva y la vieja compartan todo menos la línea que las distingue.

El puente

Con esto tienes el modelo completo y comprobado con tus manos. Una animación es un efecto conectado a una fuente. La fuente que has usado toda la vida es el reloj del documento, y produce un tiempo que crece sin parar. Existe otra clase de fuente que produce una posición dentro de un recorrido acotado, y el efecto no nota la diferencia.

Lo que queda por aprender es cómo se declara esa fuente sin escribir el andamiaje: qué recorrido se elige, sobre qué eje, y cómo se ajusta qué porción del recorrido ocupa cada animación. Es sintaxis sobre un modelo que ya entiendes, que es el orden correcto de aprender las cosas.

⚔️ Reto práctico

Monta la barra de progreso manual de esta lección y después sustituye los keyframes por tres paradas con easing por tramo y offsets no uniformes. Comprueba que la barra acelera y frena exactamente en los puntos del recorrido que marcan los offsets. Después añade una segunda animación con composite: 'add' dirigida por el tiempo —una oscilación pequeña e infinita— y verifica que las dos se componen sobre el mismo transform sin pisarse. Ahí tienes las dos clases de fuente conviviendo sobre una propiedad.