wandres.dev
GSAP II · Los tweens: to, from, fromTo, set

to, from, fromTo y set: los cuatro constructores

Qué hace cada uno de los cuatro métodos que crean tweens, cuándo se leen los valores de partida, y por qué from es la fuente del parpadeo inicial más famoso de GSAP.

⏱ 18 min

Un tween es un asignador de propiedades de alto rendimiento: le das unos objetivos, una duración y unas propiedades, y cuando su cabezal de lectura se mueve a una posición nueva calcula qué valores corresponden y los escribe. Los cuatro métodos que lo crean se diferencian únicamente en de dónde salen el valor inicial y el final, y esa diferencia aparentemente trivial determina cuándo se leen los valores del DOM, qué pasa si el tween está pausado y por qué el elemento a veces aparece un frame donde no debe.

🎯 Al terminar esta lección sabrás
  • Elegir entre to, from, fromTo y set según de dónde vengan los valores.
  • Explicar cuándo lee cada uno los valores iniciales y qué implica el renderizado inmediato.
  • Diagnosticar y arreglar el parpadeo inicial de un from.
  • Encadenar tweens y saber qué devuelve cada método.

Los cuatro, y en qué se diferencian

gsap.to(objetivos, vars) anima desde el estado actual hasta los valores que declares. Es el que se usa el 80% de las veces porque es el que se corresponde con la intención habitual: “llévalo hasta aquí”.

gsap.from(objetivos, vars) anima desde los valores que declares hasta el estado actual. Es el constructor de entradas: el elemento ya está donde tiene que acabar —lo puso el CSS— y tú describes de dónde viene.

gsap.fromTo(objetivos, varsDesde, varsHasta) declara los dos extremos explícitamente y no lee nada del DOM. Es el único que da control total, y el único que se comporta igual si se ejecuta dos veces.

gsap.set(objetivos, vars) aplica valores inmediatamente. No es un método aparte: es azúcar para un tween de duración cero, con todo lo que eso implica —vive en la timeline, se puede meter en una secuencia, y participa del sistema de sobrescritura—.

// Llevalo a 300 px a la derecha.
gsap.to('.caja', { x: 300, duration: 1 });

// Que venga desde 300 px a la derecha hasta donde ya esta.
gsap.from('.caja', { x: 300, duration: 1 });

// De aqui hasta alli, sin leer nada del DOM.
gsap.fromTo('.caja', { x: -100, opacity: 0 }, { x: 0, opacity: 1, duration: 1 });

// Ponlo ahi ahora mismo.
gsap.set('.caja', { x: 0, opacity: 1 });

Los cuatro devuelven una instancia de Tween, así que se pueden guardar y controlar después. Y como los tweens se reproducen solos al crearse y se descartan al terminar, no hace falta guardarlos si solo quieres dispararlos.

Cuándo se leen los valores de partida

Aquí está la sustancia. Un tween no renderiza al crearse: espera al siguiente tick del motor. Eso significa que los valores iniciales de un to se leen del DOM en ese primer tick, no en el momento de la llamada, lo cual es correcto y deseable: si entre la creación y el primer frame algo movió el elemento, el tween parte de donde el elemento está de verdad.

Los tweens from y fromTo son la excepción: renderizan inmediatamente al crearse. La propiedad que controla esto es immediateRender, y sus valores por defecto son false para to y true para from y fromTo.

Tiene sentido cuando lo piensas. Un from describe un estado de partida distinto del actual; si esperase al siguiente frame, el elemento se vería un frame entero en su posición final antes de saltar a la inicial. Renderizando de inmediato, el salto ocurre en la misma tarea de JavaScript y nunca llega a pintarse.

// El elemento salta a opacity 0 en esta misma linea, no en el proximo frame.
gsap.from('.tarjeta', { autoAlpha: 0, y: 40, duration: 0.7 });
⚠️
El parpadeo del from y su arreglo real

Aun con renderizado inmediato, hay un caso en el que el elemento se ve un instante: cuando el from se crea después de que el navegador ya haya pintado. Si el script está al final del cuerpo, o es un módulo diferido, o el elemento se revela con un componente que se hidrata, hay un pintado con el estado final antes de que GSAP intervenga. El síntoma es un destello de contenido a plena opacidad justo al cargar.

El arreglo no es de GSAP: es declarar el estado inicial en el CSS y no depender de que la librería llegue a tiempo. Poner visibility: hidden en el elemento y animar con autoAlpha es el patrón estándar, porque autoAlpha entiende visibility y la restaura al subir de cero. Con eso, el peor caso es que el elemento no aparezca nunca si el script falla, que es un fallo mucho más fácil de detectar que un destello.

El renderizado inmediato tiene un segundo efecto que sorprende: un from pausado también aplica sus valores iniciales. Crear un tween con paused: true no lo deja inerte; el estado inicial ya está escrito en el elemento. Es lo correcto —quieres que el elemento esté preparado para cuando lo reproduzcas— pero significa que crear tweens pausados durante el arranque modifica la página aunque no reproduzcas ninguno.

// Este tween no se reproduce, pero .panel ya esta en opacity 0.
const t = gsap.from('.panel', { autoAlpha: 0, duration: 0.5, paused: true });

