wandres.dev
GSAP X · Plugins de SVG

MotionPathPlugin: align, autoRotate y el recorrido

Cómo se mueve un elemento por un trazado, qué hace exactamente align, cómo se centra con alignOrigin, y por qué la alineación no es responsive.

⏱ 19 min

Mover un elemento a lo largo de una curva parece un problema resuelto: hay una propiedad de CSS que lo hace, offset-path, y funciona bien. Lo que offset-path no resuelve es que el elemento a mover esté fuera del SVG donde vive la curva, que la curva esté dentro de un contenedor escalado, que quieras arrancar a mitad del recorrido, o que necesites que el trazado sea un array de puntos calculado en tiempo de ejecución. MotionPathPlugin cubre todo eso, y su opción más importante —align— es precisamente la que resuelve el problema de los sistemas de coordenadas que hace difícil el caso general.

🎯 Al terminar esta lección sabrás
  • Mover un elemento por un trazado definido de las tres formas posibles.
  • Explicar qué hace align y por qué sin él las coordenadas no cuadran.
  • Centrar el elemento sobre el trazado con alignOrigin y orientarlo con autoRotate.
  • Recorrer solo una porción del trazado con start y end.

Tres formas de definir el trazado

motionPath acepta, en su forma corta, cualquiera de estas tres cosas.

import { gsap } from "gsap";
import { MotionPathPlugin } from "gsap/MotionPathPlugin";

gsap.registerPlugin(MotionPathPlugin);

// 1) Un elemento path del documento
gsap.to(".nave", { duration: 4, motionPath: "#ruta" });

// 2) Datos de path crudos
gsap.to(".nave", { duration: 4, motionPath: "M9,100c0,0,18-41,49-65" });

// 3) Un array de puntos
gsap.to(".nave", {
  duration: 4,
  motionPath: [{ x: 0, y: 0 }, { x: 120, y: -60 }, { x: 240, y: 40 }, { x: 360, y: 0 }],
});

La tercera forma tiene dos comportamientos que conviene conocer. Por defecto, las coordenadas actuales del elemento se anteponen al array, de modo que la trayectoria empieza donde el elemento ya está en lugar de saltar al primer punto; se desactiva con fromCurrent: false. Y la curvatura entre puntos se controla con curviness, donde 0 da segmentos rectos con esquinas duras, 1 es el valor por defecto y 2 exagera las curvas.

Existen además dos variantes del array. Con relative: true, cada valor se interpreta respecto al anterior en lugar de en coordenadas absolutas. Y con type: "cubic", el array se lee como una secuencia de curvas cúbicas de Bézier: ancla, dos puntos de control, ancla, dos puntos de control, y así.

align: el problema de las coordenadas

Sin align, el plugin toma los números del trazado y los mete directamente en las transformaciones x e y del elemento. Si tu path dice que va del punto 40,120 al 380,200, tu elemento se desplazará 40 píxeles a la derecha y 120 hacia abajo respecto a donde estuviera, y de ahí a 380,200.

Eso funciona si el elemento vive en el mismo sistema de coordenadas que el path y está en el origen. En cualquier otro caso, el elemento recorre una trayectoria con la forma correcta en el sitio equivocado.

align arregla eso doblando los sistemas de coordenadas: calcula la transformación necesaria para que el trazado del elemento coincida con el del path en pantalla, sin importar cuántos contenedores transformados haya entre ambos.

gsap.to(".nave", {
  duration: 5,
  ease: "power1.inOut",
  motionPath: {
    path: "#ruta",
    align: "#ruta",
    alignOrigin: [0.5, 0.5],
    autoRotate: true,
  },
});

Ese bloque es la forma canónica y la que deberías escribir por defecto. align: "#ruta" alinea el elemento con el path, que es lo que quieres el noventa por ciento de las veces. Y funciona aunque el elemento sea un div de HTML fuera del SVG: el plugin convierte entre ambos espacios de coordenadas.

Hay un valor especial: align: "self". En lugar de mover el elemento al path, mueve el path al elemento, como si el trazado se hubiera dibujado partiendo de la posición actual. Es lo que quieres cuando no quieres que el elemento salte al inicio del recorrido. Se afina con offsetX y offsetY.

alignOrigin y autoRotate

Por defecto, align con un elemento alinea la esquina superior izquierda del objetivo con el trazado. Casi nunca es lo que quieres: si mueves un cohete por una curva, quieres que sea su centro el que la siga.

alignOrigin recibe un array de dos valores de progreso: [0.5, 0.5] es el centro, [1, 0] la esquina superior derecha, [0, 1] la inferior izquierda. Y hace además algo que ahorra un error clásico: ajusta también el transformOrigin del elemento al punto correspondiente, de modo que la rotación gira alrededor del mismo punto que sigue la curva.

autoRotate orienta el elemento en la dirección de la marcha. Con true, el ángulo del elemento coincide con la tangente del trazado. Con un número, se le suma ese desplazamiento en grados, lo cual hace falta cuando el dibujo del elemento apunta hacia arriba en lugar de hacia la derecha.

// Un icono dibujado apuntando hacia arriba necesita 90 grados de correccion
gsap.to(".flecha", {
  duration: 4,
  motionPath: { path: "#ruta", align: "#ruta", alignOrigin: [0.5, 0.5], autoRotate: 90 },
});

