wandres.dev
GSAP V · Stagger y utilidades

Utilidades de valor: interpolate, random, splitColor, unitize y pipe

Interpolar entre casi cualquier par de valores, generar aleatorios controlados, descomponer colores, conservar unidades a través de una transformación, y encadenar operaciones.

⏱ 18 min

Las utilidades de la lección anterior trabajan con números desnudos. Este segundo grupo trabaja con lo que de verdad hay en el código de una interfaz: cadenas con unidades, colores, objetos con varias propiedades, y valores que necesitan un poco de azar controlado. Son las que convierten un cálculo aritmético correcto en algo que se puede escribir directamente en un estilo, y pipe es la que permite encadenarlas sin que el resultado parezca una cebolla de paréntesis.

🎯 Al terminar esta lección sabrás
  • Interpolar entre números, colores, cadenas complejas, arrays y objetos.
  • Generar valores aleatorios con rango, incremento y selección de lista.
  • Descomponer un color en sus componentes y volver a componerlo.
  • Envolver una función numérica para que respete unidades con unitize.

interpolate: entre casi cualquier par de valores

interpolate(desde, hasta, progreso) devuelve el valor intermedio. Lo interesante es la variedad de tipos que acepta.

gsap.utils.interpolate(0, 100, 0.5);            // 50
gsap.utils.interpolate('red', 'blue', 0.5);     // "rgba(128,0,128,1)"

Con cadenas complejas localiza los números dentro y los interpola uno a uno conservando el resto:

gsap.utils.interpolate(
  '0px 4px 12px rgba(0,0,0,0.2)',
  '0px 20px 40px rgba(0,0,0,0.5)',
  0.5,
);

Con arrays interpola elemento a elemento, y con objetos propiedad a propiedad:

gsap.utils.interpolate({ x: 0, y: 0 }, { x: 100, y: 50 }, 0.25);
// { x: 25, y: 12.5 }

Y con la forma diferida —omitiendo el progreso— devuelve una función lista para llamar muchas veces, que es la que quieres en un bucle:

const color = gsap.utils.interpolate('#89b4fa', '#f38ba8');

gsap.ticker.add(() => {
  barra.style.backgroundColor = color(progresoActual);
});

Hay una tercera forma que casi nadie conoce y es muy útil: pasarle un array de más de dos valores y una fracción, y reparte la interpolación entre todos ellos. Es un degradado de varias paradas resuelto en una llamada:

const paleta = gsap.utils.interpolate(['#89b4fa', '#a6e3a1', '#f9e2af', '#f38ba8']);
paleta(0);      // el primero
paleta(0.5);    // entre el segundo y el tercero
paleta(1);      // el ultimo

Aleatorios y colores

random: azar con control

random tiene tres formas.

gsap.utils.random(0, 100);        // decimal entre 0 y 100
gsap.utils.random(0, 100, 5);     // multiplo de 5 entre 0 y 100
gsap.utils.random(['a', 'b', 'c']);   // uno de los tres

El tercer argumento, el incremento, es el que la hace utilizable en diseño: un aleatorio continuo produce posiciones que rompen cualquier rejilla, y uno redondeado a ocho píxeles produce variedad dentro de un sistema.

También acepta un cuarto argumento booleano que hace que devuelva una función en lugar de un valor, para poder enchufarla donde se espera una:

const posicion = gsap.utils.random(-100, 100, 5, true);
posicion();   // un valor nuevo en cada llamada

Sin ese cuarto argumento obtienes un número, y si lo pones como valor de un tween ese número se calcula una vez y se usa para todos los objetivos. Es una fuente de confusión: dentro de un tween, la forma correcta de tener un aleatorio distinto por elemento es la cadena "random(-100, 100, 5)", que GSAP resuelve por objetivo, o la forma con el cuarto argumento a true.

splitColor: descomponer un color

splitColor(color, hsl, alpha) devuelve un array con los componentes. Por defecto, rojo, verde y azul:

gsap.utils.splitColor('red');        // [255, 0, 0]
gsap.utils.splitColor('#89b4fa');    // [137, 180, 250]

Con el segundo argumento a true devuelve tono, saturación y luminosidad, lo cual es lo que necesitas para operaciones perceptualmente sensatas: aclarar un color es subir la luminosidad, no multiplicar los tres canales.

const [h, s, l] = gsap.utils.splitColor('#89b4fa', true);
const masClaro = `hsl(${h} ${s}% ${Math.min(100, l + 15)}%)`;

Y con el tercer argumento a true incluye el canal alfa, útil cuando el color puede venir con transparencia y no quieres perderla.

El caso de uso que justifica su existencia: leer un color del CSS computado, modificarlo, y devolverlo. Sin splitColor hay que analizar la cadena a mano, con todas sus formas posibles.

const actual = getComputedStyle(el).backgroundColor;
const [r, g, b] = gsap.utils.splitColor(actual);
gsap.to(el, { backgroundColor: `rgb(${255 - r}, ${255 - g}, ${255 - b})`, duration: 0.4 });

