wandres.dev
GSAP III · Timelines

El parámetro de posición, todas sus formas

El tercer argumento que hace insustituible a GSAP: absoluto, relativo al final, relativo a la animación anterior con los punteros de inicio y fin, y basado en porcentaje. Con el modelo mental completo.

⏱ 24 min

Si hay una sola característica que justifica GSAP frente a cualquier alternativa, es el tercer argumento de los métodos de una timeline. Un único parámetro que acepta números, cadenas relativas, etiquetas, punteros a la animación anterior y porcentajes, y que permite expresar cualquier relación temporal entre dos piezas como intención en vez de como aritmética. Es la diferencia entre escribir “empieza 150 milisegundos antes de que acabe la anterior” y escribir el número que resulta de esa frase, con la certeza de que ese número dejará de ser correcto en cuanto alguien toque una duración.

🎯 Al terminar esta lección sabrás
  • Enumerar las cinco familias de valor del parámetro de posición y su semántica exacta.
  • Distinguir el puntero al inicio del puntero al final de la animación anterior.
  • Expresar solapes en porcentaje y saber respecto a qué duración se calculan.
  • Predecir la posición resultante en casos donde varias formas parecen equivalentes.

Dónde va y qué reemplaza

El parámetro va después del objeto vars, y existe en to, from, set, add, call, addLabel y addPause. En fromTo va después de los dos objetos.

tl.to(objetivo, vars, posicion);
tl.fromTo(objetivo, varsDesde, varsHasta, posicion);

Su valor por defecto es "+=0", que significa “al final de la timeline”. Por eso una cadena de .to() sin parámetro produce una secuencia estricta sin huecos.

flowchart LR
subgraph T[Timeline con cabezal propio]
  direction LR
  a[Paso A de 0 a 1 segundo]
  b[Paso B empieza en 0.8 con solape]
  c[Paso C empieza al iniciar B]
  d[Paso D empieza en el segundo 3 absoluto]
end
p[Parametro de posicion] --> q{Que forma tiene}
q -->|numero| d
q -->|mas igual o menos igual| b
q -->|menor que o mayor que| c
q -->|etiqueta| a
style b fill:#f9e2af,color:#11111b
style c fill:#cba6f7,color:#11111b
style d fill:#89b4fa,color:#11111b
style a fill:#a6e3a1,color:#11111b

Las cinco familias de valor

Familia uno: absoluto

Un número es el instante exacto, en segundos, medido desde el principio de la timeline.

// Exactamente en el segundo 3, pase lo que pase antes.
tl.to('.caja', { x: 100 }, 3);

Es la forma más rígida y la que menos se usa, precisamente porque anula la ventaja principal de las timelines: si algo anterior cambia de duración, esta pieza no se mueve. Tiene dos usos legítimos. El primero es colocar algo al principio: 0 significa “a la vez que el inicio de la timeline”, que es la manera canónica de arrancar varias cosas juntas. El segundo es sincronizar con una referencia externa que sí es absoluta, como un audio.

// Estos tres arrancan simultaneamente en el instante cero.
tl.to('.a', { x: 100 }, 0)
  .to('.b', { y: 100 }, 0)
  .to('.c', { rotation: 90 }, 0);

Familia dos: relativo al final de la timeline

Los prefijos "+=" y "-=" se miden desde el final actual de la timeline, no desde la animación anterior. La distinción importa cuando el final de la timeline y el final de la última animación añadida no coinciden, cosa que ocurre en cuanto has insertado algo en una posición anterior.

// Un segundo de hueco tras el final de la timeline.
tl.to('.caja', { x: 100 }, '+=1');

// Solape de medio segundo con el final de la timeline.
tl.to('.caja', { y: 100 }, '-=0.5');

Es la forma más usada y la que expresa el patrón habitual: “que se solape un poco con lo anterior”. El número sigue siendo un número, pero es un número local —el tamaño del solape— y no una suma acumulada, así que sobrevive a los cambios de duración de todo lo demás.

Familia tres: los punteros a la animación anterior

Aquí está la parte que la mayoría de la gente no aprende y que resuelve la mitad de los casos difíciles.

"<" apunta al inicio de la animación añadida más recientemente. ">" apunta a su final.

