Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FXVsD1DgdzZLiPuwtKzMSY
12 KiB
12 KiB
Composition format
The detail behind the contract in SKILL.md. Full references: npx hyperframes docs compositions, docs data-attributes, docs troubleshooting.
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.
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 |
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.
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.
- 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), orlocal('Exact Name')for a font installed on this machine.
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.
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.
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.
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 |
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> or its wrapper, not both |
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_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 |