Skills › HyperFrames › hyperframes › format.md
format.md
159 lines · skills/hyperframes/references/format.md at a595755
Most marks come from matching words in the file, so treat them as pointers.
1# Composition format
3The detail behind the contract in `SKILL.md`. Full references: `npx hyperframes docs compositions`, `docs data-attributes`, `docs troubleshooting`.
Composition format
The detail behind the contract in SKILL.md. Full references: npx hyperframes docs compositions, docs data-attributes, docs troubleshooting.
5## Shape of a project
7```html
8<!-- index.html: a normal document; declares variables, mounts scenes -->
9<html
10 data-composition-variables='[{"id":"headline","type":"string","label":"Headline","default":"…"}]'
11>
12 <body>
13 <div
14 id="root"
15 data-composition-id="main"
16 data-start="0"
17 data-duration="12"
18 data-width="1920"
19 data-height="1080"
20 >
21 <div
22 id="intro"
23 data-composition-id="intro"
24 data-composition-src="compositions/intro.html"
25 data-start="0"
26 data-duration="4"
27 data-track-index="1"
28 data-width="1920"
29 data-height="1080"
30 ></div>
31 </div>
32 <script>
33 window.__timelines["main"] = gsap.timeline({ paused: true });
34 </script>
35 </body>
36</html>
37```
39```html
40<!-- compositions/intro.html: everything inside <template> -->
41<template>
42 <style>
43 #root {
44 position: absolute;
45 inset: 0;
46 }
47 </style>
48 <div id="root" data-composition-id="intro" data-width="1920" data-height="1080">
49 <h1 id="headline" class="clip" data-start="0.4" data-duration="3" data-var-text="headline">
50 …
51 </h1>
52 </div>
53 <script>
54 const tl = gsap.timeline({ paused: true });
55 tl.fromTo(
56 "#headline",
57 { opacity: 0, filter: "blur(8px)" },
58 { opacity: 1, filter: "blur(0px)", duration: 0.18, ease: "power4.out" },
59 0.4,
60 );
61 window.__timelines["intro"] = tl;
62 </script>
63</template>
64```
66- Give the host, the scene root and the timeline key the same id. A host id that differs from its scene's makes each render wait 45 s per scene, then show static frames. Styles and scripts outside `<template>` are dropped.
67- Size the scene root with `inset: 0`, never hard-coded pixels; the canvas is `data-width` × `data-height`.
68- Keep ids unique across the assembled page (prefix them: `#intro-title`). A scene's timeline can't reach elements outside its file.
69- Use `fromTo`, not `from`, inside a scene: the host re-seeks the scene each time it shows, and `from` records its start state once, so it drifts on seek-back.
70- Never `add()` a scene's timeline to the root's; the runtime drives each scene itself, and nesting seeks it twice.
Shape of a project
<!-- index.html: a normal document; declares variables, mounts scenes -->
<html
data-composition-variables='[{"id":"headline","type":"string","label":"Headline","default":"…"}]'
>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="12"
data-width="1920"
data-height="1080"
>
<div
id="intro"
data-composition-id="intro"
data-composition-src="compositions/intro.html"
data-start="0"
data-duration="4"
data-track-index="1"
data-width="1920"
data-height="1080"
></div>
</div>
<script>
window.__timelines["main"] = gsap.timeline({ paused: true });
</script>
</body>
</html><!-- compositions/intro.html: everything inside <template> -->
<template>
<style>
#root {
position: absolute;
inset: 0;
}
</style>
<div id="root" data-composition-id="intro" data-width="1920" data-height="1080">
<h1 id="headline" class="clip" data-start="0.4" data-duration="3" data-var-text="headline">
…
</h1>
</div>
<script>
const tl = gsap.timeline({ paused: true });
tl.fromTo(
"#headline",
{ opacity: 0, filter: "blur(8px)" },
{ opacity: 1, filter: "blur(0px)", duration: 0.18, ease: "power4.out" },
0.4,
);
window.__timelines["intro"] = tl;
</script>
</template>- Give the host, the scene root and the timeline key the same id. A host id that differs from its scene's makes each render wait 45 s per scene, then show static frames. Styles and scripts outside
<template>are dropped. - Size the scene root with
inset: 0, never hard-coded pixels; the canvas isdata-width×data-height. - Keep ids unique across the assembled page (prefix them:
#intro-title). A scene's timeline can't reach elements outside its file. - Use
fromTo, notfrom, inside a scene: the host re-seeks the scene each time it shows, andfromrecords its start state once, so it drifts on seek-back. - Never
add()a scene's timeline to the root's; the runtime drives each scene itself, and nesting seeks it twice.
72## Attributes you'll use most
74| Attribute | Meaning |
75| ---------------------------------------------- | --------------------------------------------------------------------------------------------------- |
76| `data-start`, `data-duration` | Seconds, local to the file that declares the element |
77| `data-track-index` | Studio lane only; paint order is CSS `z-index` |
78| `data-track-kind` | `graphics` or `captions` on scene hosts (video and audio come from the tag) |
79| `data-composition-src`, `data-variable-values` | Mount a scene file; per-mount variable values (a JSON object) |
80| `data-media-start` | Offset into the source media (a trim) |
81| `data-playback-rate` | Constant speed, 0.1–10 |
82| `data-has-audio="true"` | A `<video>` keeps its own sound; a timed `<video>` with neither this nor `muted` fails lint |
83| `data-volume`, `data-fade-in`, `data-fade-out` | Gain (1 = 0 dB, up to 3.98 = +12 dB) and edge fades; envelopes use `data-automation` (`/media-use`) |
84| `data-link`, `data-sync-origin` | A video and audio edited as one (see Linked clips) |
85| `data-hidden` | Hidden in preview and render; reversible |
86| `data-no-timeline` | On the root of a still composition: no timeline to wait for |
Attributes you'll use most
| Attribute | Meaning |
|---|---|
data-start, data-duration | Seconds, local to the file that declares the element |
data-track-index | Studio lane only; paint order is CSS z-index |
data-track-kind | graphics or captions on scene hosts (video and audio come from the tag) |
data-composition-src, data-variable-values | Mount a scene file; per-mount variable values (a JSON object) |
data-media-start | Offset into the source media (a trim) |
data-playback-rate | Constant speed, 0.1–10 |
data-has-audio="true" | A <video> keeps its own sound; a timed <video> with neither this nor muted fails lint |
data-volume, data-fade-in, data-fade-out | Gain (1 = 0 dB, up to 3.98 = +12 dB) and edge fades; envelopes use data-automation (/media-use) |
data-link, data-sync-origin | A video and audio edited as one (see Linked clips) |
data-hidden | Hidden in preview and render; reversible |
data-no-timeline | On the root of a still composition: no timeline to wait for |
88## Variables
90- Declare an array of `{id, type, label, default}`: the video's own on `index.html`'s `<html>`, a scene or block file's on that file's `<html>` (or root element), where they stay inside each mount. Types: `string` (`placeholder`, `maxLength`), `number` (`min`, `max`, `step`, `unit`), `color`, `boolean`, `enum` (`options: [{value, label}]`).
91- Bind without script: `data-var-text="id"`, `data-var-src="id"` (keep a real fallback `src` on media with audio), `var(--id)` in CSS; for logic, `window.__hyperframes.getVariables()` once at init.
92- Values are an object keyed by id. A host's `data-variable-values` sets one mount; Studio's Variables panel and batch renders set only the root's variables, so to vary a block, change its host's values. Variables can't change the root's `data-duration`.
Variables
- Declare an array of
{id, type, label, default}: the video's own onindex.html's<html>, a scene or block file's on that file's<html>(or root element), where they stay inside each mount. Types:string(placeholder,maxLength),number(min,max,step,unit),color,boolean,enum(options: [{value, label}]). - Bind without script:
data-var-text="id",data-var-src="id"(keep a real fallbacksrcon media with audio),var(--id)in CSS; for logic,window.__hyperframes.getVariables()once at init. - Values are an object keyed by id. A host's
data-variable-valuessets one mount; Studio's Variables panel and batch renders set only the root's variables, so to vary a block, change its host's values. Variables can't change the root'sdata-duration.
94## Fonts
96- Bundled, so they resolve by name: Inter, Montserrat, Outfit, Nunito, Oswald, League Gothic, Archivo Black, Space Mono, IBM Plex Mono, JetBrains Mono, EB Garamond, Playfair Display, Source Code Pro, Noto Sans JP, Roboto, Open Sans, Lato, Poppins. Common system fonts (Helvetica, Arial, Times, Menlo and the like) are swapped for the nearest of these.
97- The bundle only has Latin text at a few weights per family (Inter: 400, 700, 900). The compiler still fetches each of these families from Google Fonts for the other weights and scripts, and caches the result in `~/.cache/hyperframes/fonts`.
98- Any other family needs `@font-face` in the file that uses it, or lint fails `font_family_without_font_face`: `src: url('assets/fonts/x.woff2')` for a file in the project (a capture's own fonts, or one you download there), A `local('Exact Name')`-only face draws the font installed on this machine; a cloud render has no copy of it (lint warns `font_face_local_only`), so ship the file instead when the video may render in the cloud.
Fonts
- Bundled, so they resolve by name: Inter, Montserrat, Outfit, Nunito, Oswald, League Gothic, Archivo Black, Space Mono, IBM Plex Mono, JetBrains Mono, EB Garamond, Playfair Display, Source Code Pro, Noto Sans JP, Roboto, Open Sans, Lato, Poppins. Common system fonts (Helvetica, Arial, Times, Menlo and the like) are swapped for the nearest of these.
- The bundle only has Latin text at a few weights per family (Inter: 400, 700, 900). The compiler still fetches each of these families from Google Fonts for the other weights and scripts, and caches the result in
~/.cache/hyperframes/fonts. - Any other family needs
@font-facein the file that uses it, or lint failsfont_family_without_font_face:src: url('assets/fonts/x.woff2')for a file in the project (a capture's own fonts, or one you download there), Alocal('Exact Name')-only face draws the font installed on this machine; a cloud render has no copy of it (lint warnsfont_face_local_only), so ship the file instead when the video may render in the cloud.
100## Timing and the timeline
102- A clip shows over `[start, start + duration)`: an end state landing exactly on its end is never rendered.
103- Only a composition's root timeline is paused; timelines nested inside it are not. Never `tl.play()`.
104- A scene's clock starts at 0 at its host's `data-start`; media `data-start` is local to its composition. `<video>` / `<audio>` without `data-duration` play their full length; an `<img>` without one lasts 3 s.
105- A host's `data-duration` is the scene's slot: a shorter scene timeline holds its last frame, and a slot shorter than the video goes blank when it ends.
106- Root children with `data-start` are laid out full-frame by the runtime; an untimed full-bleed background needs `position: absolute; inset: 0` or it collapses to nothing.
107- A fill on the composition root is dropped when the video has shader transitions or HDR media; put it on a full-bleed child.
108- `data-start="intro + 0.5"` starts after `intro` ends (spaces required). A typo, a missing id or a cycle silently resolves to 0, so snapshot what you time this way.
109- A clip nested inside another timed element counts its `data-start` from the composition, not from that element, and shows only while that element shows. `timeline` lists it under its parent and warns `never visible` when the windows miss (`lint`: `nested_clip_never_visible`).
110- `npx hyperframes timeline --json` lists every clip by kind with `absStart`, `absEnd`, `file`, `src`, `playbackRate`, `nested`, `host` and `durationSource`: read it instead of opening every file. It reads one level of nesting (a deeper scene shows `children=unread`); `laneError` means a `data-automation` or `data-fx-chain` that doesn't parse.
Timing and the timeline
- A clip shows over
[start, start + duration): an end state landing exactly on its end is never rendered. - Only a composition's root timeline is paused; timelines nested inside it are not. Never
tl.play(). - A scene's clock starts at 0 at its host's
data-start; mediadata-startis local to its composition.<video>/<audio>withoutdata-durationplay their full length; an<img>without one lasts 3 s. - A host's
data-durationis the scene's slot: a shorter scene timeline holds its last frame, and a slot shorter than the video goes blank when it ends. - Root children with
data-startare laid out full-frame by the runtime; an untimed full-bleed background needsposition: absolute; inset: 0or it collapses to nothing. - A fill on the composition root is dropped when the video has shader transitions or HDR media; put it on a full-bleed child.
data-start="intro + 0.5"starts afterintroends (spaces required). A typo, a missing id or a cycle silently resolves to 0, so snapshot what you time this way.- A clip nested inside another timed element counts its
data-startfrom the composition, not from that element, and shows only while that element shows.timelinelists it under its parent and warnsnever visiblewhen the windows miss (lint:nested_clip_never_visible). npx hyperframes timeline --jsonlists every clip by kind withabsStart,absEnd,file,src,playbackRate,nested,hostanddurationSource: read it instead of opening every file. It reads one level of nesting (a deeper scene showschildren=unread);laneErrormeans adata-automationordata-fx-chainthat doesn't parse.
112## Adding media
114Write what a Studio drop writes, so the clip behaves like one the person dropped:
116- `id`, `class="clip"`, `data-start` (the playhead or the requested time, never a silent 0) and `data-track-index`. Video with sound: `playsinline data-has-audio="true"`; silent footage: `muted playsinline`. Audio: `data-volume="1"`.
117- Images and video fill the frame: `position: absolute; left: 0; top: 0`, width and height equal to the canvas, `object-fit: contain`.
118- `z-index` is the number of top-level clips already in that file plus one.
119- Raise the root's `data-duration` to cover the new clip's end, or it never plays.
Adding media
Write what a Studio drop writes, so the clip behaves like one the person dropped:
id,class="clip",data-start(the playhead or the requested time, never a silent 0) anddata-track-index. Video with sound:playsinline data-has-audio="true"; silent footage:muted playsinline. Audio:data-volume="1".- Images and video fill the frame:
position: absolute; left: 0; top: 0, width and height equal to the canvas,object-fit: contain. z-indexis the number of top-level clips already in that file plus one.- Raise the root's
data-durationto cover the new clip's end, or it never plays.
121## Linked clips
123Studio's Detach audio writes a muted `<video>` and an `<audio>` of the same file sharing `data-link` and `data-sync-origin`; Studio moves, trims, splits and deletes them as one.
125- Prefer one `<video data-has-audio="true">`; link only when the sound needs its own track, volume or effects.
126- Every member keeps the same `data-start`, `data-duration`, `data-media-start` and `data-playback-rate`; retime them all together.
127- Unlink by removing `data-link` from every member. Leave `data-sync-origin` alone while they are still one source.
Linked clips
Studio's Detach audio writes a muted <video> and an <audio> of the same file sharing data-link and data-sync-origin; Studio moves, trims, splits and deletes them as one.
- Prefer one
<video data-has-audio="true">; link only when the sound needs its own track, volume or effects. - Every member keeps the same
data-start,data-duration,data-media-startanddata-playback-rate; retime them all together. - Unlink by removing
data-linkfrom every member. Leavedata-sync-originalone while they are still one source.
129## Layout audit opt-outs
131`check` measures boxes at sampled times; `overflow: hidden` does not silence a finding. Use the narrowest opt-out:
133| Attribute | Silences |
134| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
135| `data-layout-allow-overflow` | Overflow on the element and every descendant, including text-clipping checks: keep it on the smallest decorative wrapper |
136| `data-layout-bleed="true"` | One intentional crop of primary text |
137| `data-layout-allow-overlap` | Deliberate text layering, on that block only (not inherited) |
138| `data-layout-allow-occlusion` | An element meant to cover text |
139| `data-layout-allow-caption-zone` | Intentional copy in the caption band |
140| `data-layout-ignore` | A decorative element never audited |
Layout audit opt-outs
check measures boxes at sampled times; overflow: hidden does not silence a finding. Use the narrowest opt-out:
| Attribute | Silences |
|---|---|
data-layout-allow-overflow | Overflow on the element and every descendant, including text-clipping checks: keep it on the smallest decorative wrapper |
data-layout-bleed="true" | One intentional crop of primary text |
data-layout-allow-overlap | Deliberate text layering, on that block only (not inherited) |
data-layout-allow-occlusion | An element meant to cover text |
data-layout-allow-caption-zone | Intentional copy in the caption band |
data-layout-ignore | A decorative element never audited |
142## Lint errors you'll meet first
144| Rule | Fix |
145| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ |
146| `video_missing_muted` | A timed `<video>` gets `data-has-audio="true"` or `muted` |
147| `gsap_css_transform_conflict` | Set the start inside the tween (`gsap.fromTo`) instead of a CSS `transform` |
148| `gsap_animates_clip_element` | Animate a child of the `.clip`, or its `opacity` |
149| `video_nested_in_timed_element` | Time the `<video>`; its wrapper stays untimed and gives it no timing |
150| `media_missing_id`, `media_crossorigin_breaks_preview` | Give every `<audio>` an `id`; drop `crossorigin` |
151| `gsap_timeline_registered_before_async_build` | Register the timeline at the end of the async callback |
152| `standalone_composition_wrapped_in_template` | Only scene files use `<template>` |
153| `subcomposition_linked_style_names_root_id` | A linked sheet cannot style a scene's root by id: use an inline `<style>`, or select a child |
154| `subcomposition_root_styled_by_class` | Style a scene's root as `#root` |
155| `subcomposition_blanks_before_host` | Make the scene's slot span the time it should show |
156| `clip_ends_past_root_duration` | Raise the root's `data-duration` |
157| `linked_clips_out_of_sync`, `linked_clip_orphan` | Retime every linked member together; unlink from all of them |
158| `sweep_static` | A 3 s+ video with no visible change: spread the motion, or mark a still composition `data-no-timeline` |
159| `missing_data_no_timeline` | A root or host with no timeline: add `data-no-timeline`, or every render waits 45 s |
Lint errors you'll meet first
| Rule | Fix |
|---|---|
video_missing_muted | A timed <video> gets data-has-audio="true" or muted |
gsap_css_transform_conflict | Set the start inside the tween (gsap.fromTo) instead of a CSS transform |
gsap_animates_clip_element | Animate a child of the .clip, or its opacity |
video_nested_in_timed_element | Time the <video>; its wrapper stays untimed and gives it no timing |
media_missing_id, media_crossorigin_breaks_preview | Give every <audio> an id; drop crossorigin |
gsap_timeline_registered_before_async_build | Register the timeline at the end of the async callback |
standalone_composition_wrapped_in_template | Only scene files use <template> |
subcomposition_linked_style_names_root_id | A linked sheet cannot style a scene's root by id: use an inline <style>, or select a child |
subcomposition_root_styled_by_class | Style a scene's root as #root |
subcomposition_blanks_before_host | Make the scene's slot span the time it should show |
clip_ends_past_root_duration | Raise the root's data-duration |
linked_clips_out_of_sync, linked_clip_orphan | Retime every linked member together; unlink from all of them |
sweep_static | A 3 s+ video with no visible change: spread the motion, or mark a still composition data-no-timeline |
missing_data_no_timeline | A root or host with no timeline: add data-no-timeline, or every render waits 45 s |