| name | vid-motion-graphics |
| description | Generates motion graphics videos (MP4) from a content brief. Multi-scene HTML/CSS animations rendered frame-by-frame in headless Chromium via Playwright, assembled with FFmpeg. 1080×1080 default, 16:9 (1920×1080) and 9:16 (1080×1920) supported. 5 style presets. Trigger when user says "create a video", "motion graphic", "animated video", "make a reel", "create an explainer", "animated infographic", or "short video". |
| compatibility | ["claude-code","gemini-cli","github-copilot"] |
| author | OpenDirectory |
| version | 1.0.0 |
vid-motion-graphics
Generates multi-scene motion graphics as MP4. Renders HTML/CSS animations in headless Chromium via Playwright (Web Animations API frame-seeking), assembles PNG frames with FFmpeg. No React, no AI APIs, no Python — zero new dependencies beyond the graphic-gif family.
CDN fonts only. No external libraries in HTML.
Critical Rules (read before every generation)
- Use
window.renderFrame(t) — no CSS @keyframes for scene transitions. CSS animation currentTime seeking is silently ignored for backward seeks in Chromium. The renderFrame approach: a pure JS function computes opacity/transform directly from milliseconds. Playwright calls it once per frame. Deterministic, race-free.
- No
animation-delay on ANY element. Not needed with renderFrame. If you catch yourself writing animation-delay, stop — you're using the wrong architecture.
window.__videoReady = true only inside document.fonts.ready.then(...). Never set synchronously — fonts must load before Playwright captures frame 1 or text renders with fallback fonts.
- Expose
window.__stopPreview(). The browser's rAF preview loop races with Playwright's evaluate/screenshot calls. capture-frames.mjs calls __stopPreview() before the frame loop. Always include it.
- Use
t < startMs (not <=) in scene boundary checks. t <= 0 at frame 0 makes scene 1 black. The correct guard is if (t < startMs || t >= endMs) return hidden.
- Body = exact pixel dimensions. Width and height are integers (
1080px, 1920px). No %, vw/vh, or responsive units.
- No two scenes visible simultaneously (except 10% enter overlap). All scenes
opacity: 0 outside their renderFrame window.
- Transitions use
opacity only. No display toggle, no visibility — GPU-composited opacity is frame-perfect.
- Never dump HTML in chat. Save to file, show summary only.
- Title states the message, not the topic. "3 Reasons Q4 Crushed Targets" not "Q4 2024 Performance Video".
- Read
references/scene-library.md before generating ANY HTML. Use exact HTML structure and CSS class names from that file.
Step 1: Intake
Required: content_brief
Optional parameters and defaults:
| Parameter | Default | Description |
|---|
| content_brief | — | Text describing what the video communicates (required) |
| scenes | auto | Number of scenes (1–6). Auto = derived from brief. |
| duration_per_scene | 3s | Duration per scene in seconds (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 | Frames per second (24, 30, or 60) |
| music | none | Path to audio file for background track (mp3/m4a/wav) |
| source | — | Source attribution shown in final frame footer |
If content_brief is missing, ask exactly:
"To create the video, I need a content brief — what should the video communicate?
Example: 'Show 3 reasons why Q4 revenue grew 85%: new enterprise deals, reduced churn, price increase. Use bold numbers. Style: data-pulse.'
Optional: style (default: kinetic-dark), aspect ratio (default: 1:1), seconds per scene (default: 3s)"
If content_brief is present → proceed to Step 2 immediately.
Step 2: Internal Architecture (never shown to user)
1. Parse brief into scenes (max 6):
- Scene 1 = hook or title (always)
- Scenes 2–N-1 = supporting points, metrics, or story beats
- Scene N = CTA or closing summary (always, if more than 1 scene)
- One key idea per scene — if brief has 7+ ideas, consolidate the weakest ones
2. Read references/scene-library.md — choose scene type for each scene:
- Hook/opening →
title-card
- Single metric →
stat-reveal
- List of 2–4 points →
bullet-list
- Before vs after / two values →
split-screen
- Testimonial / quote →
quote-card
- Final / CTA →
cta-card
3. Read references/style-presets.md — load CSS tokens + animation personality for chosen style.
4. Calculate timing:
totalDuration = sceneCount × duration_per_scene (seconds)
totalFrames = totalDuration × fps
Each scene occupies (100 / sceneCount)% of the total @keyframes range.
| Scene | Start % | End % |
|---|
| 1 | 0% | (100/N)% |
| 2 | (100/N)% | (200/N)% |
| … | … | … |
| N | ((N-1)×100/N)% | 100% |
Within each scene's range:
- Enter: first 10% of scene range
- Hold: 10% to 85% of scene range
- Exit: 85% to 100% of scene range
5. Determine pixel dimensions:
1:1 → W=1080, H=1080
16:9 → W=1920, H=1080
9:16 → W=1080, H=1920
Step 3: HTML Generation
Read references/scene-library.md AND references/style-presets.md before writing any code.
Required HTML structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
[font CDN link from style preset]
<style>
:root {
[all CSS tokens from style preset]
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: [W]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;
}
[scene 1 HTML from scene-library.md template]
[repeat for each scene]
Design quality rules:
- Headline font size: never smaller than 60px — text must read at mobile thumbnail size
- Numbers and stats: always the largest element on screen (120–200px)
- Tight letter-spacing on display type:
-0.02em to -0.04em
- One accent color per scene — don't scatter accent across multiple elements
- Dark presets: dividers
rgba(255,255,255,0.10), never solid
- Light presets: dividers
rgba(0,0,0,0.10), never solid
transform-origin: center center on every element that uses transform
- Padding inside
.scene: minimum 80px — never let text touch viewport edges
Step 4: Self-QA (fix every failure before Step 5)
renderFrame correctness:
Readiness signal:
Layout:
Design:
Step 5: Export
Determine slug from brief content (kebab-case, ≤30 chars):
mkdir -p chart/[slug]
Save HTML: chart/[slug]/video.html
Browser preview:
open chart/[slug]/video.html
Run export (replace [skill-root] with path to this skill's directory):
bash [skill-root]/scripts/export-video.sh \
chart/[slug]/video.html \
chart/[slug]/video.mp4 \
--duration [totalDuration] \
--fps [fps] \
--width [W] \
--height [H] \
[--music path/to/audio.mp3]
The script installs Playwright on first run (~200MB Chromium), captures all frames, then assembles MP4 with FFmpeg.
Step 6: Output Summary
## Video: [title from brief]
Date: [YYYY-MM-DD] | Scenes: [N] | Style: [style] | Aspect: [ratio]
Duration: [N]s | FPS: [fps] | Frames: [N]
Files
Source: chart/[slug]/video.html
Output: chart/[slug]/video.mp4
Size: [X] MB
Checklist
- [ ] All scenes appear in sequence with no blank frames
- [ ] Text legible at mobile thumbnail size
- [ ] Scene transitions smooth (no jump cuts)
- [ ] Final scene includes CTA or closing message
- [ ] Source attribution present in final frame (if provided)
Prompt Tips (show when user asks for guidance)
"Provide a content brief — bullet points of what each scene should say."
"Name the insight directly. '3 reasons Q4 grew 85%' gives the video a spine."
"Mention the style if you have a preference: kinetic-dark (default), editorial-light, data-pulse, bold-type, minimal-clean."
"Specify aspect ratio for the platform: 1:1 for LinkedIn/Instagram feed, 9:16 for Stories/Reels, 16:9 for YouTube/presentations."
✅ Good: "Create a 9-second video. Q4 revenue hit $4.2M (85% growth). Drivers: enterprise deals, churn 1.2%, price increase. CTA: acme.com/q4. Style: data-pulse. Aspect: 1:1."
❌ Bad: "make a video about our company"