Communitygithub.com

html-motion-graphics

Construye gráficos en movimiento deterministas de calidad cinematográfica como un archivo `index.html` independiente sin servidor, impulsado por GSAP, desenfoque de movimiento SVG direccional, un rig de cámara unificado `#world` y fondos opcionales de Three.js WebGL.

¿Qué es html-motion-graphics?

Esta habilidad permite a los agentes de IA generar gráficos en movimiento de alta calidad, similares a los de una película, directamente como un solo archivo HTML autónomo. Está diseñada para crear videos promocionales, introducciones de productos, tipografía cinética y tutoriales visuales que se pueden reproducir localmente desde el disco sin necesidad de un servidor. La habilidad utiliza GSAP para animaciones precisas, un rig de cámara unificado (`#world`) para movimientos coordinados y desenfoque de movimiento SVG direccional para obtener resultados de apariencia profesional. También garantiza un 100% de determinismo, lo que significa que cada cuadro es idéntico en cada reproducción, lo que permite una depuración precisa con desplazamiento de fotogramas y renderizado de MP4 sin cabeza. La habilidad es compatible con cualquier plataforma que pueda ejecutar un navegador moderno y admite fondos Three.js WebGL para mayor profundidad. El flujo de trabajo abarca desde la definición de un concepto creativo hasta la exportación de un video MP4 final, siguiendo estrictas reglas de diseño para evitar el aspecto de una presentación de diapositivas genérica.

Compatible con~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/YanaMiku/html-motion-graphics/tree/HEAD

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

HTML Motion Graphics

Construye gráficos en movimiento deterministas de calidad cinematográfica como un archivo index.html independiente sin servidor, impulsado por GSAP, desenfoque de movimiento SVG direccional, un rig de cámara unificado #world y fondos opcionales de Three.js WebGL.

Cuándo Usarlo

  • Crear un video promocional, apertura de lanzamiento de producto, avance o bumper de marca en index.html.
  • Construir tipografía cinética, recorridos de UI de productos SaaS o explicadores visuales de múltiples escenas.
  • Convertir animaciones web estáticas similares a diapositivas en diseño de movimiento real impulsado por cámara.
  • Entregar un index.html portátil que se abra directamente a través de file:// y admita desplazamiento de fotogramas preciso y renderizado MP4 sin cabeza.

Flujo de Trabajo Principal

Paso 1: Fijar el Brief Creativo y el Concepto del Fotograma

Antes de escribir código, define un breve estilo conciso para que el resultado nunca parezca una plantilla de diapositivas genérica:

  1. Relación de Aspecto y Resolución: Por defecto 1920x1080 (16:9) para escritorio/YouTube, o 1080x1920 (9:16) para Reels/TikTok/Shorts (envuelve el contenido en un contenedor .safe escalado a 0.78 con transform-origin: 50% 45% para el espacio libre HUD social 9:16).
  2. Arquitectura del Concepto: Elige una estructura conceptual clara basada en el tema en lugar de usar por defecto gancho -> 3 mosaicos de iconos -> CTA:
    • Viaje Continuo por el Mundo / Cámara: Un espacio unificado donde la cámara hace dolly, orbita o se sumerge a través de escenas.
    • Recorrido de UI en Vivo: Componentes de producto DOM/SVG reconstruidos donde un cursor/toque interactúa, los paneles se expanden y los datos se animan de forma determinista.
    • Problema a Claridad: El caos visual se acumula, luego una sola acción del producto limpia el encuadre dejando la marca de la empresa.
    • Tipografía Cinética Pura: Las palabras entran desde varios ejes, cambian de peso/escala y se ensamblan en la marca final.
    • Dibujo Animado / Acción Continua: El personaje o sujeto principal permanece activo mientras los fondos, instrumentos y etiquetas diegéticas cambian a su alrededor.
  3. Sistema de Color y Superficie: Define propiedades personalizadas de CSS (--base, --ink, --accent, --accent-lit). Nunca dejes un fondo de un solo color plano. Combina al menos dos capas (por ejemplo, resplandor de horizonte radial + gradiente lineal, manchas suaves de malla o nebulosa WebGL + polvo a la deriva + superposición de grano de película SVG).
  4. Plan de Audio y Temporización: Fija la duración total (DURATION en segundos) y asigna cada tiempo fuerte a marcas de tiempo absolutas en segundos.

Paso 2: Construir la Arquitectura del index.html Independiente