// Empieza a la vez que la anterior.
tl.to('.a', { x: 100, duration: 2 })
  .to('.b', { y: 100, duration: 1 }, '<');

// Empieza justo cuando acaba la anterior. Equivale al comportamiento
// por defecto solo si la anterior es tambien la ultima en el tiempo.
tl.to('.c', { rotation: 90 }, '>');

Un detalle que merece leerse dos veces: “la animación añadida más recientemente” no es lo mismo que “la que acaba más tarde”. Si insertas algo en el segundo 0 de una timeline que ya llegaba al segundo 5, esa inserción es la más reciente, y un "<" posterior apuntará a su inicio, es decir, al segundo 0. Es coherente y es la fuente de la confusión más común con estos punteros.

Los punteros aceptan un desplazamiento numérico, y cuando el número sigue directamente al puntero, el "+=" está implícito:

tl.to('.b', { y: 100 }, '<1');      // un segundo despues del INICIO de la anterior
tl.to('.b', { y: 100 }, '<+=1');    // exactamente lo mismo
tl.to('.b', { y: 100 }, '<-2');     // dos segundos ANTES del inicio de la anterior
tl.to('.b', { y: 100 }, '>-0.5');   // medio segundo antes del FINAL de la anterior

Ese último, ">-0.5", es el que expresa correctamente “que empiece medio segundo antes de que acabe la anterior”. Compáralo con "-=0.5", que dice “medio segundo antes del final de la timeline”. Cuando la anterior es también la última, coinciden; en cuanto no lo es, no.

⚠️
El error de solape más común

Escribir "-=0.5" pensando en la animación anterior. Es correcto mientras la timeline crezca de forma estrictamente secuencial, y se rompe silenciosamente el día que alguien inserta algo con "<" unas líneas más arriba, porque a partir de ahí el final de la timeline ya no es el final de la última añadida. El síntoma es un solape que se convierte en un hueco, o al revés, sin que nadie haya tocado esa línea. Si de verdad quieres decir “respecto a la anterior”, usa ">" y "<".

Familia cuatro: etiquetas

Una cadena que no empieza por "+=", "-=", "<" ni ">" se interpreta como el nombre de una etiqueta. Si la etiqueta existe, la posición es la suya; si no existe, se crea al final de la timeline.

tl.addLabel('escena2', 2)
  .to('.a', { x: 100 }, 'escena2')
  .to('.b', { y: 100 }, 'escena2+=0.3');

Las etiquetas admiten desplazamientos relativos con "+=" y "-=", medidos desde la etiqueta. Tienen su propia lección en este nivel, porque son bastante más que un valor del parámetro de posición.

El hecho de que una etiqueta inexistente se cree en lugar de dar error es deliberado y muy práctico —permite escribir la timeline en orden y las etiquetas aparecen solas— pero significa que una errata en el nombre de una etiqueta no produce ningún error. Escribes 'escena_2' en vez de 'escena2' y GSAP crea una etiqueta nueva al final; tu animación aparece al final de todo y no hay ninguna pista de por qué.

Familia cinco: porcentajes

Un porcentaje inmediatamente después de "+=" o "-=" se calcula sobre la duración total de la animación que se está insertando. Un porcentaje inmediatamente después de "<" o ">" se calcula sobre la duración total de la animación anterior.

Esa asimetría es exactamente lo que hace falta, aunque cueste memorizarla:

// Solapa con el final de la timeline un 25% de la duracion de ESTE tween.
tl.to('.a', { x: 100, duration: 2 }, '-=25%');   // solapa 0.5 s

// Empieza cuando la ANTERIOR lleva el 25% recorrido.
tl.to('.b', { y: 100 }, '<25%');

// Mismo punto que el anterior, expresado desde el final de la anterior.
tl.to('.b', { y: 100 }, '>-75%');

// Un 25% de la duracion de ESTE tween despues del inicio de la anterior.
tl.to('.b', { y: 100, duration: 4 }, '<+=25%');   // un segundo despues

Los dos últimos ejemplos son el par que hay que tener claro: "<25%" y "<+=25%" son cosas distintas. Sin el "+=", el porcentaje es de la anterior; con él, del que se inserta.

