Extracción de Movimiento desde Video y Prototipos (video-to-gsap)
1. Propósito y Disparadores
Las microinteracciones fluidas y el movimiento cinematográfico diferencian las experiencias de comercio de alta gama. Sin embargo, las animaciones web mal implementadas provocan bloqueos del hilo principal, fotogramas caídos (jank), fugas de memoria y una mala Interaction to Next Paint (INP). Esta habilidad gobierna la extracción e implementación de animaciones sedosas a 60/120 FPS.
Disparadores:
- Cuando el usuario proporciona un clip de video, un GIF o un prototipo de movimiento de Figma.
- Al animar deslizamientos de carrito, transiciones de página, expansiones de acordeón, hovers en tarjetas de producto o celebraciones de finalización de pago.
- Al crear animaciones controladas por desplazamiento (scroll) con ScrollTrigger.
2. Protocolo de Descomposición de Movimiento
Al analizar un clip de movimiento, identifica sistemáticamente:
- La Secuencia de Coreografía:
- Escenario / Disparador: ¿Qué acción inicia el movimiento (hover, scroll, cambio de ruta, clic)?
- Anticipación: ¿Hay un sutil contra-movimiento antes del movimiento principal?
- Acción y Recorrido: ¿Qué elementos se mueven, en qué direcciones y sobre qué distancia (px, rem o %)?
- Asentamiento y Seguimiento: ¿Sobrepasa (spring/elastic) o desacelera suavemente (
power3.out,expo.out)?
- Desglose de Tiempos:
- Duración de cada elemento (ej., entrada del cajón:
450ms, desvanecimiento del fondo:300ms). - Desfase (stagger) entre hijos escalonados (ej., tarjetas de cuadrícula de productos:
0.06sde desfase).
- Duración de cada elemento (ej., entrada del cajón:
- Firma del Easing:
- Lineal: estrictamente para bucles continuos (ej., marquee).
- Ease-out (
power2.out,expo.out): para entradas de UI (entrada rápida, asentamiento suave). - Ease-in (
power2.in): para salidas de UI (acelera al desaparecer de la pantalla). - Ease-in-out (
power3.inOut): para transformaciones de estado que permanecen visibles.
3. Estándares de Rendimiento y Aceleración por Hardware
Regla 1: Animar Solo Propiedades Compuestas
- Permitido:
transform(x,y,xPercent,yPercent,scale,rotation,skew) yopacity(oautoAlphade GSAP). - Estrictamente Prohibido: Animar propiedades de layout o pintura (
width,height,top,left,right,bottom,margin,padding,border-width). Estas fuerzan al CPU del navegador a recalcular la geometría del layout y repintar píxeles en cada fotograma, provocando degradación del INP y caída de fotogramas.
Regla 2: Cero Fugas de Memoria con useGSAP con Alcance
- Usa siempre el paquete oficial
@gsap/reacty el hookuseGSAP. - Proporciona una referencia
scopeal contenedor. Cada selector dentro del mismo tendrá alcance automático, y todas las líneas de tiempo, tweens e instancias de ScrollTrigger se revertirán y eliminarán limpiamente al desmontar el componente. - Esto evita oyentes de eventos zombis y retención de memoria en aplicaciones de una sola página (SPA) y en ciclos de recarga rápida de React 19.
Regla 3: Eliminar FOUC (Flashes de Contenido Sin Estilo)
- Usa
autoAlphaen lugar deopacity.autoAlphaestablecevisibility: hiddencuando la opacidad llega a 0, evitando clics fantasma y destellos de renderizado antes de la hidratación. - Alternativamente, declara estados ocultos por defecto en CSS y revélalos con
gsap.fromTo.
4. Plan de Implementación en Producción
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import { ScrollTrigger } from "gsap/ScrollTrigger";
// Asegura que los plugins se registren de forma segura
if (typeof window !== "undefined") {
gsap.registerPlugin(useGSAP, ScrollTrigger);
}
interface ProductShowcaseProps {
readonly items: Array<{ id: string; title: string; price: string }>;
}
export function ProductShowcase({ items }: ProductShowcaseProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
useGSAP(
() => {
// Línea de tiempo de entrada acelerada por hardware
const tl = gsap.timeline({
defaults: {
ease: "expo.out",
duration: 0.8,
},
});
tl.from(".showcase-header", {
autoAlpha: 0,
y: 30,
clearProps: "all", // Limpia estilos en línea después de la animación
}).from(
".product-card",
{
autoAlpha: 0,
y: 40,
scale: 0.96,
stagger: 0.08,
duration: 0.7,
clearProps: "transform,opacity",
},
"-=0.4" // Solapamiento sin fisuras
);
},
{ scope: containerRef } // Límite de limpieza con alcance
);
return (
<section ref={containerRef} className="w-full py-16 px-6">
<div className="showcase-header max-w-4xl mx-auto text-center mb-12">
<h2 className="text-h1 font-bold text-content-primary">Colección Destacada</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-7xl mx-auto">
{items.map((item) => (
<article
key={item.id}
className="product-card will-change-transform bg-surface-elevated rounded-lg p-6 border border-border-subtle"
>
<h3 className="text-h3 font-semibold text-content-primary">{item.title}</h3>
<p className="text-content-secondary mt-2">{item.price}</p>
</article>
))}
</div>
</section>
);
}
5. Cumplimiento de Movimiento Reducido
Respeta siempre a los usuarios que prefieren movimiento reducido:
const { contextSafe } = useGSAP({ scope: containerRef });
// Verifica la preferencia de accesibilidad
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (prefersReducedMotion) {
// Renderiza instantáneamente en estado final de reposo sin animación
gsap.set([".showcase-header", ".product-card"], { autoAlpha: 1, y: 0, scale: 1 });
return;
}