Etiquetas con nombre
Marcar puntos de una timeline con nombres estables, navegar entre ellos, insertar pausas en puntos concretos, y usarlos como interfaz pública de una coreografía.
Una etiqueta es un nombre asociado a un instante de la timeline. Dicho así parece azúcar sintáctico sobre un número, y en cierto sentido lo es; lo que la convierte en otra cosa es que el número al que apunta se recalcula solo cuando la timeline cambia, y que a partir de ahí puedes escribir código que se refiere a “el momento en que empieza la segunda escena” sin saber ni querer saber en qué segundo cae eso. Es lo que convierte una animación en algo que otra parte de la aplicación puede controlar sin conocer sus tripas.
- Añadir etiquetas en posiciones absolutas, relativas y por acumulación.
- Navegar la timeline saltando y reproduciendo desde una etiqueta.
- Insertar pausas en puntos concretos y reanudar desde ellos.
- Exponer las etiquetas como interfaz pública de una coreografía.
Poner etiquetas
addLabel(nombre, posicion) inserta una etiqueta. La posición acepta todas las formas del parámetro de posición, así que las etiquetas se colocan con la misma gramática que todo lo demás.
const tl = gsap.timeline();
tl.addLabel('intro') // al final de lo que haya, que es 0
.from('.logo', { autoAlpha: 0, duration: 0.8 })
.addLabel('contenido') // al final: en 0.8
.from('.texto', { y: 30, autoAlpha: 0, duration: 0.6 })
.addLabel('cierre', '+=0.4') // 0.4 s despues del final actual
.to('.todo', { autoAlpha: 0, duration: 0.5 });
Sin segundo argumento, la etiqueta va al final de la timeline en ese momento, que es lo que quieres al construir en orden. Con posición explícita se puede colocar donde sea, incluidos puntos ya pasados.
Las etiquetas viven en tl.labels, un objeto plano de nombre a segundo:
console.log(tl.labels);
// { intro: 0, contenido: 0.8, cierre: 1.8 }
Ese objeto es la razón de que las etiquetas se recalculen: no son referencias vivas a un hijo, son entradas en un mapa que GSAP mantiene. Si desplazas los hijos con shiftChildren(cantidad, true), el segundo argumento indica que las etiquetas se desplacen también. Si insertas un hijo en medio de la timeline, las etiquetas posteriores no se mueven, porque son posiciones, no marcadores de contenido. Es la limitación que hay que conocer: una etiqueta marca un instante, no un hito de la secuencia.
Se quitan con removeLabel(nombre), que además devuelve el instante que ocupaba. Y clear(true) vacía la timeline incluyendo las etiquetas; clear(false) conserva las etiquetas y borra los hijos.
Navegar por etiquetas
Las etiquetas son valores válidos en todos los métodos de navegación:
tl.seek('contenido'); // salta ahí sin cambiar el estado de reproduccion
tl.play('cierre'); // salta ahi y reproduce hacia delante
tl.reverse('intro'); // salta ahi y reproduce hacia atras
tl.pause('contenido'); // salta ahi y se queda parado
tl.restart(); // vuelve al principio del todo
play(etiqueta) es la operación que hace útiles a las etiquetas de verdad: permite tener una sola timeline con varias escenas y saltar entre ellas desde la interfaz.
Además hay tres métodos de consulta:
tl.currentLabel(); // la etiqueta mas cercana en o antes del instante actual
tl.nextLabel(); // la siguiente etiqueta desde el instante actual
tl.previousLabel(); // la anterior
currentLabel acepta también un argumento y entonces actúa como setter, saltando a esa etiqueta. nextLabel y previousLabel aceptan un instante concreto en vez de usar el actual, lo cual permite calcular sin mover el cabezal.
Con esos tres se monta una navegación por escenas en muy poco código:
botonSiguiente.addEventListener('click', () => {
const destino = tl.nextLabel();
if (destino) tl.tweenTo(destino);
});
botonAnterior.addEventListener('click', () => {
const destino = tl.previousLabel();
if (destino) tl.tweenTo(destino);
});
tweenTo(posicion) merece una mención propia: en vez de saltar, anima el cabezal hasta ese punto y para. Es decir, crea un tween lineal sobre el tiempo de la timeline. La diferencia con seek es enorme en experiencia de usuario: seek teletransporta y tweenTo recorre. Y como es un tween normal, acepta un objeto vars:
tl.tweenTo('cierre', { duration: 1.2, ease: 'power2.inOut' });
Su hermano tweenFromTo(desde, hasta, vars) recorre entre dos puntos, saltando primero al de partida.
No hay que hacer nada especial para recorrer hacia atrás: si el destino está antes del instante actual, tweenTo mueve el cabezal en sentido inverso. Ojo con una consecuencia: al retroceder, los eases de los hijos se recorren del revés, con lo que un power2.out se siente como un power2.in. Si eso te molesta, easeReverse en los hijos lo corrige.
Pausas en puntos concretos
addPause(posicion, callback, params) inserta un punto en el que la timeline se detiene sola al llegar reproduciendo hacia delante. Es el mecanismo para coreografías por capítulos que esperan una acción del usuario.
const tl = gsap.timeline();
tl.from('.paso1', { autoAlpha: 0, duration: 0.6 })
.addPause() // espera aqui
.from('.paso2', { autoAlpha: 0, duration: 0.6 })
.addPause('+=0', () => mostrarBoton('continuar')) // y avisa al parar
.from('.paso3', { autoAlpha: 0, duration: 0.6 });
// Y para continuar, simplemente:
boton.addEventListener('click', () => tl.play());
La pausa solo actúa hacia delante: reproduciendo en reversa el cabezal la atraviesa sin detenerse, lo cual es casi siempre lo que quieres.
Un detalle importante: addPause inserta un elemento en la timeline, así que participa del cálculo de duración y de posición como cualquier otro hijo. Y se quita con removePause(posicion).
La combinación de etiquetas y pausas da un modelo de presentación completo:
function construirPresentacion() {
const tl = gsap.timeline({ paused: true });
['uno', 'dos', 'tres'].forEach((nombre, i) => {
tl.addLabel(nombre)
.from(`.diapositiva-${i + 1}`, { autoAlpha: 0, y: 40, duration: 0.6 })
.addPause();
});
return tl;
}
const presentacion = construirPresentacion();
document.addEventListener('click', () => presentacion.play());
Las etiquetas como interfaz pública
El uso que menos gente hace y más rendimiento da: tratar las etiquetas como la API de la coreografía. Una función que construye una timeline devuelve algo cuyos puntos de interés están nombrados, y el resto de la aplicación se refiere a esos nombres sin saber nada del interior.
export function construirIntro(raiz) {
const q = gsap.utils.selector(raiz);
const tl = gsap.timeline({ paused: true, defaults: { duration: 0.7, ease: 'power2.out' } });
tl.addLabel('inicio')
.from(q('.velo'), { autoAlpha: 1, duration: 1 })
.addLabel('contenidoVisible')
.from(q('.titular'), { y: 40, autoAlpha: 0 })
.from(q('.texto'), { y: 24, autoAlpha: 0 }, '<0.15')
.addLabel('listo');
return tl;
}
Quien la consume no necesita saber cuántos pasos hay ni cuánto duran:
const intro = construirIntro(seccion);
intro.play();
// Saltar al final si el usuario ya la ha visto en esta sesion.
if (yaVista) intro.seek('listo').pause();
// Reaccionar a un punto concreto sin acoplarse a los tiempos.
intro.eventCallback('onUpdate', () => {
if (intro.currentLabel() === 'contenidoVisible') activarScroll();
});
Ese acoplamiento por nombre en vez de por instante es lo que permite reajustar la coreografía sin romper el código que la usa. Es exactamente el mismo argumento que el de cualquier interfaz: lo que se expone son nombres estables, no números internos.
GSAP crea la etiqueta si no existe. Es deliberado y práctico al construir, y letal al consumir: tl.play('conteniodVisible') con una errata no lanza ninguna excepción. Crea una etiqueta nueva al final de la timeline y reproduce desde ahí, con lo que la animación aparece terminada al instante y no hay ninguna pista.
Como los nombres son cadenas, el compilador tampoco te ayuda. La disciplina que funciona es no escribir nunca la cadena dos veces: define las etiquetas como constantes exportadas junto a la función que construye la timeline y usa esas constantes en ambos lados.
export const HITOS = { inicio: 'inicio', visible: 'contenidoVisible', listo: 'listo' };Con eso, una errata es un error de referencia en tiempo de ejecución o, con tipos, en tiempo de compilación. Y si prefieres una comprobación defensiva, nombre in tl.labels te dice si existe antes de saltar.
Convierte una coreografía tuya en una presentación por capítulos: etiqueta cada escena, mete un addPause al final de cada una, y añade dos botones que naveguen con tweenTo sobre nextLabel y previousLabel. Comprueba que retroceder atraviesa las pausas sin detenerse y decide si eso es lo que quieres o si necesitas gestionarlo tú.