Utilidades de colección: toArray, selector, distribute y shuffle
Las utilidades que trabajan con listas y con el DOM, el reparto genérico que está detrás del stagger, y por qué merece la pena tener este vocabulario incluso en código que no anima nada.
Las últimas utilidades del conjunto son las que trabajan con listas: convertir cualquier cosa parecida a una colección en un array de verdad, acotar una búsqueda a un subárbol, repartir un valor entre los elementos de una lista o de una rejilla, y barajar. La más interesante de las cuatro es distribute, porque es el motor que hay detrás del objeto de stagger expuesto como función pública, y eso significa que puedes escalonar cualquier cosa, no solo el tiempo.
- Convertir cualquier colección en un array y acotar búsquedas a una raíz.
- Configurar
distributepara repartir un valor sobre una lista o una rejilla. - Aplicar el mismo criterio de reparto a varias propiedades a la vez.
- Decidir cuándo importar estas utilidades y cuándo reimplementarlas.
toArray y selector
toArray(objetivos, ambito) convierte texto de selector, NodeList, HTMLCollection, un elemento suelto o un array en un array real.
const items = gsap.utils.toArray('.item');
const dentro = gsap.utils.toArray('.item', seccion); // solo dentro de seccion
La ganancia frente a querySelectorAll no es la conversión en sí, que es un Array.from, sino que acepta todas las formas por igual. Una función que reciba objetivos y llame a toArray funciona tanto si le pasan una cadena como un elemento, un array o el resultado de otra utilidad, sin comprobaciones de tipo.
function revelar(objetivos, opciones = {}) {
const lista = gsap.utils.toArray(objetivos);
if (!lista.length) return null;
return gsap.from(lista, {
y: 24,
autoAlpha: 0,
duration: 0.5,
stagger: { amount: 0.6, ...opciones },
});
}
// Todas estas llamadas funcionan.
revelar('.tarjeta');
revelar(document.querySelectorAll('.tarjeta'));
revelar([el1, el2, el3]);
revelar(el1);
selector(raiz) devuelve una función de búsqueda acotada a ese elemento. Ya la vimos en la lección de targeting porque es la herramienta que impide que un componente anime los elementos de otro:
const q = gsap.utils.selector(raiz);
q('.titulo'); // solo los .titulo dentro de raiz
q('.texto');
Acepta un elemento, o una referencia de las que envuelven un elemento en los marcos de trabajo modernos, lo cual la hace utilizable directamente desde un componente sin desenvolver nada.
La diferencia entre las dos: toArray con ámbito hace una búsqueda; selector devuelve una función que puedes usar muchas veces. Para dos o más búsquedas en la misma raíz, selector es más limpio.
shuffle(array) completa el grupo. Baraja en el sitio, modificando el array que le pasas, y devuelve ese mismo array. Es útil combinada con toArray para animaciones en orden aleatorio estable, que no es lo mismo que from: "random" en un stagger: aquel sortea los tiempos, este cambia el orden de la lista, con lo que también afecta a cualquier valor que dependa del índice.
const desordenados = gsap.utils.shuffle(gsap.utils.toArray('.pieza'));
gsap.from(desordenados, { autoAlpha: 0, stagger: { amount: 1 } });
distribute: el reparto como función pública
distribute(config) devuelve una función que, dado un índice, un objetivo y la lista, devuelve un valor repartido. Sus propiedades son exactamente las del objeto de stagger más una:
| Propiedad | Qué hace | Por defecto |
|---|---|---|
base |
Valor de partida al que se suma el reparto | 0 |
amount |
Cantidad total a repartir entre todos | — |
each |
Cantidad a sumar entre cada uno | — |
from |
Punto de emanación: índice, palabra clave o par de ratios | 0 |
grid |
Filas y columnas, o "auto" |
— |
axis |
Restringir la medida a "x" o "y" |
— |
ease |
Curva que distribuye los valores | "none" |
Se usa igual que se usaría el stagger, pero sobre cualquier propiedad:
// Los del centro a escala 0.5, los de los bordes a 3.
gsap.to('.celda', {
scale: gsap.utils.distribute({
base: 0.5,
amount: 2.5,
from: 'center',
}),
duration: 0.8,
});
Como devuelve una función con la firma (indice, objetivo, lista), encaja directamente donde GSAP espera una función por objetivo. Y como es una función normal, también sirve fuera de un tween:
const repartidor = gsap.utils.distribute({ base: 0, amount: 100, from: 'center' });
const objetivos = gsap.utils.toArray('.celda');
// Calcular el valor de un elemento concreto sin animar nada.
const valor = repartidor(2, objetivos[2], objetivos);
El uso que más rinde es aplicar el mismo criterio de reparto a varias cosas a la vez, lo cual produce efectos coherentes que parecen mucho más elaborados de lo que son:
const desde = { grid: 'auto', from: 'center' };
gsap.from('.celda', {
scale: gsap.utils.distribute({ base: 0.4, amount: 0.6, ...desde }),
rotation: gsap.utils.distribute({ base: -20, amount: 40, ...desde }),
autoAlpha: 0,
duration: 0.7,
ease: 'power2.out',
stagger: { amount: 0.8, ...desde },
});
Tres repartos —escala, rotación y tiempo— emanando del mismo punto de la misma rejilla. Cambiar from: 'center' por from: [1, 0] recoloca los tres a la vez, y ese es el tipo de coherencia que hace que un efecto se lea como diseñado.
El objeto de stagger no tiene base porque el tiempo siempre parte de cero. Al repartir otras propiedades, el valor de partida importa: sin base, un reparto de escala iría de 0 a la cantidad, y una escala cero no suele ser lo que quieres. base es el valor mínimo, y amount es cuánto se suma como máximo.
checkPrefix y el resto
checkPrefix(propiedad) devuelve el nombre de la propiedad CSS con el prefijo de proveedor si hace falta, o null si el navegador no la soporta en absoluto. En 2026 los prefijos son casi historia, pero la segunda mitad de esa frase sigue siendo útil: es una comprobación de soporte en una línea.
if (gsap.utils.checkPrefix('backdropFilter')) {
gsap.to('.velo', { backdropFilter: 'blur(12px)', duration: 0.4 });
}
Y ya vimos getUnit, que devuelve la unidad de una cadena y es la pieza que necesitas si haces a mano lo que unitize hace automáticamente.
El vocabulario, más allá de la animación
Merece la pena cerrar el nivel con una observación sobre estas dieciséis funciones en conjunto.
Ninguna de ellas anima nada. clamp, mapRange, normalize, snap, wrap, wrapYoyo, interpolate, random, pipe, distribute, splitColor, unitize, getUnit, toArray, shuffle y checkPrefix son aritmética, manipulación de cadenas y operaciones sobre listas. No tocan el ticker, no crean tweens y no dependen del estado del motor.
Eso significa dos cosas prácticas. La primera es que se pueden probar con tests unitarios normales, sin ningún entorno de navegador ni simulación de frames, lo cual es raro en cualquier cosa que venga con una librería de animación. La segunda es que forman un vocabulario que merece la pena tener en cualquier código que traduzca datos a valores visuales, anime o no.
La razón no es ahorrar líneas. Math.min(max, Math.max(min, v)) no es largo. La razón es que una expresión aritmética anónima no dice qué está haciendo, y clamp(0, 100, v) sí. En una revisión de código, la diferencia entre leer (v - 100) / 100 * 45 y leer mapRange(100, 200, 0, 45, v) es la diferencia entre reconstruir la intención y leerla.
Sobre si importarlas o copiarlas: si el proyecto ya usa GSAP, úsalas sin pensarlo, están ahí. Si no lo usa, importar el motor entero por seis funciones aritméticas no tiene sentido, y la respuesta correcta es escribirlas tú con los nombres y las semánticas de esta lección. Son unas cuarenta líneas en total y el valor está en los nombres, no en la implementación.
La propiedad grid: "auto" es la que más gente usa y la que más silenciosamente falla. GSAP deduce filas y columnas midiendo la posición real de los elementos con sus rectángulos, y lo hace en el instante en que se crea el tween. Nunca vuelve a medir.
En una rejilla responsive con auto-fit, eso significa que la animación es correcta en el ancho que tenía la ventana al cargar la página y deja de serlo en cuanto el usuario cambia el tamaño o gira el dispositivo. Con cuatro columnas medidas y tres columnas reales, el punto de emanación cae en un sitio arbitrario y el efecto radial se convierte en ruido. Y como el efecto sigue siendo un escalonado plausible, nadie lo reporta como bug: se reporta como “la animación queda rara en móvil”.
Peor todavía: si la animación se crea antes de que las tipografías web hayan cargado, las medidas son las de la tipografía de reserva, y una rejilla que dependa del contenido puede tener un número de columnas distinto en ese instante.
Las dos mitigaciones son sencillas y hay que elegir una conscientemente. Si el número de columnas es conocido y fijo por breakpoint, escribe grid: [filas, columnas] explícito y olvídate de medir. Si de verdad tiene que ser dinámico, la animación va en una timeline y se reconstruye al cambiar el tamaño con tl.time(0).clear() seguido de la construcción, con la reconstrucción retardada para no hacerla en cada píxel de arrastre. Lo que no funciona es dejar grid: "auto" y confiar.
Escribe la función revelar de esta lección de forma que acepte cualquier forma de objetivo, deduzca si los elementos están en rejilla o en lista, y reparta escala, rotación y tiempo con el mismo from. Añádele un observador de cambio de tamaño que la reconstruya con retardo, y comprueba con las herramientas de desarrollo que al estrechar la ventana el punto de emanación sigue estando donde debe.