La ventaja de los porcentajes es que sobreviven a los cambios de duración de una forma que los segundos no. “Que arranque cuando la anterior lleve un tercio” sigue siendo correcto si la anterior pasa de durar uno a durar tres segundos; “que arranque 0,33 segundos después del inicio de la anterior” deja de serlo.

Y hay un matiz de precisión: la duración de referencia es la total, que incluye repeticiones y sus retardos. Un tween de un segundo con repeat: 2 tiene una duración total de tres, así que un ">-50%" respecto a él son 1,5 segundos antes de su final absoluto, no medio.

Tabla de referencia

Valor Significado
3 Segundo 3 desde el inicio de la timeline
0 A la vez que el inicio de la timeline
"+=1" Un segundo después del final de la timeline
"-=1" Un segundo antes del final de la timeline
"+=50%" Media duración del tween insertado, después del final de la timeline
"-=25%" Un cuarto de la duración del tween insertado, solapando el final
"<" Inicio de la animación añadida más recientemente
">" Final de la animación añadida más recientemente
"<1" o "<+=1" Un segundo después de ese inicio
"<-1" Un segundo antes de ese inicio
">-0.5" Medio segundo antes de ese final
"<25%" Al 25% de la duración total de la animación anterior
"<+=25%" Un 25% de la duración del tween insertado, tras el inicio de la anterior
"etiqueta" En la etiqueta; se crea al final si no existe
"etiqueta+=2" Dos segundos después de la etiqueta
"etiqueta+=30%" Un 30% de la duración del tween insertado, tras la etiqueta

Un ejemplo que usa todo

const tl = gsap.timeline({ defaults: { duration: 0.8, ease: 'power2.out' } });

tl.from('.fondo',   { autoAlpha: 0, duration: 1.2 })
  .from('.logo',    { scale: 0.7, autoAlpha: 0 }, '<40%')   // cuando el fondo lleva el 40%
  .addLabel('texto')                                        // etiqueta en este punto
  .from('.titular', { y: 40, autoAlpha: 0 })
  .from('.linea',   { scaleX: 0, transformOrigin: 'left' }, '<')  // a la vez que el titular
  .from('.parrafo', { y: 24, autoAlpha: 0 }, '>-0.4')       // 0.4 s antes de que acabe la linea
  .from('.boton',   { scale: 0.8, autoAlpha: 0, ease: 'back.out(1.7)' }, '+=0.2')
  .to('.indicador', { autoAlpha: 1 }, 'texto+=0.5');        // relativo a la etiqueta

Cada posición dice qué relación existe entre dos piezas. Ninguna dice un instante calculado. Cambia la duración del fondo a dos segundos y todo se recoloca conservando las relaciones: el logo sigue entrando al 40%, el párrafo sigue solapando 0,4 segundos con la línea.

Los punteros apuntan a lo último añadido, no a lo último en el tiempo

Es la trampa que se lleva más horas de depuración de toda la API, y merece un ejemplo explícito.

const tl = gsap.timeline();
tl.to('.a', { x: 100, duration: 5 })      // ocupa de 0 a 5
  .to('.b', { y: 100, duration: 1 }, 0)   // insertado en 0, ocupa de 0 a 1
  .to('.c', { rotation: 90 }, '<');       // <-- apunta al inicio de B, o sea 0

.c no empieza en el segundo 5 ni al final de nada: empieza en el 0, porque el puntero mira a .b, que es lo último añadido, aunque .a sea lo último en terminar. La documentación lo dice en una nota al pie y prácticamente nadie la lee.

La regla de higiene que lo evita: construye las timelines en orden temporal. Si necesitas insertar algo en una posición anterior, hazlo con una etiqueta explícita en vez de con un número, y sigue usando "<" y ">" solo para relaciones entre piezas contiguas. Cuando una timeline empiece a tener inserciones fuera de orden, es que quiere ser dos timelines anidadas.

⚔️ Reto práctico

Escribe una timeline de seis pasos usando una forma distinta del parámetro en cada uno: absoluta, relativa al final, puntero al inicio, puntero al final con desplazamiento, porcentaje de la anterior y etiqueta. Después imprime tl.duration() y las posiciones de inicio de cada hijo con tl.getChildren().map((h) => h.startTime()) y comprueba que salen los números que habías predicho.