Communitygithub.com

html-motion-graphics

Erstelle kinoreife, deterministische Bewegtgrafiken als eigenständige `index.html` – unterstützt von GSAP, gerichtetem SVG-Bewegungsunschärfe, einer einheitlichen `#world`-Kamerasteuerung und optionalen Three.js-WebGL-Hintergründen.

Was ist html-motion-graphics?

Die **html-motion-graphics** Fähigkeit ermöglicht es, professionelle Bewegtgrafiken zu erstellen, die komplett in einer einzigen `index.html`-Datei ohne Server benötigen. Sie basiert auf GSAP für Animationen, einem `#world`-Kamerarig, das sowohl Hintergrund als auch Vordergrund synchron bewegt, und optionalem Three.js für WebGL-Hintergründe. Die Besonderheit liegt in der **Determinismusgarantie**: Jeder visuelle Zustand ist eine reine Funktion der Zeit, sodass Scrubbing (`?debug=1`) und framegenaues MP4-Rendering (z.B. für Headless-Export) immer identische Ergebnisse liefern. Die Fähigkeit unterstützt verschiedene Konzepte wie kontinuierliche Kamerafahrten, kinetische Typografie und interaktive UI-Touren. Sie ist speziell für Entwickler:innen und Agentur-Kreative konzipiert, die portable, hochwertige Motion-Designs für Promotion-Videos, Produkt-Launches oder Social-Media-Clips benötigen, und stellt strenge Gestaltungsregeln bereit, um langweilige Slideshow-Effekte zu vermeiden.

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

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

HTML Motion Graphics

Erstelle kinoreife, deterministische Bewegtgrafiken als eigenständige index.html-Datei ohne Server – unterstützt von GSAP, gerichtetem SVG-Bewegungsunschärfe, einer einheitlichen #world-Kamerasteuerung und optionalen Three.js-WebGL-Hintergründen.

Wann verwenden

  • Erstellung von Promotionsvideos, Produkt-Launch-Openern, Teasern oder Brand-Bumpern in index.html.
  • Aufbau kinetischer Typografie, SaaS-UI-Produkttouren oder multi-szenischer visueller Erklärfilme.
  • Umwandlung statischer, slide-artiger Web-Animationen in echtes kameragesteuertes Motion-Design.
  • Bereitstellung einer portablen index.html, die sich direkt per file:// öffnen lässt und framegenauer Scrubbing sowie Headless-MP4-Rendering unterstützt.

Kern-Workflow

Schritt 1: Kreatives Briefing und Konzept-Frame festlegen

Definiere vor dem Schreiben von Code einen knappen Stil-Brief, damit das Ergebnis nie wie eine generische Slideshow-Vorlage aussieht:

  1. Seitenverhältnis und Auflösung: Standardmäßig 1920x1080 (16:9) für Desktop/YouTube oder 1080x1920 (9:16) für Reels/TikTok/Shorts (Inhalt in einem .safe-Container wrappen, skaliert auf 0.78 mit transform-origin: 50% 45% für 9:16-Social-HUD-Freiraum).
  2. Konzept-Architektur: Wähle ein klares strukturelles Konzept basierend auf dem Thema, anstatt auf Hook -> 3 Icon-Kacheln -> CTA zurückzugreifen:
    • Durchgehende Welt / Kamerafahrt: Ein einheitlicher Raum, in dem die Kamera durch Szenen fährt, umkreist oder eintaucht.
    • Living UI Tour: Nachgebaute DOM/SVG-Produktkomponenten, bei denen ein Cursor/Tippen interagiert, Panels sich ausdehnen und Daten deterministisch animieren.
    • Problem zur Klarheit: Visuelles Chaos baut sich auf, dann fegt eine einzige Produktaktion den Rahmen sauber zum Markenzeichen.
    • Reine kinetische Typografie: Wörter treten aus verschiedenen Achsen ein, verändern Gewicht/Skalierung und setzen sich zum finalen Markenzeichen zusammen.
    • Bühnen-Cartoon / Durchgehende Aktion: Charakter oder Held bleibt aktiv, während Hintergründe, Instrumente und diegetische Labels sich um ihn herum ändern.
  3. Farb- und Oberflächensystem: Definiere CSS-Custom-Properties (--base, --ink, --accent, --accent-lit). Lasse niemals einen flachen einfarbigen Hintergrund stehen. Kombiniere mindestens zwei Ebenen (z.B. radialer Horizont-Glow + linearer Verlauf, weiche Mesh-Blobs oder WebGL-Nebel + treibender Staub + SVG-Filmkorn-Overlay).
  4. Audio- und Timing-Plan: Lege die Gesamtdauer (DURATION in Sekunden) fest und ordne jeden Beat absoluten Zeitstempeln in Sekunden zu.

Schritt 2: Eigenständige index.html-Architektur aufbauen

Liefere eine einzige in sich geschlossene index.html mit Inline-<style> und <script type="module">, das GSAP (und Three.js, falls 3D-Tiefe/Partikel benötigt werden) von https://esm.sh oder https://cdn.jsdelivr.net importiert.

  • Fest aufgelegte Bühne: Fixiere #stage auf 1920x1080 (oder 1080x1920) und skaliere proportional per CSS --fit, damit die Kompositionskoordinaten nie zwischen Monitoren verschieben.
  • Einheitliches #world-Kamerarig: Platziere <canvas> (falls WebGL), Hintergrundverläufe und #dom-Szenenebenen innerhalb von #world. Wenn die Kamera zoomt oder schwenkt, bewegen sich Hintergrund und Vordergrund gemeinsam. Lege Linsen-Overlays (Lichtlecks, Vignette, Filmkorn) außerhalb von #world.
  • CSS-first versteckte Zustände: Setze opacity: 0 auf animierte Elemente (.line, .ch, .hl-t, .obj) in CSS und verwende immediateRender: false bei GSAP-fromTo-Tweens, um erstes Frame-Flimmern zu vermeiden.
