vid-motion-graphics
Erzeugt mehrszenige Motion Graphics als MP4. Rendert HTML/CSS-Animationen in einem headless Chromium über Playwright (Web Animations API Frame-Seeking), setzt PNG-Frames mit FFmpeg zusammen. Kein React, keine KI-APIs, kein Python — null neue Abhängigkeiten jenseits der graphic-gif-Familie.
Nur CDN-Schriftarten. Keine externen Bibliotheken im HTML.
Kritische Regeln (vor jeder Generierung lesen)
window.renderFrame(t)verwenden — kein CSS@keyframesfür Szenenübergänge. Das Zurücksetzen der CSS-AnimationcurrentTimewird von Chromium für Rückwärtssprünge stillschweigend ignoriert. Der renderFrame-Ansatz: Eine reine JS-Funktion berechnetopacity/transformdirekt aus Millisekunden. Playwright ruft sie einmal pro Frame auf. Deterministisch, konfliktfrei.- Kein
animation-delayauf irgendeinem Element. Wird mit renderFrame nicht benötigt. Erwischst du dich dabei,animation-delayzu schreiben, hör auf — du verwendest die falsche Architektur. window.__videoReady = truenur innerhalb vondocument.fonts.ready.then(...). Niemals synchron setzen — Schriftarten müssen geladen sein, bevor Playwright Frame 1 erfasst, sonst wird Text mit Fallback-Schriften gerendert.window.__stopPreview()verfügbar machen. Die rAF-Vorschauschleife des Browsers konkurriert mit Playwrights evaluate/screenshot-Aufrufen.capture-frames.mjsruft__stopPreview()vor der Frame-Schleife auf. Immer einfügen.t < startMs(nicht<=) in Szenengrenzenprüfungen verwenden.t <= 0bei Frame 0 macht Szene 1 schwarz. Die korrekte Absicherung istif (t < startMs || t >= endMs) return hidden.- Body = exakte Pixelabmessungen. Breite und Höhe sind ganze Zahlen (
1080px,1920px). Keine%,vw/vhoder responsive Einheiten. - Keine zwei Szenen gleichzeitig sichtbar (außer 10% Überlapp beim Eintritt). Alle Szenen
opacity: 0außerhalb ihres renderFrame-Fensters. - Übergänge verwenden nur
opacity. Keindisplay-Umschalten, keinvisibility— GPU-komponierte Deckkraft ist frame-perfekt. - HTML niemals im Chat ausgeben. In Datei speichern, nur Zusammenfassung zeigen.
- Titel gibt die Botschaft an, nicht das Thema. „3 Gründe, warum Q4 die Ziele übertroffen hat“ nicht „Q4 2024 Leistungsvideo“.
- Lies
references/scene-library.mdvor dem Generieren von HTML. Verwende die exakte HTML-Struktur und CSS-Klassennamen aus dieser Datei.
Schritt 1: Aufnahme
Erforderlich: content_brief
Optionale Parameter und Standardwerte:
| Parameter | Standard | Beschreibung |
|---|---|---|
| content_brief | — | Text, der beschreibt, was das Video kommuniziert (erforderlich) |
| scenes | auto | Anzahl der Szenen (1–6). Auto = aus der Vorgabe abgeleitet. |
| duration_per_scene | 3s | Dauer pro Szene in Sekunden (1–8s) |
| style | kinetic-dark | kinetic-dark / editorial-light / data-pulse / bold-type / minimal-clean |
| aspect_ratio | 1:1 | 1:1 (1080×1080) / 16:9 (1920×1080) / 9:16 (1080×1920) |
| fps | 30 | Bilder pro Sekunde (24, 30 oder 60) |
| music | none | Pfad zur Audiodatei für den Hintergrundtrack (mp3/m4a/wav) |
| source | — | Quellenangabe, die im Fuß der letzten Szene angezeigt wird |
Wenn content_brief fehlt, frage genau:
„Um das Video zu erstellen, benötige ich eine Inhaltsvorgabe – was soll das Video kommunizieren?
Beispiel: ‚Zeige 3 Gründe, warum der Q4-Umsatz um 85 % gestiegen ist: neue Unternehmensabschlüsse, geringere Abwanderung, Preiserhöhung. Verwende fette Zahlen. Stil: data-pulse.‘
Optional: Stil (Standard: kinetic-dark), Seitenverhältnis (Standard: 1:1), Sekunden pro Szene (Standard: 3s)"
Wenn content_brief vorhanden ist → fahre sofort mit Schritt 2 fort.
Schritt 2: Interne Architektur (dem Benutzer nie zeigen)
1. Vorgabe in Szenen parsen (max. 6):
- Szene 1 = Aufhänger oder Titel (immer)
- Szenen 2–N-1 = unterstützende Punkte, Metriken oder Story-Elemente
- Szene N = CTA oder abschließende Zusammenfassung (immer, wenn mehr als 1 Szene)
- Eine Kernidee pro Szene – wenn die Vorgabe 7+ Ideen hat, konsolidiere die schwächsten
2. references/scene-library.md lesen — Szenentyp für jede Szene wählen:
- Aufhänger/Eröffnung →
title-card - Einzelne Metrik →
stat-reveal - Liste von 2–4 Punkten →
bullet-list - Vorher vs. Nachher / zwei Werte →
split-screen - Erfahrungsbericht/Zitat →
quote-card - Finale / CTA →
cta-card
3. references/style-presets.md lesen — CSS-Tokens + Animationspersönlichkeit für den gewählten Stil laden.
4. Timing berechnen:
gesamtDauer = szenenAnzahl × dauerProSzene (Sekunden)
gesamtFrames = gesamtDauer × fps
Jede Szene belegt (100 / szenenAnzahl)% des gesamten @keyframes-Bereichs.
| Szene | Start % | Ende % |
|---|---|---|
| 1 | 0% | (100/N)% |
| 2 | (100/N)% | (200/N)% |
| … | … | … |
| N | ((N-1)×100/N)% | 100% |
Innerhalb jedes Szenenbereichs:
- Eintritt: erste 10 % des Szenenbereichs
- Halten: 10 % bis 85 % des Szenenbereichs
- Austritt: 85 % bis 100 % des Szenenbereichs
5. Pixelabmessungen bestimmen:
1:1→ B=1080, H=108016:9→ B=1920, H=10809:16→ B=1080, H=1920
Schritt 3: HTML-Generierung
Lies references/scene-library.md UND references/style-presets.md, bevor du Code schreibst.
Erforderliche HTML-Struktur:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
[CDN-Link für Schriftart aus der Stilvorlage]
<style>
:root {
[alle CSS-Tokens aus der Stilvorlage]
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: [B]px; height: [H]px;
overflow: hidden;
background: var(--bg);
font-family: var(--font-body);
position: relative;
}
.scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80px;
opacity: 0;
will-change: opacity, transform;
}
.scene-inner {
width: 100%;
max-width: 960px;
}
/* Szenentyp-CSS aus scene-library.md */
[hier Szenentyp-CSS einfügen]
</style>
</head>
<body>
<div class="scene scene-1">
<div class="scene-inner">
[Szenen-1-HTML aus scene-library.md-Vorlage]
</div>
</div>
[für jede Szene wiederholen]
<script>
window.__videoReady = false;
window.TOTAL_DURATION_MS = [gesamtDauer * 1000];
// ── Animations-Hilfsfunktionen ─────────────────────────────────────────────────────────
function lerp(a, b, p) { return a + (b - a) * p; }
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function easeOutCubic(t) { return 1 - Math.pow(1 - clamp(t, 0, 1), 3); }
function sceneState(t, startMs, endMs) {
if (t < startMs || t >= endMs) return { opacity: 0, ty: 0 };
const prog = (t - startMs) / (endMs - startMs);
if (prog < 0.10) {
const p = easeOutCubic(prog / 0.10);
return { opacity: p, ty: lerp(24, 0, p) };
}
if (prog < 0.85) return { opacity: 1, ty: 0 };
const p = (prog - 0.85) / 0.15;
return { opacity: 1 - p, ty: lerp(0, -12, p) };
}
function applySceneState(el, state) {
el.style.opacity = state.opacity;
el.style.transform = state.ty !== 0 ? `translateY(${state.ty.toFixed(2)}px)` : '';
}
// ── Haupt-Render-Funktion — von Playwright einmal pro Frame aufgerufen ────────────────
window.renderFrame = function(t) {
// Szene 1: 0ms – [D]ms
applySceneState(document.querySelector('.scene-1'), sceneState(t, 0, [D]));
// Szene 2: [D]ms – [2D]ms
applySceneState(document.querySelector('.scene-2'), sceneState(t, [D], [2D]));
// ... pro Szene wiederholen ...
};
// ── Vorschauschleife — von Playwright vor der Frame-Erfassung gestoppt ─────────────────
let __previewActive = false;
let __previewRafId = null;
window.__stopPreview = function() {
__previewActive = false;
if (__previewRafId !== null) { cancelAnimationFrame(__previewRafId); __previewRafId = null; }
};
document.fonts.ready.then(() => {
window.renderFrame(0);
window.__videoReady = true;
__previewActive = true;
const startTime = performance.now();
function previewTick() {
if (!__previewActive) return;
const elapsed = performance.now() - startTime;
if (elapsed < window.TOTAL_DURATION_MS) {
window.renderFrame(elapsed);
__previewRafId = requestAnimationFrame(previewTick);
} else {
window.renderFrame(window.TOTAL_DURATION_MS - 1);
__previewActive = false;
}
}
__previewRafId = requestAnimationFrame(previewTick);
});
</script>
</body>
</html>
Regeln zur Designqualität:
- Überschrift-Schriftgröße: niemals kleiner als 60px – Text muss in der mobilen Miniaturansicht lesbar sein
- Zahlen und Statistiken: immer das größte Element auf dem Bildschirm (120–200px)
- Enge Zeichenabstände bei Display-Schriftarten:
-0.02embis-0.04em - Eine Akzentfarbe pro Szene – Akzente nicht auf mehrere Elemente verteilen
- Dunkle Voreinstellungen: Trennlinien
rgba(255,255,255,0.10), niemals deckend - Helle Voreinstellungen: Trennlinien
rgba(0,0,0,0.10), niemals deckend transform-origin: center centerauf jedem Element, dastransformverwendet- Innenabstand innerhalb von
.scene: mindestens 80px – Text darf niemals die Darstellungsbereichsränder berühren
Schritt 4: Selbst-QA (jeden Fehler vor Schritt 5 beheben)
renderFrame-Korrektheit:
-
window.renderFrame(t)definiert – reine Funktion, keine Nebenwirkungen außerhalb von Stil-Schreibvorgängen - Szenengrenze verwendet
t < startMs(nichtt <= startMs) – vermeidet schwarzen Frame 0 - Keine Instanzen von
animation-delayoder@keyframesfür Szenenübergänge - Keine zwei Szenen haben überlappende
opacity: 1-Fenster (außer 10% Überlapp beim Eintritt) -
window.__stopPreview()verfügbar gemacht und die Vorschau-rAF-Schleife prüft__previewActive
Bereitschaftssignal:
-
window.__videoReady = falsevordocument.fonts.readydeklariert -
window.__videoReady = trueNUR innerhalb vondocument.fonts.ready.then(...)gesetzt -
window.renderFrame(0)innerhalb vondocument.fonts.ready.then(...)vor dem Setzen von__videoReady = trueaufgerufen
Layout:
-
html, bodyverwenden exakte Pixelabmessungen ([B]px,[H]px) - Keine
%,vw,vh,rem-Einheiten aufbody-Breite/-Höhe -
overflow: hiddenaufhtml, body - Alle Szenen
position: absolute; inset: 0
Design:
- Alle Farben aus den Token der Stilvorlage – keine freien Hex-Werte
- Alle Schriftarten aus der Stilvorlage – keine freien font-family-Strings
- Überschrift-Schriftgröße ≥ 60px
- Quelle in der letzten Szene, wenn der Parameter
sourceangegeben wurde
Schritt 5: Export
Slug aus dem Inhalt der Vorgabe ermitteln (Kebab-Schreibweise, ≤30 Zeichen):
mkdir -p chart/[slug]
HTML speichern: chart/[slug]/video.html
Browser-Vorschau:
open chart/[slug]/video.html
Export ausführen (ersetze [skill-root] mit dem Pfad zum Verzeichnis dieser Fähigkeit):
bash [skill-root]/scripts/export-video.sh \
chart/[slug]/video.html \
chart/[slug]/video.mp4 \
--duration [gesamtDauer] \
--fps [fps] \
--width [B] \
--height [H] \
[--music pfad/zu/audio.mp3]
Das Skript installiert Playwright beim ersten Start (~200 MB Chromium), erfasst alle Frames und setzt das MP4 mit FFmpeg zusammen.
Schritt 6: Ausgabe-Zusammenfassung
## Video: [Titel aus der Vorgabe]
Datum: [JJJJ-MM-TT] | Szenen: [N] | Stil: [stil] | Seitenverhältnis: [verhältnis]
Dauer: [N]s | FPS: [fps] | Frames: [N]
Dateien
Quelle: chart/[slug]/video.html
Ausgabe: chart/[slug]/video.mp4
Größe: [X] MB
Checkliste
- [ ] Alle Szenen erscheinen nacheinander ohne leere Frames
- [ ] Text in mobiler Miniaturansicht lesbar
- [ ] Szenenübergänge flüssig (keine Sprungschnitte)
- [ ] Letzte Szene enthält CTA oder Abschlussbotschaft
- [ ] Quellenangabe im letzten Frame vorhanden (falls angegeben)
Tipps zur Eingabeaufforderung (zeigen, wenn der Benutzer nach Anleitung fragt)
„Gib eine Inhaltsvorgabe – Aufzählungspunkte, was jede Szene sagen soll.“
„Benenne die Erkenntnis direkt. ‚3 Gründe, warum Q4 um 85 % gewachsen ist‘ gibt dem Video ein Rückgrat.“
„Erwähne den Stil, wenn du eine Präferenz hast: kinetic-dark (Standard), editorial-light, data-pulse, bold-type, minimal-clean.“
„Gib das Seitenverhältnis für die Plattform an: 1:1 für LinkedIn/Instagram-Stream, 9:16 für Stories/Reels, 16:9 für YouTube/Präsentationen.“
✅ Gut: „Erstelle ein 9-Sekunden-Video. Der Q4-Umsatz betrug 4,2 Mio. $ (85 % Wachstum). Treiber: Unternehmensabschlüsse, Abwanderung 1,2 %, Preiserhöhung. CTA: acme.com/q4. Stil: data-pulse. Seitenverhältnis: 1:1.“
❌ Schlecht: „mach ein video über unsere firma“