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

El targeting: qué puede ser un objetivo

Las formas de decirle a un tween sobre qué actuar, por qué GSAP puede animar objetos que no son elementos, y cómo acotar los selectores para que un componente no anime los de otro.

⏱ 17 min

El primer argumento de un tween se llama targets en plural, y esa s no es casual: cualquier tween puede actuar sobre uno o sobre mil objetivos con exactamente el mismo código. Lo que sí cambia según lo que pases es cuándo se resuelven los objetivos, qué ocurre si el selector no encuentra nada, y si el tween se puede reutilizar en otra instancia del mismo componente. En una aplicación con componentes que se montan varias veces, el targeting mal hecho es la causa número uno de que una animación afecte a la tarjeta equivocada.

🎯 Al terminar esta lección sabrás
  • Enumerar las formas válidas de objetivo y saber cuál usar en cada caso.
  • Acotar un selector al subárbol de un componente con gsap.utils.selector.
  • Convertir cualquier cosa parecida a una lista en un array real con gsap.utils.toArray.
  • Animar objetos que no son elementos del DOM y saber qué se pierde al hacerlo.

Las cinco formas de objetivo

Texto de selector. GSAP hace un document.querySelectorAll internamente y anima todo lo que encuentre.

gsap.to('.tarjeta', { y: -10, duration: 0.3 });

Es la forma más cómoda y la más peligrosa en aplicaciones con componentes: .tarjeta significa todas las tarjetas del documento, no las de este componente.

Referencia directa a un elemento. Sin ambigüedad y sin coste de búsqueda.

const el = document.querySelector('#panel');
gsap.to(el, { autoAlpha: 1, duration: 0.4 });

Array de elementos. Cualquier array de objetivos, mezclados si hace falta.

gsap.to([cabecera, cuerpo, pie], { y: 0, duration: 0.5, stagger: 0.08 });

Objetos parecidos a arrays. Una NodeList, una HTMLCollection, el resultado de querySelectorAll. GSAP los acepta directamente.

Objetos de JavaScript cualesquiera. Aquí está la capacidad diferencial: el objetivo no tiene que estar en el DOM ni parecerse a un elemento.

const camara = { x: 0, y: 3, zoom: 1 };

gsap.to(camara, {
  zoom: 2.4,
  y: 8,
  duration: 2,
  ease: 'power2.inOut',
  onUpdate: () => aplicarCamara(camara),
});

Un objeto plano, tres números, y un tween con easing, control, repetición y todo lo demás. Es el mismo motor: para GSAP, un elemento del DOM es solo un objeto con un plugin que sabe escribir en él.

ℹ️
Un array vacío no es un error

Un tween sobre cero objetivos se crea correctamente, corre su duración y dispara sus callbacks. No falla. Eso es deliberado: permite escribir código que anima lo que haya sin comprobar antes. Lo que sí produce un aviso en consola es pasar null o un selector que no encuentra nada, y ese aviso se puede silenciar con gsap.config({ nullTargetWarn: false }) a costa de perder el detector de erratas en los selectores.

El problema del componente montado dos veces

Este es el problema real, y merece la lección entera.

// En un componente Tarjeta que puede aparecer veinte veces en la pagina.
function animarEntrada() {
  gsap.from('.tarjeta__titulo', { y: 20, autoAlpha: 0, duration: 0.5 });
}

Ese código anima los veinte títulos cada vez que se llama, desde cualquiera de las veinte instancias. El resultado es que veinte tarjetas se animan veinte veces, con veinte tweens en conflicto sobre cada elemento.

La solución nativa es hacer la búsqueda dentro del elemento raíz del componente. La solución de GSAP es gsap.utils.selector, que devuelve una función de búsqueda acotada:

function montar(raiz) {
  const q = gsap.utils.selector(raiz);

  gsap.from(q('.tarjeta__titulo'), { y: 20, autoAlpha: 0, duration: 0.5 });
  gsap.from(q('.tarjeta__texto'), { y: 12, autoAlpha: 0, duration: 0.5, delay: 0.1 });
}

q busca solo dentro de raiz. Es equivalente a raiz.querySelectorAll(...) con dos ventajas: se escribe una vez y se usa muchas, y acepta también una referencia de un framework en vez de un elemento, lo cual la hace directamente utilizable desde React, Angular o cualquier cosa que envuelva elementos.

El patrón completo en una aplicación con componentes es siempre el mismo: cada instancia conoce su raíz, crea su función de búsqueda acotada, y nunca usa selectores globales. Un componente que anima con selectores globales es un componente que no se puede montar dos veces.