Si eso no es lo que quieres, immediateRender: false lo desactiva explícitamente.

from y la repetición: el detalle que muerde

Un to es idempotente en el sentido útil: ejecutarlo dos veces lleva al mismo sitio. Un from no lo es, y produce el bug más común del método.

La primera vez, el from lee el estado actual del DOM como destino y anima hacia él. La segunda vez, el estado actual es el destino de la primera, así que el destino sigue siendo el mismo. Hasta ahí bien. El problema aparece cuando el from se recrea después de que algo haya movido el elemento, o cuando dos from sobre el mismo elemento se interfieren: el segundo lee como destino el estado que el primero acaba de establecer.

En una secuencia de entrada que se dispara una vez, nunca lo notarás. En un componente que se monta y desmonta, o en una animación que se vuelve a crear al cambiar de ruta, lo vas a notar como elementos que se quedan a medio camino y no vuelven.

La regla práctica: usa from para lo que ocurre una sola vez y fromTo para lo que se puede repetir. fromTo no lee nada del DOM, así que se comporta idénticamente la primera vez y la centésima.

// Se puede volver a crear las veces que haga falta.
gsap.fromTo('.item',
  { autoAlpha: 0, y: 24 },
  { autoAlpha: 1, y: 0, duration: 0.5 }
);

set no es un atajo trivial

gsap.set parece equivalente a escribir estilos a mano, y no lo es en tres aspectos que importan.

Primero, escribe a través del sistema de propiedades de GSAP, lo cual significa que un gsap.set(el, { x: 100 }) actualiza la caché interna de transformaciones. Escribir el.style.transform = 'translateX(100px)' a mano no la actualiza, y el siguiente tween partirá de un valor equivocado.

Segundo, acepta exactamente la misma sintaxis que un tween, incluidos los valores relativos, los aleatorios, las funciones por objetivo y los staggers. Colocar cien elementos en posiciones distintas es una línea.

// Reparte los elementos con una posicion inicial dependiente del indice.
gsap.set('.punto', {
  x: (i) => i * 24,
  opacity: (i) => 1 - i * 0.05,
});

Tercero, como es un tween de duración cero, se puede insertar en una timeline y ocurrirá cuando el cabezal llegue a esa posición, ni antes ni después. Es la forma correcta de decir “en este punto de la secuencia, pon esto así”: un element.style.x = ... en un callback ocurriría al ejecutarse la construcción, no al llegar el cabezal.

Encadenar y limpiar

Los cuatro métodos devuelven el tween, y el tween tiene métodos que devuelven el propio tween, así que el encadenamiento funciona:

const t = gsap.to('.caja', { x: 300, duration: 1 })
  .pause()
  .progress(0.5);

Y hay un método que conviene conocer desde el principio: revert(). Mata el tween y devuelve los objetivos a su estado previo a la animación, incluida la eliminación de los estilos en línea que GSAP había escrito. Es distinto de kill(), que solo detiene la animación y deja los valores donde estén.

const t = gsap.from('.panel', { autoAlpha: 0, y: 30, duration: 0.6 });

// Detener y dejarlo como estaba antes, sin estilos en linea residuales.
t.revert();

Los estilos en línea residuales son la fuente de una categoría entera de bugs: un elemento animado por GSAP queda con transform y opacity escritos directamente en style, y esos ganan a cualquier regla de la hoja de estilos. Si después alguien intenta cambiar la posición con una clase, no pasará nada y nadie entenderá por qué. revert() limpia, y clearProps dentro del vars hace lo mismo al terminar el tween.

// Al acabar, borrar las transformaciones que GSAP escribio.
gsap.to('.caja', { x: 300, duration: 1, clearProps: 'transform' });
El orden de creación decide quién gana, y no es el que crees

Dos tweens sobre la misma propiedad del mismo objetivo entran en conflicto, y GSAP no los mata automáticamente: la propiedad overwrite vale false por defecto. Ambos siguen vivos y escriben en cada frame, así que el que gana es el que se actualice el último en la cola, que es el creado más tarde. Eso funciona por accidente en muchos casos y produce un temblor visible en otros, porque los dos están escribiendo valores distintos sesenta veces por segundo.

La solución no es matar todo con overwrite: true, que es demasiado agresivo —mata todos los tweens del objetivo aunque animen propiedades distintas—. Es overwrite: 'auto', que en el primer renderizado busca conflictos reales y mata solo las partes de los otros tweens que animan las mismas propiedades, dejando intacto lo demás. Es lo que quieres en cualquier animación disparada por interacción del usuario, donde un mouseenter y un mouseleave rápidos crean exactamente ese conflicto. Ponlo en los tweens de interacción y déjalo fuera de las coreografías, donde por construcción no hay solapes que no sean deliberados.

⚔️ Reto práctico

Monta el conflicto para verlo: dos gsap.to sobre el mismo elemento y la misma propiedad, con duraciones y destinos distintos, creados con 200 ms de diferencia. Observa el temblor. Después añade overwrite: 'auto' al segundo y comprueba que desaparece. Finalmente añade un tercer tween que anime una propiedad distinta y verifica que 'auto' no lo mata mientras que true sí.