<!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>

Schritt 3: Anti-PPT Motion-Design-Regeln durchsetzen

Wende diese Gestaltungsregeln auf jede Szene an, sodass das Ergebnis wie Motion-Design und nicht wie eine animierte Slide-Show wirkt:

  1. Eine Aussage pro Beat: Halte Überschriften auf 2–5 Wörter in Satzschreibung (Gewicht 500–600). Stapele niemals Augenbrauen-Aufmacher + 2-zeiligen Titel + 2-zeiligen Body + 4 Icon-Badges in einem statischen Raster.
  2. Asymmetrisches Timing: Betrete mit entschlossener Verlangsamung (0.7-1.1s, power4.out oder back.out(1.5)) und verlasse schneller (0.35-0.5s, power2.in). Überlappe ausgehende und eingehende Elemente um 0.15-0.3s, damit der Bildschirm niemals tot wird.
  3. Gerichteter SVG-Bewegungsunschärfe: Verwende feGaussianBlur mit geteiltem stdDeviation="x y" ausgerichtet an der Bewegungsrichtung und erweitere den Filterbereich (x="-70%" y="-70%" width="240%" height="240%"), damit Unschärfeschweife nie abgeschnitten werden. Entferne den Filter (filter: none) nach Abschluss.
  4. Motivierte Kamerabewegung: Kombiniere kontinuierliche subtile Kameraatmung (+/-1-3% Sinus-Drift) mit gezielten Schusswechseln (Totale -> Halbnahe -> Nahaufnahme auf dem aktiven Element). Schwenke niemals eine Szene so weit, dass Hintergrundkanten leere schwarze Balken freilegen (stelle 240px+ Overscan auf #world bereit).
  5. Kontrast zuerst: Halte Titeltext knackig (#fff mit weichem Lesbarkeitsschatten, kein schwerer Multi-Layer-Neon-Glühen auf Text). Hebe Glühen und Glanz für Heldenobjekte, UI-Highlights und 3D-Akzente auf.

Schritt 4: 100% Determinismus garantieren

Jeder visuelle Zustand muss eine reine Funktion von t = tl.time() sein, sodass Scrubbing (?debug=1) und Headless-MP4-Rendering identische Bilder liefern:

  • In der Render-Schleife verboten: Verwende niemals Math.random(), Date.now(), performance.now(), setInterval oder kumulative Integration (x += vx * dt).
  • Deterministische Alternativen: Treibe Partikel, Filmkorn-Offsets und Oszillationen mit gesetzten Phasenkonstanten und Math.sin(t * freq + seed) oder getwenten Distanz-Proxys (flow) an.
  • Absolute Timeline-Platzierung: Platziere Tweens mit expliziten numerischen Zeitstempeln (tl.to(..., 2.40)) anstatt mit verketteten relativen Offsets (+=0.3), damit das Anpassen eines Beats niemals die nachgelagerte Audiosynchronisation unterbricht.

Schritt 5: Visuell verifizieren und exportieren (auf Anfrage)

  1. Schlüsselzeitstempel-Prüfung: Öffne index.html?debug=1 im Browser (oder erstelle Screenshots via Playwright/Puppeteer mit window.OPENER.seek(t)) an jedem Szenenhöhepunkt und Übergangs-Mittelpunkt. Überprüfe null Textabschneidung, hohen Vorder-/Hintergrund-Kontrast und glatte Überlappungen.
  2. MP4-Export-Pipeline: Wenn der Benutzer eine Videodatei anfordert, rendere deterministische Bilder mit ?clean=1 und window.OPENER.seek(frame / fps) bei 60fps, dann kodiere mit FFmpeg:
ffmpeg -framerate 60 -i frames/f%05d.png -c:v libx264 -pix_fmt yuv420p -crf 14 -tune grain output.mp4

Gotchas

  • FOUC vor Schriftart-Ladung: Das Aufteilen von Text in .ch-Spannen vor dem Laden von Webschriftarten führt zu falsch berechneten Glyphen-Bounding-Boxen. Warte immer await document.fonts.ready ab, bevor du splitChars() ausführst.
  • GSAP transformOrigin auf SVG-Gruppen: Das Animieren von rotation oder scale auf verschachtelten <g>-Elementen verschiebt deren Bounding-Box und löst Gelenke. Steuere SVG-Transformationen über ein JS-Proxy-Objekt, das translate(x,y) rotate(r) scale(sx,sy) direkt im onUpdate schreibt.
  • Modul-CORS auf file://: Externes <script type="module" src="..."> schlägt fehl, wenn es direkt von der Festplatte geöffnet wird, wegen CORS-Regeln mit null-Origin. Behalte CSS und JS inline innerhalb der index.html bei und importiere Bibliotheken von HTTPS-CNDs.
  • Throttling von requestAnimationFrame in versteckten Tabs: Headless-Browser drosseln rAF in Hintergrund-Tabs. Rufe immer gsap.ticker.tick() und applyCam(t) synchron innerhalb von window.OPENER.seek(t) auf.

Verwandte Skills