toArray y por qué querrías un array de verdad

gsap.utils.toArray convierte casi cualquier cosa en un array real: texto de selector, NodeList, HTMLCollection, un solo elemento, o un array que ya lo era.

const items = gsap.utils.toArray('.item');

Como los tweens ya aceptan todas esas formas, la pregunta legítima es para qué sirve. La respuesta es que un array real tiene map, filter, slice, forEach e índices, y eso permite hacer cosas que un tween con selector no puede:

const items = gsap.utils.toArray('.item');

// Filtrar antes de animar.
const visibles = items.filter((el) => el.offsetParent !== null);
gsap.to(visibles, { autoAlpha: 1, stagger: 0.05 });

// Un tween distinto por elemento, con datos leidos del propio elemento.
items.forEach((el) => {
  const factor = Number(el.dataset.paralaje ?? 1);
  gsap.to(el, { y: -80 * factor, duration: 1.2, ease: 'none' });
});

// Saber cuantos hay para calcular tiempos.
const paso = 1.2 / items.length;

toArray acepta además un segundo argumento de ámbito, igual que selector, así que gsap.utils.toArray('.item', raiz) busca solo dentro de esa raíz.

Hay una diferencia de comportamiento que importa: el selector de un tween se resuelve una sola vez, al crearse. Si añades elementos al DOM después, el tween no los conoce. Un array capturado con toArray tiene exactamente la misma limitación, así que en listas dinámicas el patrón correcto es crear la animación después de insertar, no antes.

Animar lo que no es DOM

Volviendo al caso de los objetos arbitrarios, hay tres patrones que aparecen constantemente.

Un valor que se dibuja. El objeto es un estado intermedio y el onUpdate lo pinta.

const estado = { valor: 0 };

gsap.to(estado, {
  valor: 87,
  duration: 1.6,
  ease: 'power2.out',
  onUpdate() {
    contador.textContent = Math.round(estado.valor) + '%';
  },
});

Una propiedad de un objeto de otra librería. Funciona directamente si es un número accesible por punto.

// La camara de una escena 3D. GSAP no sabe ni le importa lo que es.
gsap.to(camara.position, { z: 12, duration: 2, ease: 'power1.inOut' });
gsap.to(camara.rotation, { y: Math.PI / 4, duration: 2 });

Un objeto que sirve de proxy para algo que no tiene propiedades numéricas. El caso típico es una propiedad con setter, o un valor que hay que aplicar mediante una llamada.

const proxy = { volumen: 1 };

gsap.to(proxy, {
  volumen: 0,
  duration: 0.8,
  onUpdate: () => nodoDeGanancia.gain.setValueAtTime(proxy.volumen, ctx.currentTime),
});

Lo que se pierde al animar objetos que no son elementos es únicamente lo que aporta el plugin de CSS: los atajos de transformación, la interpolación de colores y unidades, autoAlpha. Todo lo demás —eases, timelines, stagger, callbacks, control— funciona idénticamente. Es la misma máquina.

Los objetivos se resuelven al crear, no al reproducir

Este es el detalle que produce el bug más difícil de ver de todo el targeting. Un tween creado con paused: true ya ha resuelto sus objetivos, aunque no se haya reproducido nunca. Si construyes tus animaciones al arrancar y las guardas pausadas para dispararlas después, y en ese intervalo el marco de trabajo vuelve a renderizar y sustituye los nodos del DOM por otros equivalentes, tu tween sigue apuntando a los elementos viejos, que ya no están en el documento. La animación corre perfectamente, los callbacks se disparan, y no se ve absolutamente nada.

El síntoma es inconfundible una vez que lo has visto: la animación “funciona” según los logs y no pasa nada en pantalla. Y aparece siempre en el mismo sitio, en aplicaciones con componentes que preconstruyen timelines. La regla que lo evita: construye las animaciones lo más tarde posible y sobre la raíz que tienes en la mano, no al arrancar y contra selectores globales. Si de verdad necesitas preconstruir, guarda una función que cree la animación en vez de la animación creada.

⚔️ Reto práctico

Escribe un componente que se pueda montar varias veces y anime solo lo suyo, sin usar identificadores únicos ni clases generadas. Después rómpelo a propósito cambiando q('.titulo') por '.titulo' y observa cómo cada instancia anima las de las demás. Es el ejercicio de cinco minutos que evita una tarde de depuración en cada proyecto futuro.