FrameVideo
HTML is the source of truth for video. A composition is an HTML file with data-* attributes for timing, a GSAP timeline for animation, and CSS for appearance. The framework handles clip visibility, media playback, and timeline sync.
When To Use
Use this skill for any FrameVideo composition task. This is the largest skill (1100+ lines) — use the navigation below to find what you need quickly.
🚀 New to FrameVideo?
- Read "Quick Start" (line 46) — minimal working example
- Read "Core Concepts" (line 95) — data attributes, timing, timeline contract
- Try the example, then come back for specific features
🎯 Looking for something specific?
Layout & Structure:
- Safe areas & margins → "Layout & Safe Areas" (line 180)
- Multi-scene compositions → "Scene Transitions" (line 450) [MANDATORY for multi-scene]
- Composition architecture → "Composition Structure" (line 120)
Animation & Motion:
- Which animation library? → "Animation Adapter Routing" (line 28)
- GSAP patterns → Load
gsapskill (default choice) - Audio-reactive animation → "Audio Reactive Visuals" (line 620)
- Custom effects → Animation adapter skills (gsap, animejs, waapi, etc.)
Media & Assets:
- Video/audio playback → "Video & Audio" (line 140)
- Captions & subtitles → "Captions" (line 580)
- Parametrized compositions → "Variables" (line 160)
Workflow:
- AI-driven production → "AI Production Route" (line 200)
- Quality checks →
framevideo-visual-qaskill - CLI commands →
framevideo-cliskill
Do NOT use for:
- CLI commands (init, lint, preview, render) →
framevideo-cliskill - Asset preprocessing (TTS, transcribe, bg-removal) →
framevideo-mediaskill - Quality checks and validation →
framevideo-visual-qaskill - Chanjing digital humans →
chanjing-digital-humanskill
Animation Adapter Routing
Default to gsap for most FrameVideo composition animation. Use a specific adapter skill only when the content or user request calls for it:
| Use | Adapter skill |
|---|---|
| Scene choreography, text/card motion, staggered timelines, most scripted animation | gsap |
| User requests Anime.js, or porting compact Anime.js DOM/SVG examples | animejs |
| Simple finite CSS keyframes, shimmer, glow, masks, and non-sequenced decoration | css-animations |
Lightweight native element.animate() motion with no external library |
waapi |
| Existing Lottie/dotLottie assets from design tools | lottie |
| Deterministic 3D scenes, GLTF, WebGL, camera moves, shader plates | three |
| WebGPU/TypeGPU shaders, particles, liquid glass, compute pipelines | typegpu |
Shared rule for every adapter: render-critical animation must be deterministic and seekable. Do not use wall-clock time, infinite loops, or async registration for timelines/instances.
Quick Start
Minimal working composition:
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>My Video</title>
</head>
<body>
<div data-composition-id="main" data-width="1920" data-height="1080">
<div id="scene-1" class="clip" data-start="0" data-duration="5" data-track-index="1">
<div class="scene-content">
<h1>Hello FrameVideo</h1>
</div>
</div>
<style>
[data-composition-id="main"] { background: #000; color: #fff; }
.scene-content {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 120px 160px;
box-sizing: border-box;
}
h1 { font-size: 120px; }
</style>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
tl.from("h1", { opacity: 0, y: 40, duration: 0.8, ease: "power3.out" }, 0.3);
window.__timelines["main"] = tl;
</script>
</div>
</body>
</html>
Next: Validate with npx framevideo lint, preview with npx framevideo preview.
Section Index
This skill has 1100+ lines organized into the following major sections. Jump to the section you need:
🎬 Core Concepts (lines 95-200) — Read first if new to FrameVideo
- Data Attributes & Timing
- Composition Structure
- Timeline Contract
- Video & Audio
- Variables (Parametrized Compositions)
🎨 Layout & Design (lines 180-300)
- Safe Areas & Margins
- Typography & Readability
- Color & Contrast
- Responsive Layouts
✨ Animation & Motion (lines 300-450)
- Animation Adapter Routing (see above)
- GSAP Integration (or load
gsapskill for details) - Timing & Easing
- Stagger Patterns
🎞️ Scene Transitions (lines 450-580) — MANDATORY for multi-scene videos
- Crossfades
- Wipes & Reveals
- Shader Transitions
- Scene Architecture
📝 Captions & Subtitles (lines 580-650)
- Caption Timing
- Styling & Positioning
- Transcript Integration
🎵 Audio-Reactive Visuals (lines 620-720)
- Beat Detection
- Amplitude-Based Animation
- Music Synchronization
🔧 Advanced Patterns (lines 720-900)
- HTML-in-Canvas Effects
- Sub-Compositions
- Dynamic Content
- Performance Optimization
📚 References (lines 900-1100)
- Examples Gallery
- Troubleshooting
- Best Practices
- API Reference
Note: Line numbers are approximate guides. Use your Read tool's offset parameter to jump to sections, or read the full file if you need comprehensive context.
Core Concepts
- Video & Audio Elements
- Variables (Parametrized Compositions)
Layout & Safe Areas (expand when positioning elements):
- Layout Before Animation Principle
- Container Patterns
- Safe Area Rules
- Text Handling
- Common Layout Patterns
Animation & Motion (expand when adding motion):
- Animation Guardrails
- Entrance/Exit Patterns
- Stagger & Sequencing
- Scene Rhythm Templates
- Animation Conflicts
Scene Transitions (mandatory for multi-scene videos):
- 4 Non-Negotiable Rules
- Transition Implementation
Advanced Features (as needed):
- Captions & Subtitles
- Audio-Reactive Animation
- CSS Marker Highlighting
- Shader Transitions
References (deep dives):
- See "References" section at end for 15+ detailed guides
Core Concepts
Data Attributes & Timing
Every clip requires these attributes:
| Attribute | Required | Values |
|---|---|---|
id |
Yes | Unique identifier |
data-start |
Yes | Seconds or clip ID reference ("el-1", "intro + 2") |
data-duration |
Yes* | Seconds. *Optional for video/audio (uses media length) |
data-track-index |
Yes | Integer. Same-track clips cannot overlap |
Optional:
data-media-start- Trim offset into source (seconds)data-volume- 0-1 (audio only, default 1)
Important: data-track-index does NOT control visual layering. Use CSS z-index.
Composition Structure
Root element with required attributes:
<div data-composition-id="main" data-width="1920" data-height="1080">
<!-- content -->
</div>
Standalone compositions (main index.html): Put root <div> directly in <body>. Do NOT use <template>.
Sub-compositions (loaded via data-composition-src): MUST use <template> wrapper:
<template id="my-comp-template">
<div data-composition-id="my-comp" data-width="1920" data-height="1080">
<!-- content -->
</div>
</template>
Timeline Contract
5 mandatory rules:
- Create paused:
const tl = gsap.timeline({ paused: true }); - Register timeline:
window.__timelines["main"] = tl;(key must matchdata-composition-id) - Synchronous construction: Never build timelines inside
async,setTimeout, or Promises - Duration from data attribute: Use
data-duration, not GSAP timeline length - Framework auto-nests: Don't manually add sub-composition timelines
Video & Audio
Video must be muted:
<video
id="el-v"
data-start="0"
data-duration="30"
data-track-index="0"
src="video.mp4"
muted
playsinline
></video>
Audio track separate:
<audio
id="el-a"
data-start="0"
data-duration="30"
data-track-index="2"
src="video.mp4"
data-volume="1"
></audio>
Use same source file. Framework controls playback - never call .play().
Variables (Parametrized Compositions)
Three-step pattern:
- Declare on
<html>root:
<html data-composition-variables='[
{"id":"title","type":"string","label":"Title","default":"Hello"}
]'>
- Read in script:
const { title } = window.__framevideo.getVariables();
document.getElementById("hero").textContent = title;
- Override at render:
npx framevideo render --variables '{"title":"Q4 Report"}'
Variable types: string, number, color, boolean, enum.
AI Production Route
When the user asks to go from an idea, script, plot, storyboard, reference image, or rough video concept to a generated video, invoke framevideo-ai-production as the front half of the workflow. That skill turns creative material into Clip, Shot, and Chanjing AIGC plans.
For real Chanjing AI image/video generation:
- Use
framevideo-ai-productionto produce Shot-level Chanjing AIGC plans. - Use
chanjing-digital-human/references/ai-creation.mdfor Chanjing AI Creation model discovery, idempotent submission, short sync polling, download, and local asset paths. - Only compose with local assets under
assets/ai-creation/images/orassets/ai-creation/videos/. - Return here to build the FrameVideo HTML composition, audio, captions, transitions, QA, preview, and render.
Do not directly insert remote Chanjing output URLs into composition HTML.
Approach
Discovery (exploratory requests only)
For open-ended requests ("make me a product launch video", "create something for our brand") where the user hasn't committed to a direction, understand intent before picking colors:
- Audience — who watches this? Developers? Executives? General consumers?
- Platform — where does it play? Social (15s), website hero, product demo, internal?
- Priority — what matters most? Motion quality? Content accuracy? Brand fidelity? Speed?
- Variations — does the user want options, or a single best shot?
For specific requests ("add a title card", "fix the timing on scene 3"), skip discovery.
For exploratory requests, consider offering 2-3 variations that differ meaningfully — not just color swaps, but different pacing, energy levels, or structural approaches. One safe/expected, one ambitious. Don't mandate this — it's a tool available when appropriate.
Step 1: Design system
If a design spec exists in the project, read it first. Look in precedence order: frame.md → design.md → DESIGN.md (design.md and DESIGN.md are different files on Linux — check both casings; frame.md is always lowercase, no FRAME.md variant). frame.md is the preferred spec for video/framevideo projects and wins if more than one exists; it uses the same format as design.md. It's the source of truth for brand colors, fonts, and constraints. Use its exact values — don't invent colors or substitute fonts. Any format works (YAML frontmatter, prose, tables — just extract the values).
If it names fonts you can't find locally (no fonts/ directory with .woff2 files, not a built-in font), warn the user before writing HTML: "the spec specifies [font name] but no font files found. Please add .woff2 files to fonts/ or I'll fall back to [closest built-in alternative]."
If no frame.md or design.md exists, offer the user a choice:
- User named a style or mood? → Read visual-styles.md for the 8 named presets. Pick the closest match.
- Want to browse options visually? → Run the design picker: read references/design-picker.md for the full workflow. This serves a visual picker page. The user configures mood, palette, typography, and motion in the browser, then copies the generated design.md and pastes it back into the conversation.
- Want to skip and go fast? → Ask: mood, light or dark, any brand colors/fonts? Then pick a palette from house-style.md.
The design spec defines the brand. It does not define video composition rules. Those come from references/video-composition.md and house-style.md. Use brand colors at video-appropriate scale — not at web-UI opacity.
Step 2: Prompt expansion
Always run on every composition (except single-scene pieces and trivial edits). This step grounds the user's intent against the design spec (frame.md or design.md) and house-style.md and produces a consistent intermediate that every downstream agent reads the same way.
Read references/prompt-expansion.md for the full process and output format.
Step 3: Plan
Before writing HTML, think at a high level:
- What — what should the viewer experience? Identify the narrative arc, key moments, and emotional beats.
- Structure — how many compositions, which are sub-compositions vs inline, what tracks carry what (video, audio, overlays, captions).
- Rhythm — declare your scene rhythm before implementing. Which scenes are quick hits, which are holds, where do shaders land, where does energy peak. Name the pattern: fast-fast-SLOW-fast-SHADER-hold. Read references/beat-direction.md for rhythm templates.
- Timing — which clips drive the duration, where do transitions land, what's the pacing.
- Layout — build the end-state first. See "Layout Before Animation" below.
- Animate — then add motion using the rules below.
Build what was asked. A request for "a title card" is not a request for "a title card + 3 supporting scenes + ambient music + captions." Every scene, every element, every tween should earn its place. If additional scenes or elements would genuinely improve the piece, propose them — don't add them.
For small edits (fix a color, adjust timing, add one element), skip straight to the rules.
Visual QA Skill
Use the framevideo-visual-qa skill when creating a new composition, making substantial layout/caption/asset/layering changes, fixing visual issues, or before declaring a composition visually ready. That skill owns reusable video QA rules for safe areas, text/background contrast, text overflow, unintentional overlap, and motion collisions. Keep project-specific brand identity in frame.md, design.md, or DESIGN.md; keep general visual QA method in the skill.
Layout Before Animation Principle
Build the end state first. Position every element where it should be at its most visible moment - fully entered, correctly placed, not yet exiting. Write this as static HTML+CSS first. No GSAP yet.
Why: If you position elements at their animated start state (offscreen, scaled to 0, opacity 0) and tween them to where you think they should land, you're guessing. Overlaps are invisible until render. By building the end state first, you see and fix layout problems before adding motion.
The process:
- Identify the hero frame - the moment when most elements are simultaneously visible
- Write static CSS for that frame
- Add entrances with
gsap.from()- animate FROM offscreen/invisible TO the CSS position - Add exits with
gsap.to()- animate TO offscreen/invisible FROM the CSS position
Example:
/* Step 1-2: Build the readable end state */
.scene-content {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100%;
padding: 120px 160px;
gap: 24px;
box-sizing: border-box;
}
.title { font-size: 120px; }
.subtitle { font-size: 42px; }
// Step 3: Animate INTO those positions
tl.from(".title", { y: 60, opacity: 0, duration: 0.6, ease: "power3.out" }, 0);
tl.from(".subtitle", { y: 40, opacity: 0, duration: 0.5, ease: "power3.out" }, 0.2);
// Step 4: Animate OUT from those positions
tl.to(".title", { y: -40, opacity: 0, duration: 0.4, ease: "power2.in" }, 3);
Container Patterns
✅ Full-Scene Container (Recommended):
.scene-content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
padding: 120px 160px; /* Safe area */
gap: 24px;
box-sizing: border-box;
}
❌ Do NOT use absolute positioning for content:
/* WRONG - breaks on long text */
.scene-content {
position: absolute;
top: 200px;
left: 160px;
width: 1920px;
height: 1080px;
}
Reserve position: absolute for decorative elements only (particles, glows, background shapes).
Safe Area Rules
Keep key information inside the safe area:
- Horizontal:
5% from left/right edges (96px on 1920px width) - Vertical:
5% from top/bottom edges (54px on 1080px height) - Bottom (captions/CTA):
10% from bottom (108px)
What must stay safe:
- Critical text, headlines, body copy
- Logo (unless intentionally edge-anchored)
- CTA buttons and links
- Captions and subtitles
- Presenter faces
- Product UI screenshots
- Legal/price copy
What can extend beyond:
- Full-bleed backgrounds
- Decorative glows and particles
- Grain and texture overlays
- Transition effects
Mark intentional overflow: <div class="grain" data-layout-allow-overflow>
Text Handling
Natural wrapping:
.headline {
max-width: 1200px; /* Wraps at this width */
font-size: 96px;
line-height: 1.1;
}
Dynamic fitting:
const { title } = window.__framevideo.getVariables();
const fontSize = window.__framevideo.fitTextFontSize(title, {
maxWidth: 1200,
fontFamily: 'Inter',
fontWeight: 700
});
document.getElementById('title').style.fontSize = fontSize + 'px';
Avoid fixed-size text containers - use elastic containers with padding instead.
Common Layout Patterns
Centered Title Card:
<div class="scene-content">
<h1>Main Message</h1>
<p class="subtitle">Supporting detail</p>
</div>
<style>
.scene-content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
width: 100%;
height: 100%;
padding: 120px 160px;
gap: 32px;
box-sizing: border-box;
}
</style>
Split Layout (Text + Visual):
<div class="scene-content">
<div class="text-side">
<h2>Product Feature</h2>
<p>Explanation</p>
</div>
<div class="visual-side">
<img src="assets/product.png" />
</div>
</div>
<style>
.scene-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 80px;
align-items: center;
width: 100%;
height: 100%;
padding: 120px 160px;
box-sizing: border-box;
}
</style>
Animation Guardrails
Timing offsets - Never start first animation at t=0:
// ❌ Wrong
tl.from(".title", { opacity: 0 }, 0);
// ✅ Right
tl.from(".title", { opacity: 0 }, 0.2); // 0.1-0.3s offset
Vary easing curves - Use at least 3 different eases per scene:
tl.from("#el1", { opacity: 0, ease: "power3.out" }, 0.2);
tl.from("#el2", { opacity: 0, ease: "expo.out" }, 0.4);
tl.from("#el3", { opacity: 0, ease: "back.out(1.4)" }, 0.6);
Common eases: power1.out, power2.out, power3.out, expo.out, back.out(1.7), elastic.out(1, 0.3)
Don't repeat entrance patterns - Each element should have unique direction or style:
// ❌ Wrong - everything from top
tl.from("#title", { y: 60, opacity: 0 }, 0.2);
tl.from("#subtitle", { y: 60, opacity: 0 }, 0.5);
// ✅ Right - varied directions
tl.from("#title", { y: 60, opacity: 0 }, 0.2);
tl.from("#subtitle", { x: -30, opacity: 0 }, 0.5);
tl.from("#cta", { scale: 0.8, opacity: 0 }, 0.8);
Performance properties - Prefer GPU-accelerated:
- ✅ Fast:
opacity,x,y,scale,rotation - ❌ Slow:
width,height,top,left,margin,padding,font-size
Common Animation Patterns
Stagger entrances:
// Stagger by time
tl.from(".item", {
y: 30,
opacity: 0,
duration: 0.5,
stagger: 0.1,
ease: "power2.out"
}, 0.5);
// Stagger from center
tl.from(".grid-item", {
scale: 0.8,
opacity: 0,
duration: 0.4,
stagger: { amount: 0.6, from: "center" },
ease: "back.out(1.4)"
}, 1.0);
Sequence vs Overlap:
// Sequence - one after another
tl.from("#el1", { opacity: 0, duration: 0.5 }, 0);
tl.from("#el2", { opacity: 0, duration: 0.5 }, ">"); // After previous ends
// Overlap - start before previous ends
tl.from("#el1", { opacity: 0, duration: 0.8 }, 0);
tl.from("#el2", { opacity: 0, duration: 0.5 }, "<0.4"); // 0.4s after el1 starts
Label-based sequencing:
tl.addLabel("intro", 0);
tl.from("#title", { opacity: 0 }, "intro");
tl.from("#subtitle", { opacity: 0 }, "intro+=0.5");
tl.addLabel("content", 2.5);
tl.from("#feature1", { x: -40, opacity: 0 }, "content");
Scene Rhythm Templates
Declare rhythm before implementing:
- Fast-Fast-SLOW: Quick intro beats → hold for message (2s, 3s, 5s)
- Build-PEAK-Resolve: Escalating energy → climax → settle (3s, 2s, 4s)
- Even Pulse: Consistent rhythm (all beats 3-4s, good for instructional)
See references/beat-direction.md for more templates.
Animation Conflicts
Never animate the same property on the same element from multiple timelines:
// ❌ Wrong - both animate opacity
timeline1.to("#el", { opacity: 0.5 });
timeline2.to("#el", { opacity: 1 }); // Conflict!
// ✅ Right - separate properties
timeline1.to("#el", { opacity: 0.5 });
timeline2.to("#el", { x: 100 }); // No conflict
Scene Transitions (Non-Negotiable)
Every multi-scene composition MUST follow these 4 rules:
Rule 1: Always Use Transitions
No jump cuts between scenes. Every scene change needs a transition.
Rule 2: Always Use Entrance Animations
Every element in every scene animates IN via gsap.from(). No element may appear fully-formed.
Rule 3: Never Use Exit Animations (Except Final Scene)
Do NOT animate elements out before a transition. The transition IS the exit. The outgoing scene's content MUST be fully visible when the transition starts.
Rule 4: Final Scene Only
The last scene is the ONLY scene where gsap.to(..., { opacity: 0 }) or exit animations are allowed.
Example:
// Scene 1 (0-7s)
// ✅ Entrance animations only
tl.from("#s1-title", { y: 50, opacity: 0, duration: 0.7, ease: "power3.out" }, 0.3);
tl.from("#s1-subtitle", { y: 30, opacity: 0, duration: 0.5, ease: "power2.out" }, 0.6);
// ❌ NO exit animations - transition handles it
// Transition at 7s (see references/transitions.md)
// Scene 2 (8-15s)
// ✅ Entrance animations
tl.from("#s2-heading", { x: -40, opacity: 0, duration: 0.6, ease: "expo.out" }, 8.0);
// Final scene (15-20s)
// ✅ OK to fade out on final scene only
tl.from("#s3-cta", { scale: 0.9, opacity: 0, duration: 0.5 }, 15.2);
tl.to("#s3-cta", { opacity: 0, duration: 0.5, ease: "power2.in" }, 19.5);
See references/transitions.md for transition implementation.
Layout Before Animation
.scene-content {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100%;
padding: 120px 160px;
gap: 24px;
box-sizing: border-box;
}
.title {
font-size: 120px;
}
.subtitle {
font-size: 42px;
}
/* Container fills any scene size (1920x1080, 1080x1920, etc).
Padding positions content. Flex + gap handles spacing. */
WRONG — hardcoded dimensions and absolute positioning:
.scene-content {
position: absolute;
top: 200px;
left: 160px;
width: 1920px;
height: 1080px;
display: flex; /* ... */
}
// Step 3: Animate INTO those positions
tl.from(".title", { y: 60, opacity: 0, duration: 0.6, ease: "power3.out" }, 0);
tl.from(".subtitle", { y: 40, opacity: 0, duration: 0.5, ease: "power3.out" }, 0.2);
tl.from(".logo", { scale: 0.8, opacity: 0, duration: 0.4, ease: "power2.out" }, 0.3);
// Step 4: Animate OUT from those positions
tl.to(".title", { y: -40, opacity: 0, duration: 0.4, ease: "power2.in" }, 3);
tl.to(".subtitle", { y: -30, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.1);
tl.to(".logo", { scale: 0.9, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.2);
When elements share space across time
If element A exits before element B enters in the same area, both should have correct CSS positions for their respective hero frames. The timeline ordering guarantees they never visually coexist — but if you skip the layout step, you won't catch the case where they accidentally overlap due to a timing error.
What counts as intentional overlap
Layered effects (glow behind text, shadow elements, background patterns) and z-stacked designs (card stacks, depth layers) are intentional. The layout step is about catching unintentional overlap — two headlines landing on top of each other, a stat covering a label, content bleeding off-frame.
Data Attributes
All Clips
| Attribute | Required | Values |
|---|---|---|
id |
Yes | Unique identifier |
data-start |
Yes | Seconds or clip ID reference ("el-1", "intro + 2") |
data-duration |
Required for img/div/compositions | Seconds. Video/audio defaults to media duration. |
data-track-index |
Yes | Integer. Same-track clips cannot overlap. |
data-media-start |
No | Trim offset into source (seconds) |
data-volume |
No | 0-1 (default 1) |
data-track-index does not affect visual layering — use CSS z-index.
Composition Clips
| Attribute | Required | Values |
|---|---|---|
data-composition-id |
Yes | Unique composition ID |
data-start |
Yes | Start time (root composition: use "0") |
data-duration |
Yes | Takes precedence over GSAP timeline duration |
data-width / data-height |
Yes | Pixel dimensions (1920x1080 or 1080x1920) |
data-composition-src |
No | Path to external HTML file |
data-variable-values |
No | JSON object of per-instance variable overrides on a sub-comp host |
On the root <html> element:
| Attribute | Required | Values |
|---|---|---|
data-composition-variables |
No | JSON array of declared variables (id/type/label/default) — drives Studio editing UI and provides defaults for getVariables() |
Composition Structure
Sub-compositions loaded via data-composition-src use a <template> wrapper. Standalone compositions (the main index.html) do NOT use <template> — they put the data-composition-id div directly in <body>. Using <template> on a standalone file hides all content from the browser and breaks rendering.
Sub-composition structure:
<template id="my-comp-template">
<div data-composition-id="my-comp" data-width="1920" data-height="1080">
<!-- content -->
<style>
[data-composition-id="my-comp"] {
/* scoped styles */
}
</style>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// tweens...
window.__timelines["my-comp"] = tl;
</script>
</div>
</template>
Load in root: <div id="el-1" data-composition-id="my-comp" data-composition-src="compositions/my-comp.html" data-start="0" data-duration="10" data-track-index="1"></div>
Variables (Parametrized Compositions)
Render the same composition with different content — title, theme color, prices, captions — without editing the source HTML.
Three-step pattern:
- Declare variables on the composition's
<html>root withdata-composition-variables. Each entry needsid,type(one ofstring,number,color,boolean,enum),label, anddefault. Enum entries also needoptions: [{value, label}, ...]. - Read the resolved values inside the composition's script with
window.__framevideo.getVariables(). Returns the merged result of declared defaults + per-instance overrides + CLI overrides. - Override at render time with
npx framevideo render --variables '{...}'(top-level) or withdata-variable-values='{...}'on the host element (per-instance for sub-comps).
<!doctype html>
<html
data-composition-variables='[
{"id":"title","type":"string","label":"Title","default":"Hello"},
{"id":"theme","type":"enum","label":"Theme","default":"light","options":[
{"value":"light","label":"Light"},
{"value":"dark","label":"Dark"}
]}
]'
>
<body>
<div data-composition-id="root" data-width="1920" data-height="1080">
<h1 id="hero" class="clip" data-start="0" data-duration="3"></h1>
<script>
const { title, theme } = window.__framevideo.getVariables();
document.getElementById("hero").textContent = title;
document.body.dataset.theme = theme;
</script>
</div>
</body>
</html>
# Dev preview uses declared defaults
npx framevideo preview
# Render with overrides
npx framevideo render --variables '{"title":"Q4 Report","theme":"dark"}' --output q4.mp4
# Or from a JSON file
npx framevideo render --variables-file ./vars.json
Sub-composition per-instance values: the same getVariables() works inside sub-comps loaded via data-composition-src. Each host element passes its own values:
<div
data-composition-id="card-pro"
data-composition-src="compositions/card.html"
data-variable-values='{"title":"Pro","price":"$29"}'
></div>
<div
data-composition-id="card-enterprise"
data-composition-src="compositions/card.html"
data-variable-values='{"title":"Enterprise","price":"Custom"}'
></div>
The runtime layers each host's data-variable-values over the sub-comp's declared defaults on a per-instance basis, so the same source can be embedded multiple times with different content.
Rules of thumb:
- Always provide a sensible
defaultfor every declared variable. Dev preview uses defaults — without them, the composition won't render correctly until--variablesis provided. - Read variables once at the top of the script (
const { title } = ...), not inside frame loops or event handlers —getVariables()allocates a fresh object per call. - Use
--strict-variablesin CI to fail fast on undeclared keys or type mismatches. - Variable types are validated at render time.
string,number,boolean, andcolor(hex string) checktypeof;enumchecks the value is in the declaredoptions.
Video and Audio
Video must be muted playsinline. Audio is always a separate <audio> element:
<video
id="el-v"
data-start="0"
data-duration="30"
data-track-index="0"
src="video.mp4"
muted
playsinline
></video>
<audio
id="el-a"
data-start="0"
data-duration="30"
data-track-index="2"
src="video.mp4"
data-volume="1"
></audio>
For background music from the Chanjing platform, use the CLI to download a local asset first:
npx framevideo chanjing music list --compact
npx framevideo chanjing music download --id <music-id> --chorus --duration 10 --json
Use the returned htmlSnippet or author the same shape manually. Keep BGM local under assets/music/; do not reference remote Chanjing/OSS URLs directly in composition HTML. Suggested BGM volume is 0.10-0.22; use data-volume="0.12" when speech or digital-human narration is present.
For sound effects from the Chanjing platform, download the SFX asset first and place it at the event time:
npx framevideo chanjing sound-effect list --compact
npx framevideo chanjing sfx download --id <effect-id> --volume 0.8 --json
Keep SFX local under assets/sfx/. Use short, event-specific <audio> clips with data-start set to the exact cue time, data-track-index="30" or higher, and data-volume="0.6-1" depending on the mix.
Timeline Contract
- All timelines start
{ paused: true }— the player controls playback - Register every timeline:
window.__timelines["<composition-id>"] = tl - Framework auto-nests sub-timelines — do NOT manually add them
- Duration comes from
data-duration, not from GSAP timeline length - Never create empty tweens to set duration
Rules (Non-Negotiable)
Deterministic: No Math.random(), Date.now(), or time-based logic. Use a seeded PRNG if you need pseudo-random values (e.g. mulberry32).
GSAP: Only animate visual properties (opacity, x, y, scale, rotation, color, backgroundColor, borderRadius, transforms). Do NOT animate visibility, display, or call video.play()/audio.play().
Animation conflicts: Never animate the same property on the same element from multiple timelines simultaneously.
No repeat: -1: Infinite-repeat timelines break the capture engine. Calculate the exact repeat count from composition duration: repeat: Math.ceil(duration / cycleDuration) - 1.
Synchronous timeline construction: Never build timelines inside async/await, setTimeout, or Promises. The capture engine reads window.__timelines synchronously after page load. Fonts are embedded by the compiler, so they're available immediately — no need to wait for font loading.
Never do:
- Forget
window.__timelinesregistration - Use video for audio — always muted video + separate
<audio> - Nest video inside a timed div — use a non-timed wrapper
- Use
data-layer(usedata-track-index) ordata-end(usedata-duration) - Animate video element dimensions — animate a wrapper div
- Call play/pause/seek on media — framework owns playback
- Create a top-level container without
data-composition-id - Use
repeat: -1on any timeline or tween — always finite repeats - Build timelines asynchronously (inside
async,setTimeout,Promise) - Use
gsap.set()on clip elements from later scenes — they don't exist in the DOM at page load. Usetl.set(selector, vars, timePosition)inside the timeline at or after the clip'sdata-starttime instead. - Use
<br>in content text — forced line breaks don't account for actual rendered font width. Text that wraps naturally + a<br>produces an extra unwanted break, causing overlap. Let text wrap viamax-widthinstead. Exception: short display titles where each word is deliberately on its own line (e.g., "THE\nIMMORTAL\nGAME" at 130px).
Scene Transitions (Non-Negotiable)
Every multi-scene composition MUST follow ALL of these rules. Violating any one of them is a broken composition.
- ALWAYS use transitions between scenes. No jump cuts. No exceptions.
- ALWAYS use entrance animations on every scene. Every element animates IN via
gsap.from(). No element may appear fully-formed. If a scene has 5 elements, it needs 5 entrance tweens. - NEVER use exit animations except on the final scene. This means: NO
gsap.to()that animates opacity to 0, y offscreen, scale to 0, or any other "out" animation before a transition fires. The transition IS the exit. The outgoing scene's content MUST be fully visible at the moment the transition starts. - Final scene only: The last scene may fade elements out (e.g., fade to black). This is the ONLY scene where
gsap.to(..., { opacity: 0 })is allowed.
WRONG — exit animation before transition:
// BANNED — this empties the scene before the transition can use it
tl.to("#s1-title", { opacity: 0, y: -40, duration: 0.4 }, 6.5);
tl.to("#s1-subtitle", { opacity: 0, duration: 0.3 }, 6.7);
// transition fires on empty frame
RIGHT — entrance only, transition handles exit:
// Scene 1 entrance animations
tl.from("#s1-title", { y: 50, opacity: 0, duration: 0.7, ease: "power3.out" }, 0.3);
tl.from("#s1-subtitle", { y: 30, opacity: 0, duration: 0.5, ease: "power2.out" }, 0.6);
// NO exit tweens — transition at 7.2s handles the scene change
// Scene 2 entrance animations
tl.from("#s2-heading", { x: -40, opacity: 0, duration: 0.6, ease: "expo.out" }, 8.0);
Animation Guardrails
- Offset first animation 0.1-0.3s (not t=0)
- Vary eases across entrance tweens — use at least 3 different eases per scene
- Don't repeat an entrance pattern within a scene
- Avoid full-screen linear gradients on dark backgrounds (H.264 banding — use radial or solid + localized glow)
- 60px+ headlines, 20px+ body, 16px+ data labels for rendered video
font-variant-numeric: tabular-numson number columns
If no frame.md or design.md exists, follow house-style.md for aesthetic defaults.
Typography and Assets
- Built-in fonts: Write the
font-familyyou want in CSS — the compiler embeds supported fonts automatically. - Custom fonts: If the spec (
frame.mdordesign.md) names a font that isn't built-in, the user must provide.woff2files in afonts/directory. If missing, warn before writing HTML. When files exist, add@font-facedeclarations pointing to the local files. - Add
crossorigin="anonymous"to external media - For dynamic text overflow, use
window.__framevideo.fitTextFontSize(text, { maxWidth, fontFamily, fontWeight }) - All files live at the project root alongside
index.html; sub-compositions use../
Editing Existing Compositions
- Read actual files, don't guess. When editing, extending, or creating companion compositions, read the existing source. Don't reconstruct hex codes from memory. Don't guess GSAP easing patterns. The composition IS the spec — extract exact values from it.
- Match existing fonts, colors, animation patterns from what you read
- Only change what was requested
- Preserve timing of unrelated clips
Output Checklist
Chanjing OAuth During Authoring
When a requested video needs Chanjing-backed features (public voices, platform speech, digital humans, or any Studio panel reporting missing Chanjing auth), i
…(truncated)