compose-video
Compose Video
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.
Approach
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).
- Timing — which clips drive the duration, where do transitions land, what's the pacing.
- Execute — then implement using the rules below.
For small edits (fix a color, adjust timing, add one element), skip straight to the rules.
When no visual-style.md or animation direction is provided, follow house-style.md for motion defaults, sizing, and color palettes.
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-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 |
Composition Structure
Every composition is a <template> wrapping a <div> with data-composition-id. Each must include its own GSAP script and register its timeline:
<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>
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>
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. The renderer must produce identical output every time.
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 — causes flickering in headless renders.
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
Editing Existing Compositions
- Read the full composition first — match existing fonts, colors, animation patterns
- Only change what was requested — don't rewrite untouched sections
- Don't rewrite entire files for small changes
- Preserve timing of unrelated clips
Typography and Assets
- Every composition loads its own fonts (
@importor@font-facein<style>) - Use
font-display: blockfor local fonts — renderer needs fonts loaded before capturing - Add
crossorigin="anonymous"to media loaded from external URLs - Minimum readable text: 20px landscape, 18px portrait
- All files (video, audio, fonts, images) live at the project root alongside
index.html - From sub-compositions, use
../to reference root files
For PiP, title cards, and slide show patterns, see patterns.md.
Output Checklist
- Every top-level container has
data-composition-id - Every composition has
data-width,data-height,data-duration - Compositions in own HTML files, loaded via
data-composition-src -
<template>wrapper on sub-compositions -
window.__timelinesregistered for every composition - 100% deterministic — no randomness
- Each composition includes GSAP:
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>