Si no usas alignOrigin, tienes dos alternativas para centrar: poner xPercent: -50, yPercent: -50 en el elemento antes del tween, o fijar transformOrigin: "50% 50%" en las vars del tween. Fíjate en que transformOrigin va fuera del objeto motionPath, como propiedad hermana.

⚠️
align no es responsive

Los cálculos de alineación se hacen una sola vez, al empezar la animación. Si el usuario redimensiona la ventana a mitad del recorrido, el elemento sigue la trayectoria calculada con las medidas viejas. La documentación no ofrece una opción para esto; hay que rehacerlo a mano: guardar el progress() del tween, matarlo, crear uno nuevo y colocarlo en el progreso guardado.

let tween = crearRecorrido();

window.addEventListener("resize", () => {
  const p = tween.progress();
  tween.progress(0).kill();
  tween = crearRecorrido();
  tween.progress(p);
});

function crearRecorrido() {
  return gsap.to(".nave", {
    duration: 5,
    ease: "none",
    repeat: -1,
    motionPath: { path: "#ruta", align: "#ruta", alignOrigin: [0.5, 0.5], autoRotate: true },
  });
}

Porciones del recorrido

start y end son valores de progreso que delimitan el tramo recorrido. Por defecto van de 0 a 1.

// Recorrer solo el tramo central
motionPath: { path: "#ruta", start: 0.25, end: 0.75 }

// Recorrer hacia atras
motionPath: { path: "#ruta", start: 1, end: 0 }

// Vuelta y media
motionPath: { path: "#ruta", start: 0, end: 1.5 }

Aceptan decimales positivos y negativos, y end puede ser menor que start, lo que hace que se recorra en sentido inverso. Valores mayores que 1 dan la vuelta, útil en contornos cerrados.

Y hay dos ajustes finos que rara vez hacen falta pero conviene saber que existen: resolution, que controla en cuántos trozos se divide cada segmento para corregir el ritmo del avance —por defecto 12—, y useRadians, que hace que la rotación se exprese en radianes en lugar de grados, pensado para motores que trabajan así.

La herramienta de edición

Ajustar una curva escribiendo coordenadas es tedioso. MotionPathHelper monta un editor interactivo sobre la propia página: arrastras anclas y manejadores, ves el resultado en vivo, y un botón copia los datos del path al portapapeles.

import { MotionPathHelper } from "gsap/MotionPathHelper";
gsap.registerPlugin(MotionPathPlugin, MotionPathHelper);

const t = gsap.to(".nave", {
  duration: 4,
  motionPath: { path: "#ruta", align: "#ruta", alignOrigin: [0.5, 0.5], autoRotate: true },
});

MotionPathHelper.create(t);   // solo durante el desarrollo

Requiere MotionPathPlugin registrado. Acepta también un elemento o texto de selector en lugar de un tween, en cuyo caso crea una curva básica que puedes editar desde cero.

align resuelve un problema que no es de animación sino de álgebra de matrices, y por eso es la opción más valiosa del plugin

Vale la pena entender qué está haciendo align por debajo, porque es la parte del plugin que de verdad te ahorra trabajo y porque el problema que resuelve aparece en muchos más sitios. Un elemento cualquiera de una página vive en un sistema de coordenadas que es el resultado de componer todas las transformaciones de sus ancestros, más el viewBox de cualquier SVG que lo contenga, más el desplazamiento de scroll de cualquier contenedor desplazable del camino. Un path dentro de un SVG vive en el sistema del viewBox de ese SVG, que a su vez está escalado según las dimensiones que el CSS le haya dado al elemento contenedor. Cuando quieres que un div de HTML siga una curva dibujada dentro de un SVG, estás pidiendo convertir puntos de un espacio al otro, y esa conversión es una multiplicación de matrices que hay que componer recorriendo el árbol hacia arriba desde ambos elementos hasta encontrar un ancestro común. Hacerlo a mano es perfectamente posible —el propio plugin expone getGlobalMatrix y convertCoordinates para quien lo necesite— y es exactamente el tipo de código que se escribe mal la primera vez, se arregla con un factor de corrección obtenido por prueba y error, y se rompe en cuanto alguien añade un transform a un contenedor intermedio. Que una opción llamada align esconda toda esa maquinaria detrás de una cadena de selector es probablemente el mejor ejemplo de todo el ecosistema de GSAP de lo que significa una buena abstracción: no oculta el problema, oculta el álgebra, y deja expuesta la parte que sí tiene que decidir el humano, que es qué punto del elemento debe seguir la curva. El único aviso, y es importante, es que la abstracción es de un solo disparo: la matriz se compone una vez al empezar. En cuanto entiendes que lo que se calculó fue una matriz y no una relación viva, la limitación responsive deja de parecer un descuido y se convierte en lo que es, una consecuencia inevitable de haber resuelto el álgebra por adelantado.

⚔️ Seguir la curva
  1. Mueve un elemento por un path sin align y observa dónde acaba. Añade align y compara.
  2. Quita alignOrigin: [0.5, 0.5] y comprueba qué esquina del elemento sigue la curva.
  3. Pon autoRotate: true sobre un icono que apunte hacia arriba y corrige con el desplazamiento en grados.
  4. Redimensiona la ventana a mitad de un recorrido en bucle y comprueba el desajuste. Implementa la recreación con progress().
  5. Recorre solo el tramo del 25% al 75% de un contorno cerrado, y luego dale una vuelta y media.