Utilidades de rango: mapRange, clamp, normalize, snap, wrap y wrapYoyo
Las seis operaciones sobre números que aparecen en cualquier código de animación, con su semántica exacta, y por qué todas devuelven una función si les quitas el último argumento.
Cualquier código que traduzca un valor de entrada en un valor visual acaba escribiendo las mismas seis operaciones: llevar un número de un rango a otro, recortarlo para que no se salga, convertirlo a una fracción, redondearlo a incrementos, hacerlo dar la vuelta al pasarse, o hacerlo rebotar. Están en gsap.utils, están bien resueltas —incluidos los casos con negativos, que es donde el operador módulo de JavaScript miente— y funcionan perfectamente sin GSAP de por medio, porque solo hacen aritmética.
- Aplicar cada una de las seis utilidades con su semántica exacta.
- Aprovechar la forma diferida que devuelve una función reutilizable.
- Resolver el caso del módulo con negativos sin escribirlo a mano.
- Combinar
normalizeymapRangepara traducir entre espacios de valores.
Las seis operaciones
clamp(min, max, valor) recorta al rango.
gsap.utils.clamp(0, 100, -12); // 0
gsap.utils.clamp(0, 100, 250); // 100
gsap.utils.clamp(0, 100, 42); // 42
normalize(min, max, valor) convierte un valor de un rango en una fracción entre cero y uno.
gsap.utils.normalize(100, 200, 150); // 0.5
No recorta: un valor fuera del rango da una fracción fuera de cero-uno, que a menudo es exactamente lo que quieres para detectar el desbordamiento.
mapRange(entradaMin, entradaMax, salidaMin, salidaMax, valor) traduce entre dos rangos. Es normalize seguido de una interpolación, y es la operación más usada de las seis.
gsap.utils.mapRange(-10, 10, 0, 100, 5); // 75
snap(incremento, valor) redondea al múltiplo más cercano. Con un array en vez de un número, devuelve el elemento más cercano.
gsap.utils.snap(5, 13); // 15
gsap.utils.snap([0, 5, 10], 7); // 5
wrap(min, max, valor) envuelve al rango: al pasarse del máximo vuelve al mínimo. Con un array y un índice, cicla sobre él.
gsap.utils.wrap(5, 10, 12); // 7
gsap.utils.wrap([0, 10, 20], 4); // 10
wrapYoyo(min, max, valor) hace lo mismo pero rebotando en vez de saltando: al pasarse del máximo vuelve hacia atrás.
gsap.utils.wrapYoyo(5, 10, 12); // 8
gsap.utils.wrapYoyo([0, 10, 20, 30], 4); // 20
La diferencia entre wrap y wrapYoyo es la diferencia entre un carrusel infinito y un péndulo. wrap(0, 3, 4) da 1, saltando del final al principio; wrapYoyo(0, 3, 4) da 2, dando la vuelta.
wrap no es valor % max. El operador módulo de JavaScript conserva el signo del dividendo, así que -1 % 3 da -1 y no 2. Ciclar hacia atrás con módulo produce índices negativos y, con arrays, undefined. La forma correcta es ((v % r) + r) % r, con dos operaciones de módulo, y es el bug que aparece en el 90% de los carruseles infinitos escritos a mano cuando el usuario retrocede desde el primer elemento.
La forma diferida
Las seis aceptan que omitas el último argumento, y en ese caso devuelven una función que espera ese valor. Es una currificación parcial, y no es azúcar: es lo que las hace enchufables directamente donde GSAP espera una función.
const recortar = gsap.utils.clamp(0, 100);
recortar(-12); // 0
recortar(250); // 100
const aPorcentaje = gsap.utils.mapRange(0, 800, 0, 100);
aPorcentaje(400); // 50
const aRejilla = gsap.utils.snap(8);
aRejilla(13); // 16
Con eso, se pueden usar como valor de un tween sin escribir un envoltorio, porque GSAP llama a la función una vez por objetivo pasándole el índice:
// Cada elemento coge un color de la lista, ciclando.
gsap.to('.punto', {
backgroundColor: gsap.utils.wrap(['#f38ba8', '#a6e3a1', '#89b4fa']),
duration: 0.6,
});
Ese wrap con array devuelve una función que, dado el índice, devuelve el elemento correspondiente ciclando. Cuatro elementos y tres colores dan rosa, verde, azul, rosa.
Componer las traducciones
El patrón que aparece una y otra vez es traducir un valor de un espacio a otro con recorte. Escribirlo con las utilidades queda legible:
// El scroll vertical, de 0 a 1200 px, se traduce en una rotacion de 0 a 45 grados,
// sin pasarse aunque el usuario siga bajando.
const aRotacion = gsap.utils.pipe(
gsap.utils.clamp(0, 1200),
gsap.utils.mapRange(0, 1200, 0, 45),
);
gsap.ticker.add(() => {
gsap.set('.aguja', { rotation: aRotacion(window.scrollY) });
});
pipe encadena funciones pasando el resultado de cada una a la siguiente, y lo vemos en detalle en la lección de utilidades de valor. El punto aquí es que la forma diferida es lo que hace posible esa composición: sin ella habría que escribir funciones flecha envolviendo cada llamada.
Otro par que se repite: normalizar y volver a mapear para pasar por un ease.
const ease = gsap.parseEase('power2.out');
function opacidadPorDistancia(distancia) {
const t = gsap.utils.clamp(0, 1, gsap.utils.normalize(0, 300, distancia));
return 1 - ease(t);
}
Ahí normalize convierte una distancia en píxeles a una fracción, clamp la mantiene en rango, y el ease la curva. Tres operaciones nombradas en vez de una expresión aritmética con divisiones y mínimos.
Casos donde cada una es la respuesta
clamp cuando un valor externo puede salirse: posiciones de puntero, valores de scroll, entradas del usuario. Es también la forma de evitar los NaN que produce dividir por rangos degenerados.
normalize cuando necesitas una fracción para alimentar un ease o una interpolación. Es la operación previa a casi cualquier cosa.
mapRange cuando dos magnitudes con unidades distintas tienen que relacionarse linealmente. Es la única de las seis que aparece en prácticamente todos los proyectos.
snap para rejillas de posicionamiento, para valores que deben caer en incrementos discretos, y para el punto de reposo de un arrastre. La variante con array es la que resuelve “que se pegue al más cercano de estos puntos”.
wrap para carruseles infinitos, para ciclar sobre paletas de color, y para ángulos que deben mantenerse en el rango de una vuelta.
wrapYoyo para movimientos de vaivén y para índices que deben rebotar en vez de saltar, como una lista que se recorre adelante y atrás.
// Carrusel infinito: el indice siempre cae dentro del array.
const siguiente = gsap.utils.wrap(0, diapositivas.length);
let actual = 0;
botonSiguiente.onclick = () => mostrar(actual = siguiente(actual + 1));
botonAnterior.onclick = () => mostrar(actual = siguiente(actual - 1));
Fíjate en que retroceder desde cero funciona: siguiente(-1) con cinco diapositivas devuelve 4. Escrito con % a mano devolvería -1.
gsap.utils es aritmética pura: no toca el DOM, no crea animaciones, no depende del ticker. Eso significa que se pueden importar y usar en código que no tiene nada que ver con animación —cálculo de layout, procesamiento de datos, lógica de un juego— y que se pueden probar con tests unitarios normales sin ningún entorno de navegador.
De ahí sale una decisión de arquitectura que rinde: cuando estas operaciones aparecen en tu código de dominio escritas a mano, sustitúyelas. No por ahorrar líneas —Math.min(max, Math.max(min, v)) no es largo— sino porque una expresión aritmética anónima no dice qué está haciendo, y clamp sí. El código que traduce entre espacios de valores es el que más se reescribe mal en las revisiones, precisamente porque nadie recuerda si aquel / 300 * 45 era una normalización o un mapeo.
Y si te preocupa el peso: importar gsap para usar solo utils es traer el motor entero, así que en un módulo que no anima nada la respuesta correcta no es importar GSAP, sino copiar las seis funciones. Son diez líneas, están bien especificadas en esta lección, y con las dos operaciones de módulo bien puestas se comportan igual.
Escribe tú las seis en un módulo propio, con la forma diferida incluida, y pásales una batería de pruebas con los valores de esta lección más los casos límite: rango invertido, valor exactamente en el máximo, índices negativos grandes y arrays de un solo elemento. Compara tus resultados con los de gsap.utils y anota las diferencias; las que encuentres serán casi todas en los negativos.