unitize: conservar la unidad

Este es el que resuelve un problema molesto y del que nadie se acuerda hasta que lo tiene delante. Las utilidades numéricas trabajan con números; los valores de CSS son cadenas con unidad. unitize envuelve una función numérica para que acepte y devuelva cadenas con unidad, quitándola antes de llamar y volviéndola a poner después.

const envolver = gsap.utils.unitize(gsap.utils.wrap(0, 100));
envolver('150px');   // "50px"

Con un segundo argumento, fuerza una unidad concreta en la salida sea cual sea la de entrada:

const aPorcentaje = gsap.utils.unitize(gsap.utils.mapRange(-10, 10, 0, 100), '%');
aPorcentaje(5);   // "75%"

Su uso canónico es el carrusel infinito con modifiers, donde el valor que llega es una cadena con unidad y la operación de envolver es numérica:

gsap.to('.tira', {
  x: '-=1200',
  duration: 12,
  ease: 'none',
  repeat: -1,
  modifiers: {
    x: gsap.utils.unitize(gsap.utils.wrap(-1200, 0)),
  },
});

Esas tres líneas de modifiers son el carrusel infinito completo: el tween mueve la tira indefinidamente hacia la izquierda y el modificador envuelve el valor al rango de una vuelta antes de aplicarlo. Sin unitize habría que quitar y poner el px a mano en cada frame.

También está getUnit, que devuelve la unidad de una cadena, por si necesitas hacer la operación tú:

gsap.utils.getUnit('30px');   // "px"
gsap.utils.getUnit('2.5rem'); // "rem"

pipe: encadenar sin paréntesis anidados

pipe recibe varias funciones y devuelve una que las aplica en orden, pasando el resultado de cada una a la siguiente.

const procesar = gsap.utils.pipe(
  gsap.utils.clamp(0, 100),
  gsap.utils.snap(5),
);

procesar(8);     // 10
procesar(-30);   // 0
procesar(250);   // 100

Sin pipe, eso es gsap.utils.snap(5, gsap.utils.clamp(0, 100, 8)), que se lee del revés: la primera operación que se aplica está escrita la última. Con cuatro operaciones encadenadas la expresión se vuelve ilegible, y ese es exactamente el momento en que alguien introduce un error de orden.

La combinación con unitize cubre el caso completo de “entra una cadena con unidad, sale otra”:

const posicionValida = gsap.utils.pipe(
  gsap.utils.clamp(0, 1200),
  gsap.utils.snap(20),
  gsap.utils.mapRange(0, 1200, 0, 100),
);

const conUnidad = gsap.utils.unitize(posicionValida, '%');
conUnidad(640);   // "53.333...%" tras recortar, pegar a 20 y mapear
💡
pipe también funciona con funciones tuyas

No hay nada especial en las funciones de gsap.utils: pipe acepta cualquier función de un argumento. Mezclar utilidades con lógica propia es lo normal, y es la forma de tener una transformación con nombre en lugar de una expresión.

const aOpacidad = gsap.utils.pipe(
  gsap.utils.normalize(0, 600),
  gsap.utils.clamp(0, 1),
  gsap.parseEase('power2.out'),
  (v) => 1 - v,
);
El orden en pipe importa más de lo que parece: recorta antes de mapear

Hay un error de composición que produce resultados incorrectos sin dar ningún síntoma, y merece la pena reconocerlo. Compara estas dos tuberías:

const a = gsap.utils.pipe(gsap.utils.clamp(0, 1200), gsap.utils.mapRange(0, 1200, 0, 100));
const b = gsap.utils.pipe(gsap.utils.mapRange(0, 1200, 0, 100), gsap.utils.clamp(0, 1200));

La primera recorta la entrada al rango de entrada y luego mapea: con 2000 da 100. La segunda mapea primero —dando 166,7— y luego recorta con un rango que ya no corresponde, porque 166,7 está muy por debajo de 1200 y pasa intacto. El resultado es un valor de salida fuera de rango que se propaga silenciosamente hasta que alguien ve una opacidad de 1,67 o una barra que se sale del contenedor.

La regla que lo evita: el recorte pertenece al espacio en el que estás cuando lo aplicas. Si recortas la entrada, usa el rango de entrada; si recortas la salida, usa el de salida. Y si recortas en los dos sitios, escribe los dos rangos, que cuesta una línea y elimina la clase entera de bugs. Es el mismo cuidado que hay que tener con las unidades en física: una operación puede ser aritméticamente correcta y estar en el espacio equivocado.

⚔️ Reto práctico

Monta un termómetro completo con estas utilidades y ninguna aritmética suelta: una entrada de temperatura de -20 a 45 grados que se traduzca en una altura de barra en porcentaje, un color interpolado sobre una paleta de cuatro paradas, y un valor de texto pegado a incrementos de medio grado. Todo debe salir de un pipe por cada salida, sin ninguna división escrita a mano.