Callbacks y sus parámetros
Los seis callbacks de un tween, cuándo se dispara cada uno de verdad, cómo pasarles argumentos, qué es this dentro de ellos, y por qué las promesas no sustituyen a onComplete.
Los callbacks parecen la parte aburrida de la API hasta que tienes que depurar por qué onComplete se ejecuta dos veces, o por qué onStart no se dispara nunca en un tween que claramente está corriendo, o por qué el this dentro del callback no es lo que esperabas. Cada uno de esos comportamientos tiene una explicación exacta en el modelo de tiempo del motor, y una vez que la conoces los callbacks pasan de ser una fuente de sorpresas a ser el mecanismo con el que se cose la animación al resto de la aplicación.
- Enumerar los seis callbacks y el momento exacto en que cada uno se dispara.
- Pasar argumentos a un callback y fijar el valor de
this. - Leer el progreso y la razón desde dentro de
onUpdatesin cerrar sobre variables externas. - Elegir entre callbacks y promesas según lo que la animación tenga que hacer.
Los seis, y cuándo se disparan
onStart se dispara cuando el tiempo del tween pasa de cero a otro valor. La frase importa: no es “cuando se crea” ni “cuando se llama a play”. Un tween con delay no dispara onStart hasta que el retardo pasa. Y como puede volver a pasar de cero a otro valor —si lo reinicias, o si el cabezal de la timeline padre lo recorre otra vez— se puede disparar más de una vez.
onUpdate se dispara en cada tick en el que el cabezal se mueve. Es el más caro por definición, sesenta o ciento veinte veces por segundo, así que lo que pongas dentro debe ser barato.
onComplete se dispara al llegar al final. Con repeat no se dispara al acabar cada iteración sino al terminar todas; el de cada iteración es onRepeat.
onRepeat se dispara cada vez que el tween entra en una iteración nueva. Solo existe si repeat es distinto de cero.
onReverseComplete se dispara al llegar al principio reproduciendo hacia atrás. Es el simétrico de onComplete y es lo que necesitas para limpiar cuando una animación de entrada se ha invertido para salir.
onInterrupt se dispara cuando la animación se interrumpe antes de terminar, típicamente porque otra la ha sobrescrito o porque la han matado. No se dispara si la animación termina con normalidad, así que sirve para distinguir “acabó” de “la cortaron”.
gsap.to('.caja', {
x: 400,
duration: 1,
repeat: 2,
onStart: () => console.log('empieza'),
onUpdate: () => {},
onRepeat: () => console.log('otra vuelta'),
onComplete: () => console.log('acabo del todo'),
onInterrupt:() => console.log('la cortaron'),
});
Con repeat: 2, la secuencia de mensajes es: empieza, otra vuelta, otra vuelta, acabó del todo. Tres iteraciones, dos repeticiones, un onComplete.
Una animación dentro de una timeline que se reproduce hacia atrás dispara onReverseComplete, no onComplete. Es correcto y es la fuente de un bug muy concreto: código de limpieza puesto en onComplete que nunca se ejecuta cuando el usuario cierra el panel con reverse() en vez de dejar que la animación termine hacia delante. Si la limpieza debe ocurrir en ambos casos, va en los dos callbacks o en un onInterrupt según lo que estés limpiando.
Parámetros y ámbito
Cada callback tiene su variante Params, que recibe un array de argumentos:
gsap.to('.item', {
autoAlpha: 0,
duration: 0.4,
onComplete: registrar,
onCompleteParams: ['salida', 42],
});
function registrar(fase, id) {
console.log(fase, id); // "salida" 42
}
Es la forma de reutilizar una misma función desde varios tweens sin crear un cierre distinto en cada uno, lo cual importa cuando hay cientos de tweens y cada cierre retiene referencias. Los argumentos se pasan tal cual, sin sustituciones ni interpretación: lo que pongas en el array es lo que recibe la función.
Para el this hay dos mecanismos. callbackScope fija el this de todos los callbacks del tween:
gsap.to('.item', {
x: 200,
duration: 1,
callbackScope: miComponente,
onComplete() { this.terminado = true; },
});
Y si no defines callbackScope, dentro de un callback declarado con la sintaxis de método —no con función flecha— el this es la propia animación:
gsap.to('.item', {
x: 200,
duration: 1,
onUpdate() {
// this es el tween.
console.log(this.progress().toFixed(2), this.ratio.toFixed(3));
},
});
Ese detalle es el que hace que la sintaxis de método sea preferible a la flecha en los callbacks de GSAP: con flecha pierdes el acceso al tween sin ganar nada.
Leer estado desde dentro
Dentro de un callback, además del progreso hay una propiedad que casi nadie usa y es muy útil: ratio. Es el progreso después de pasar por el ease, mientras que progress() es el progreso lineal antes del ease.
La diferencia importa cuando quieres interpolar algo por tu cuenta con la misma sensación que el tween. ratio puede salirse del rango cero-uno —con back o con elastic lo hace— y ese es justamente el valor que necesitas.
gsap.to({}, {
duration: 1,
ease: 'back.out(1.8)',
onUpdate() {
// progress va 0 -> 1 lineal; ratio sigue la curva y se pasa de 1.
dibujar(this.progress(), this.ratio);
},
});
Ese gsap.to({}, {...}) sobre un objeto vacío es un patrón reconocible: un tween que no anima nada y solo existe para producir una curva de progreso que tú consumes. Es la forma correcta de aprovechar el motor de eases y el control de timelines para algo que dibujas a mano.
También son accesibles desde this todos los métodos de control: this.pause(), this.timeScale(0.5), this.kill(). Matar el tween desde su propio onUpdate es legal y es la forma de implementar una condición de parada.
Callbacks contra promesas
Los tweens y las timelines implementan then, así que se pueden esperar:
await gsap.to('.panel', { autoAlpha: 1, duration: 0.4 });
await gsap.to('.contenido', { y: 0, duration: 0.6 });
hacerLoSiguiente();
Es limpio y tentador, y tiene tres limitaciones que hay que conocer antes de adoptarlo como patrón general.
La primera es que una promesa se resuelve una sola vez. Un tween con repeat: -1 nunca la resuelve, y un tween que se reinicia no vuelve a resolverla. onComplete sí se dispara cada vez.
La segunda es que una cadena de await es una secuencia estricta: cada paso empieza cuando el anterior acaba. No hay forma de solapar, y solapar es justo lo que hace que una coreografía se vea bien. Eso es trabajo de una timeline, no de promesas.
La tercera es que una cadena de await no se puede rebobinar, pausar, ni escalar en velocidad. Es código imperativo, no una estructura de datos.
La regla: usa promesas cuando la animación es un paso en un flujo asíncrono —espera a que termine la salida antes de navegar— y callbacks o timelines cuando la animación es una coreografía.
El callback más peligroso es onUpdate, y no por el coste del callback en sí, que es despreciable. Es por lo que la gente mete dentro: lecturas de layout. Un getBoundingClientRect(), un offsetWidth, un getComputedStyle dentro de onUpdate fuerza al navegador a recalcular el layout en mitad del frame, justo después de que GSAP haya escrito valores. Eso es el vaivén de lecturas y escrituras que el motor evita cuidadosamente con su renderizado perezoso, y tú lo reintroduces en una línea.
El coste no es lineal: con un solo elemento no se nota, con cien elementos animándose y cada uno leyendo su rectángulo, el frame pasa de dos milisegundos a cuarenta. Y el síntoma es de los que despistan, porque la animación va perfecta en desarrollo con tres elementos y se arrastra en producción con la lista completa.
La regla es absoluta: en onUpdate solo se escribe. Todo lo que haya que leer se lee una vez en onStart y se guarda. Si de verdad necesitas medir durante la animación, mide en un listener del ticker añadido con prioritize, que corre antes de que GSAP escriba nada y por tanto lee un layout ya estabilizado.
Instrumenta el coste: monta cien elementos animándose con un onUpdate que lea getBoundingClientRect() y mide la duración media del frame en el perfilador. Quita la lectura, guardando el rectángulo en onStart, y vuelve a medir. Anota la diferencia; es el argumento que vas a necesitar la próxima vez que alguien defienda leer el layout en cada frame.