Entrega un solo index.html autocontenido con <style> en línea y <script type="module"> que importe GSAP (y Three.js si se necesita profundidad 3D/partículas) desde https://esm.sh o https://cdn.jsdelivr.net.

  • Escenario de Resolución Fija: Bloquea #stage a 1920x1080 (o 1080x1920) y escala proporcionalmente para ajustarse a cualquier viewport mediante CSS --fit para que las coordenadas de composición nunca cambien entre monitores.
  • Rig de Cámara Unificado #world: Coloca <canvas> (si es WebGL), gradientes de fondo y capas de escena #dom dentro de #world. Cuando la cámara hace zoom o paneo, tanto el fondo como el primer plano se mueven juntos. Coloca las superposiciones de lente (fugas de luz, viñeta, grano de película) fuera de #world.
  • Estados Ocultos Primero en CSS: Establece opacity: 0 en los elementos animados (.line, .ch, .hl-t, .obj) en CSS y usa immediateRender: false en los tweens fromTo de GSAP para evitar destellos en el primer fotograma.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Motion Graphic</title>
<style>
  :root {
    --base: #070913;
    --ink: #f8fafc;
    --accent: #38bdf8;
    --accent-lit: #bae6fd;
    --fit: 1;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: system-ui, -apple-system, sans-serif; }
  #stage {
    position: fixed; left: 50%; top: 50%; width: 1920px; height: 1080px;
    transform: translate(-50%, -50%) scale(var(--fit));
    overflow: hidden; background: var(--base); color: var(--ink);
  }
  #world { position: absolute; inset: -240px; transform-origin: 50% 50%; will-change: transform; }
  .bg-layer {
    position: absolute; inset: 0;
    background:
      radial-gradient(70% 45% at 50% 105%, color-mix(in srgb, var(--accent) 32%, transparent), transparent 70%),
      linear-gradient(180deg, #04060c 0%, var(--base) 60%, #0d1326 100%);
  }
  .scene { position: absolute; inset: 240px; display: grid; place-items: center; pointer-events: none; }
  .line { font-size: 112px; font-weight: 600; letter-spacing: -0.02em; line-height: 1.1; white-space: nowrap; opacity: 0; text-shadow: 0 6px 32px rgba(0,0,0,0.55); }
  .wd { display: inline-block; white-space: nowrap; }
  .ch { display: inline-block; opacity: 0; will-change: transform, opacity, filter; }
  .vignette { position: absolute; inset: 0; pointer-events: none; background: radial-gradient(circle at 50% 50%, transparent 58%, rgba(0,0,0,0.65) 100%); }
  .grain { position: absolute; inset: -60px; pointer-events: none; filter: url(#grain); opacity: 0.16; mix-blend-mode: overlay; }
  #hud { position: fixed; bottom: 16px; left: 50%; transform: translateX(-50%); display: none; gap: 10px; align-items: center; background: rgba(15,23,42,0.85); padding: 8px 14px; border-radius: 999px; color: #fff; font: 13px/1 monospace; z-index: 99; }
  #hud.show { display: flex; }
</style>
</head>
<body>
<svg width="0" height="0" style="position:absolute">
  <defs>
    <filter id="grain" x="0" y="0" width="100%" height="100%">
      <feTurbulence type="fractalNoise" baseFrequency="0.75" numOctaves="3" stitchTiles="stitch" />
      <feColorMatrix type="saturate" values="0" />
    </filter>
  </defs>
</svg>
<div id="stage">
  <div id="world">
    <div class="bg-layer"></div>
    <div class="scene" id="s1"><h1 class="line" id="t1">Motion without limits</h1></div>
  </div>
  <div class="vignette"></div>
  <div class="grain" id="grain"></div>
</div>
<div id="hud">
  <button id="btnPlay">Pause</button>
  <input id="scrub" type="range" min="0" max="1" step="0.001" value="0" style="width:260px" />
  <span id="clock">0.00s</span>
</div>
<script type="module">
import gsap from "https://esm.sh/[email protected]";

const W = 1920, H = 1080, DURATION = 6.0;
const stage = document.getElementById("stage");
const world = document.getElementById("world");
const grain = document.getElementById("grain");

function fitStage() {
  const s = Math.min(window.innerWidth / W, window.innerHeight / H);
  stage.style.setProperty("--fit", s);
}
window.addEventListener("resize", fitStage);
fitStage();

function splitChars(el) {
  const text = el.textContent.trim();
  el.innerHTML = text.split(/\s+/).map(word =>
    `<span class="wd">${[...word].map(c => `<span class="ch">${c}</span>`).join("")}</span>`
  ).join(" ");
  return el.querySelectorAll(".ch");
}

function blurTween(tl, target, at, fromV, toV, dur, axis = "x", ease = "power3.out") {
  const id = "mb_" + Math.random().toString(36).slice(2, 8);
  const defs = document.querySelector("svg defs");
  const filter = document.createElementNS("http://www.w3.org/2000/svg", "filter");
  filter.setAttribute("id", id);
  filter.setAttribute("x", "-70%"); filter.setAttribute("y", "-70%");
  filter.setAttribute("width", "240%"); filter.setAttribute("height", "240%");
  const blur = document.createElementNS("http://www.w3.org/2000/svg", "feGaussianBlur");
  blur.setAttribute("stdDeviation", "0 0");
  filter.appendChild(blur);
  defs.appendChild(filter);
  const proxy = { v: fromV };
  tl.set(target, { filter: `url(#${id})` }, at);
  tl.to(proxy, {
    v: toV, duration: dur, ease,
    onUpdate() {
      const a = proxy.v.toFixed(2), b = (proxy.v * 0.08).toFixed(2);
      blur.setAttribute("stdDeviation", axis === "x" ? `${a} ${b}` : `${b} ${a}`);
    },
    onComplete() { target.style.filter = "none"; }
  }, at);
}

const CAM = { s: 1, fx: W / 2, fy: H / 2 };
function applyCam(t) {
  const breathe = CAM.s * (1 + 0.012 * Math.sin(t * 0.9));
  const tx = W / 2 - CAM.fx * breathe;
  const ty = H / 2 - CAM.fy * breathe;
  world.style.transform = `translate(${tx}px, ${ty}px) scale(${breathe})`;
  const k = Math.floor(t * 12);
  grain.style.transform = `translate(${(k * 37 % 60) - 30}px, ${(k * 61 % 60) - 30}px)`;
}

const tl = gsap.timeline({
  paused: true,
  onUpdate() { applyCam(tl.time()); }
});

await document.fonts.ready;
const t1 = document.getElementById("t1");
const chars1 = splitChars(t1);
tl.set(t1, { opacity: 1 }, 0.2);
tl.fromTo(chars1,
  { opacity: 0, yPercent: 65, scaleY: 1.4 },
  { opacity: 1, yPercent: 0, scaleY: 1, duration: 0.85, ease: "power4.out", stagger: 0.022, immediateRender: false },
  0.2
);
blurTween(tl, t1, 0.2, 28, 0, 0.85, "y", "power4.out");
tl.to(CAM, { s: 1.08, duration: 3.0, ease: "sine.inOut" }, 0.2);
tl.to(chars1, { opacity: 0, yPercent: -65, duration: 0.42, ease: "power2.in", stagger: 0.01 }, 4.6);
blurTween(tl, t1, 4.6, 0, 42, 0.48, "y", "power2.in");
tl.to({}, { duration: 0.01 }, DURATION);

const params = new URLSearchParams(location.search);
const isClean = params.get("clean") === "1";
const isDebug = params.get("debug") === "1";
const hud = document.getElementById("hud");
const scrub = document.getElementById("scrub");
const clock = document.getElementById("clock");
const btnPlay = document.getElementById("btnPlay");

if (isDebug) hud.classList.add("show");

function seek(t) {
  const clamped = Math.max(0, Math.min(DURATION, t));
  tl.pause(clamped, false);
  gsap.ticker.tick();
  applyCam(clamped);
  scrub.value = clamped / DURATION;
  clock.textContent = `${clamped.toFixed(2)}s`;
}

tl.eventCallback("onUpdate", () => {
  const t = tl.time();
  applyCam(t);
  scrub.value = t / DURATION;
  clock.textContent = `${t.toFixed(2)}s`;
});
tl.eventCallback("onComplete", () => { if (!isClean) tl.restart(); });

btnPlay.onclick = () => {
  if (tl.paused()) { tl.play(); btnPlay.textContent = "Pause"; }
  else { tl.pause(); btnPlay.textContent = "Play"; }
};
scrub.oninput = () => seek(parseFloat(scrub.value) * DURATION);
window.addEventListener("keydown", e => {
  if (e.code === "Space") { e.preventDefault(); btnPlay.click(); }
  if (e.code === "KeyR") { tl.restart(); }
});

window.OPENER = { tl, DURATION, seek };
applyCam(0);
if (!isClean) tl.play(0);
</script>
</body>
</html>

Paso 3: Aplicar Reglas de Diseño de Movimiento Anti-PPT

Aplica estas reglas de artesanía en cada escena para que el resultado se lea como diseño de movimiento en lugar de una presentación de diapositivas animada:

  1. Una Declaración por Tiempo Fuerte: Mantén los titulares de 2 a 5 palabras en minúscula (peso 500-600). Nunca acumules un subtítulo + título de 2 líneas + cuerpo de 2 líneas + 4 insignias de iconos en una cuadrícula estática.
  2. Temporización Asimétrica: Entra con una desaceleración decisiva (0.7-1.1s, power4.out o back.out(1.5)) y sale más rápido (0.35-0.5s, power2.in). Superpone los elementos salientes y entrantes por 0.15-0.3s para que la pantalla nunca se quede muerta.
  3. Desenfoque de Movimiento SVG Direccional: Usa feGaussianBlur con stdDeviation="x y" dividido alineado con la dirección del movimiento, expandiendo la región del filtro (x="-70%" y="-70%" width="240%" height="240%") para que las colas de desenfoque nunca se recorten. Elimina el filtro (filter: none) al completarse.
  4. Movimiento de Cámara Motivado: Combina una respiración sutil continua de la cámara (+/-1-3% deriva sinusoidal) con cambios de toma intencionados (plano general -> plano medio corto -> primer plano del elemento activo). Nunca panees una escena tan lejos que los bordes del fondo expongan barras negras vacías (proporciona 240px+ de sobreexploración en #world).
  5. Contraste Primero: Mantén el texto del título nítido (#fff con sombra de legibilidad suave, sin floración de neón pesada de múltiples capas en el texto). Reserva el resplandor y el brillo para objetos principales, aspectos destacados de la interfaz de usuario y acentos 3D.

Paso 4: Garantizar el 100% de Determinismo

Cada estado visual debe ser una función pura de t = tl.time() para que el desplazamiento (?debug=1) y el renderizado MP4 sin cabeza produzcan fotogramas idénticos:

  • Prohibido en el Bucle de Renderizado: Nunca uses Math.random(), Date.now(), performance.now(), setInterval o integración acumulativa (x += vx * dt).
  • Reemplazos Deterministas: Impulsa partículas, compensaciones de grano de película y oscilaciones usando constantes de fase con semilla y Math.sin(t * freq + seed) o proxies de distancia interpolados (flow).
  • Colocación Absoluta en la Línea de Tiempo: Programa tweens usando marcas de tiempo numéricas explícitas (tl.to(..., 2.40)) en lugar de desplazamientos relativos encadenados (+=0.3) para que ajustar un tiempo fuerte nunca rompa la sincronización de audio posterior.

Paso 5: Verificar Visualmente y Exportar (Cuando se Solicite)

  1. Inspección de Marca de Tiempo Clave: Abre index.html?debug=1 en un navegador (o captura capturas de pantalla a través de Playwright/Puppeteer usando window.OPENER.seek(t)) en cada pico de escena y punto medio de transición. Verifica que no haya recorte de texto, alto contraste entre el primer plano y el fondo, y superposiciones suaves.
  2. Canalización de Exportación MP4: Cuando el usuario solicite un archivo de video, renderiza fotogramas deterministas usando ?clean=1 y window.OPENER.seek(frame / fps) a 60 fps, luego codifica con FFmpeg:
ffmpeg -framerate 60 -i frames/f%05d.png -c:v libx264 -pix_fmt yuv420p -crf 14 -tune grain output.mp4

Problemas Comunes

  • FOUC Antes de la Carga de la Fuente: Dividir el texto en espacios .ch antes de que las fuentes web terminen de cargarse calcula mal los cuadros delimitadores de los glifos. Siempre usa await document.fonts.ready antes de ejecutar splitChars().
  • transformOrigin de GSAP en Grupos SVG: Animar rotation o scale en elementos <g> anidados desplaza su cuadro delimitador y desacopla las uniones. Controla las transformaciones SVG a través de un objeto proxy JS que escriba translate(x,y) rotate(r) scale(sx,sy) directamente en onUpdate.
  • CORS de Módulo en file://: <script type="module" src="..."> externo falla cuando se abre directamente desde el disco debido a las reglas CORS de origen null. Mantén CSS y JS en línea dentro de index.html mientras importas librerías desde CDN HTTPS.
  • Limitación de requestAnimationFrame en Pestaña Oculta: Los navegadores sin cabeza limitan rAF en pestañas en segundo plano. Siempre llama a gsap.ticker.tick() y applyCam(t) sincrónicamente dentro de window.OPENER.seek(t).

Skills relacionados