Files
hyperframes/docs/guides/claude-design-hyperframes.md
WaterrrForeverandClaude Opus 5.5 688229314f feat(skills)!: publish seven knowledge skills aligned with Studio and retire the workflow skills (#5651)
* refactor(skills): fold six domain skills into hyperframes and media-use guides

hyperframes-core, -animation, -keyframes, -creative and -studio move to
skills/hyperframes/{core,animation,keyframes,creative,studio}/, and
hyperframes-audio moves to skills/media-use/mix/. Each moved SKILL.md becomes
a frontmatter-free GUIDE.md so installers that scan recursively for SKILL.md
no longer see them as skills; the published list goes from 21 to 15.

Relative links, code paths and skill-name mentions are rewritten to the new
locations, the catalog surfaces, marketplace core-skills list, fallback core
set, module-copy targets and skills-manifest.json follow, and
`hyperframes skills update` now removes the retired skills (RETIRED_SKILLS)
when they are still installed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(skills): slim the core entries and give media-use a HeyGen and a free path

The four entries every run opens are rewritten to what an agent cannot infer:
hyperframes 21.8 KB to 7.7 KB, hyperframes-cli 24.8 KB to 6.9 KB,
hyperframes-registry 9.9 KB to 4.3 KB, media-use 9.7 KB to 4.9 KB. Detail moved
whole into references (routing close calls and creator edits, CLI pin upgrades,
catalog search tiers, CLI conventions and feedback reports).

- hyperframes: state table, routing, the render contract, a terminal work loop
  (check, snapshot, preview before render), the desktop-app hand-off, and a
  table of the folded guides.
- media-use: one table of HeyGen versus free local tools per need; HeyGen
  sign-in, credits and the CLI's video-agent, avatar, clone, lipsync, translate
  and clipping commands; every free path works with no account.
- scripts/check-core-skills.mjs holds entries and descriptions to budget and
  checks every taught `npx hyperframes` command and path; runs in lint.
- Core entries are exempt from the formatter: its table padding cost ~1.5 KB
  per entry on every run.
- Content tests follow the text to its new home.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): plugin runs skip skills update; media-use explains HeyGen sign-in and TTS setup

The smoke eval showed a presenter request never reaching media-use and the
free voiceover failing on a missing kokoro-onnx; plugin runs also ran the
skills update the plugin rules forbid.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(skills): drop a committed Python bytecode file and ignore bytecode

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(skills): align the entry with the Studio skills and keep the workflows' measured facts

The entry drops routing and the intent interview and carries the contract, the
editable-project rules and the work loop. Studio's format, runtimes, motion,
look and captions references come over for terminal use, frame presets move to
assets/, media-use gains Studio's mixing and local-command references, and
film.md and overlays.md keep what the workflow skills measured.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(skills)!: retire the workflow skills and folded guides; publish seven knowledge skills

The skill pack now publishes exactly seven skills - hyperframes,
hyperframes-cli, hyperframes-registry, media-use, figma, slideshow and
remotion-to-hyperframes - carrying knowledge, not process. The eight
workflow skills, the folded domain guides and the routing/interview
references are deleted; their measured facts already live in the
hyperframes references (format, runtimes, motion, look, captions, film,
overlays) and media-use's mixing reference.

- bake-basemap.mjs and locate.mjs move to skills/hyperframes/scripts/
  (bake-basemap gains --help); extract-audio-data.py moves to
  media-use/audio/scripts/ for the caption motion reference.
- skills update: the eight workflows join RETIRED_SKILLS so existing
  installs prune them (by lock attribution only, since their names are
  generic); naming a retired skill reports where its knowledge went and
  succeeds instead of failing as unknown.
- Capture prompts point at the hyperframes skill's references/film.md;
  the live-map warning names the new bake-basemap path.
- Content tests that pinned knowledge are retargeted to the kept
  references; tests of deleted workflows, scripts and process go.
- Catalog surfaces, templates and docs describe the seven skills.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): resolve the local.md captions link and allow capture's output files in citations

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): regenerate the skills manifest from tracked files only

The committed media-use hash included an ignored __pycache__ bytecode file present
in the local checkout, so CI's clean checkout hashed differently.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(docs): restore generated catalog pages and docs navigation to main

Catalog PRs commit item sources only; CI regenerates the pages from the edited
registry sources. docs/docs.json is catalog navigation, so its Workflows group
rename is left for a non-catalog PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* build(cli): raise the CLI size budget for the skill references it bundles

The CLI bundles skills/hyperframes, which now carries the folded domain guides and
frame presets (155 KB to 1.08 MB in git). Budgets are the CI-measured size plus 5%:
packed 7526634, unpacked 22593213.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: keep repo-sweep guards meaningful after the workflow skills left

The last shell ffprobe caller and most root index.html examples lived in retired
skills. The shell detector is now pinned on a literal command line, and the
structure-rule sweep needs more than 5 roots (11 remain) rather than 30.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(skills): slim figma, slideshow and remotion to the Studio shape; restore the storyboard board

slideshow 33.5 KB to 4.9, figma 18.2 to 6.2, remotion-to-hyperframes 11.0 to
4.9, each keeping the facts only it holds (the figma motion helpers, verify gate,
storyboard decoding and usage beacons; present and hand-off; the Remotion lint,
SSIM check and API map). The board page format from the storyboard recipe comes
back as hyperframes/references/storyboard.md: the desktop app's Storyboard tab
reads boards/storyboard.html by its frame, title and time classes. The entry and
the init template now say to snapshot the moments an edit changed before
replying, which the terminal eval found skipped in 22 of 38 runs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): manifest without local bytecode; Studio's edit prompt names kept files

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): render when the person asks for the file or for no stops, not only after approval

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): keep the voiceover carve in media-use; presets stage their own fonts

Review follow-ups on #5651:
- carve.mjs and its tests move to skills/media-use/audio/scripts; the audio
  docs, mixing.md and two Studio comments point there.
- code-editorial's FRAME.md says to copy its bundled WOFF2s and add the
  @font-face blocks, instead of promising the retired build-frame.mjs.
- media-use states the host rule before the HeyGen sign-in steps.
- storyboard-format marks transition_in as a note the agent builds itself.
- Two stale citations (creator-editing-recipes.md, sub-compositions.md).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(skills): regenerate the manifest without local bytecode

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): the CLI render step follows step 4's rule, not approval alone

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(ci): run the core-skills check from lint again after the main merge

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(skills): films that tell a story start from three storylines; maps use real imagery

Opus 5.5 arm D read motion.md and applied it, but its launch film was a
feature list (pain, three feature cards, proof, logo): film.md left story
to the model. And the map case never opened film.md, so it drew Natural
Earth outlines instead of baking satellite tiles.

- references/story.md: the generic half of Studio's moment-film story and
  craft (one thing, three storylines with world and device, the hook,
  beats, worlds, copy, brand, tells of a generated film). The person picks
  a storyline, or the agent takes its recommendation when asked for the
  file or for no stops.
- The entry's work loop names the storyline step for films over ~15 s; the
  references table routes stories to story.md and maps to film.md.
- film.md points at story.md and gives maps and real places a section.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): story.md follows the entry: no voiceover unless asked

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): bake-basemap starts from a globe and stops waiting out its idle timeout

The Opus 5.5 map run spent 13 of its 38 minutes on parallel bakes that
stalled and 15 more recording 300 frames, after rewriting the script because
it could only start on a flat map.

- PROJECTION=globe with a seeded starfield and atmosphere, and FROM/PAN to
  turn the globe to CENTER before the dive.
- Each frame polls for loaded tiles instead of waiting for an "idle" event
  that had often already fired: 30 frames went from 198-219 s to 10-13 s
  locally with byte-identical frames.
- Progress prints seconds a frame and minutes left; film.md shows the globe
  call and says to run one bake at a time.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): render in the foreground; wait on a detached render's log, never pgrep -f

Both Opus 5.5 explainer runs started a detached render, then waited with
`until ! pgrep -f "hyperframes render"`, which matches its own loop: each
render took 88-117 s and the wait sat for the tool's full 10 minutes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(skills): route each kind of film; storylines carry look and sound; add review and sound

The entry routes by request (launch, explainer, music, data, vertical, 3D,
character) to a self-contained file under references/types/, and a film
over ~15 s now gets music and sounds on its visible actions unless the
person asked for silence. Each storyline names its look and its sound, so
picking one picks both; STORYBOARD.md frames carry `reads` and `sound`.

review.md is the whole-film pass: the check warnings, the review sheets,
seven questions to look for, P0/P1/P2 notes and REVIEW.md. motion.md gains
eases by role, six title entrances, a reading-time rule and nine measured
transitions; look.md a design plan before the first scene, with presets
kept for decks and named picks. story.md adds the second default that
generated design falls into, with what to do instead.

The blank template and format.md no longer teach the fade-and-slide-up
entrance on a dark canvas.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(cli): check warns on unread lines, dead stretches, small type, empty frame 0 and silence

A coding agent cannot watch its film, so check now measures what a viewer
notices over time. A new `craft` section holds warnings only (exit 0
unless --strict), sampled on the dense 8 fps grid the overlap pass
already seeks, skipped for --range checks:

- line_unread: a reading-size line (>= 30 px tall at 1080) on screen
  under 0.4 s for 1-2 words, else under 3/4 of 0.5 s + 0.3 s a word;
  the film's last line needs 2 s. Short lines are re-measured between
  samples to ~0.01 s before judging. Captions-track and
  data-layout-ignore text are exempt.
- dead_stretch: more than 3 s where the keepsMoving liveness signature
  does not change, outside the final 3 s and any data-hold element;
  quiet when sweep_static fires or a whole-composition keepsMoving
  assertion owns the limit.
- text_below_floor: text under 2.6% of the short side (28 px on 1080)
  held 0.5 s, one finding per file listing every element.
- first_frame_empty: frame 0 paints only the background.
- film_silent: over 15 s with no unmuted audio or audible video.

Each message names the fix and the skill reference to read. keepsMoving
now also leaves the film's last 3 s (a still end card) out of its
static runs, so the two agree.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(cli): snapshot --review captures a beat sheet, seam strips and a phone sheet

snapshot --review picks the frames that show a film over time instead of
evenly spaced ones, in the same single browser session:

- review-beats.jpg: one settled frame per scene at its start + 0.3 s,
  plus one per timed data-beat="name" element, labelled time and id.
  Scenes are the composition's top-level timed clips and sub-composition
  hosts, read like `timeline` reads rows (captions and audio excluded)
  and timed by the runtime's start resolver over the live page.
- review-seam-NN.jpg: frames at -0.2, -0.1, 0, +0.1, +0.2 s around
  every cut (each scene start, and any scene end no scene starts at).
- review-phone.jpg: the beat frames at 360 px wide.

--composition scopes the review to one sub-composition; with fewer than
two scenes the beats spread evenly and a note says so. snapshot also
gains --json: one envelope with every frame's path and time and, under
--review, the scenes, beats and seams behind the sheets; progress moves
to stderr.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cli): craft warnings stay out of --strict; a hold gets a real next action, not a caret

Craft findings (unread lines, dead stretches, small type, an empty first
frame, silence) are judgment calls a film may break on purpose, so
--strict keeps gating defects only. The check reference no longer
suggests a blinking caret to keep a still frame moving.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cli): review spreads beats when every scene starts together; map credits skip the type floor

A map film whose basemap and overlay both start at 0 s got one beat
frame; the even spread now keys on distinct scene starts. film.md tells
agents to mark the imagery credit data-layout-ignore and back it, since
the new type floor flagged it on the first real run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cli): review frames each scene at its middle too, not only 0.3 s in

On the first real explainer, 0.3 s into a scene showed a bare colour
field before its title entered. Each scene longer than about a second
and a half now adds a frame at its middle, so the sheet shows what the
scene builds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(skills): a film is built from a numeric SPEC.md, with defaults banned by name and a look test

Studied in full: flxrnc's Motion Lab (vague vs directed prompt), 0xMovez's
course, EianLu's riso skill, agentchengfeng's style samples and the two
Opus 5.5 guides. Their common point: adjectives are where the default look
lives, so the agent now writes SPEC.md before any scene (GOAL, OUTPUT,
TIME, LOOK, MOTION, SHOTS, BANNED, QA), with a number for every adjective.

spec.md bans the defaults by name, unless the brand or reference uses
them (a dark void with small things floating, glow and particles, the
cream-serif-mono-label look, fade-and-slide-up, corner brackets), and
grows that list after the first render. Each storyline gets a 3 s test of
its hero shot before the pick; a look is written as the rules of one
medium. Composition numbers: the hero fills 40% of the frame, one hero
move per shot, the shot's information in within 2.5 s then a held poster.

motion.md adds springs by damping ratio, shorter exits, overlap, stagger
and the beat grid; review.md reviews as a harsh director and grows BANNED;
vertical.md takes the riso skill's measured safe band; explainer.md the
shapes of information.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): story before look; every object reads as itself; posters only for info cards

The first spec-driven C02 swapped the dark default for a flat Swiss
poster, and the story went with it: two black bars nobody read as mirrors,
slogans instead of cause and effect, a storyline picked for its picture.

The spec now serves the story: a storyline is picked for the story a
stranger would follow, the look test asks first whether a stranger
understands what they see, and SPEC.md gains an OBJECTS line giving each
thing the material and details that make it read without a label. The
2.5 s held-poster rule is limited to information cards; in a story film
every shot causes the next. Explainers start from a person or situation
the viewer knows. Review names every object as a stranger would and
treats one that needs explaining as a P0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(skills): storylines are three films in words, each with a device and a form

The three storylines are pitched as text and the person picks from them;
nothing is built for a storyline before the pick, so the 3 s hero-shot
look test is gone. Ideas pair a device with a form, five or more first,
keeping the three that differ most, and the one stop states the rest in
an assumption line that names every paid item beside its free option.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci: list main's new embedded-captions hf-cli test among the retired workflow files

The merge with main removed it with the rest of the retired skill, and the
deletion guard rejects any removal it has not been told about.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(core): keep the audio preset comment within core's size budget

The comment's new citation to media-use's mixing reference made the published
package 14 bytes larger than its budget, which sits at main's exact size.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cli): craft keeps its own 8 fps grid; comments back within the checks

Review on #5651 found that the merge moved craft onto the overlap pass's
times, which main caps at 180, so a brief line in a long film fell between
samples and line_unread missed it. Craft now builds its own grid at 8 fps up to
600 samples, as before the merge, and a 60 s film with a line shown for 0.2 s
pins it.

The Comments check passes again: REVIEW.md and STORYLINES.md join the files a
skill writes into a project, the comment lines this branch added come back
under each file's share at the merge-base, and the audio preset comment drops
a word the check reads as history.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 19:19:31 +00:00

47 KiB

Claude Design + HyperFrames (Template-First)

⚠️ Doing a "Send to HyperFrames" import (sending your design to HeyGen as a URL, not a downloaded ZIP)? Use the Send-to guide instead: https://raw.githubusercontent.com/heygen-com/hyperframes/main/docs/guides/claude-design-send-to-hyperframes.md

This Template-First guide is for the Download-ZIP flow. Its authoring rules differ from Send-to, and following it for a Send-to import yields a low-fidelity result.

Your medium is HyperFrames compositions: plain HTML + CSS + a paused GSAP timeline. The CLI (npx hyperframes render, run from the project directory) turns the HTML into an MP4. You author the HTML -- the user renders locally.

HyperFrames replaces your default video-artifact workflow. Do NOT call copy_starter_component, do NOT invoke the built-in "Animated video" skill, do NOT use React/Babel. Plain HTML + GSAP only.


Your role

You produce a valid first draft -- not a final render. Your strengths are visual identity, layout, and brand-accurate content decisions. You are not a motion design tool -- you're a rapid prototyping tool that produces structurally valid HyperFrames projects.

The user's workflow:

  1. Claude Design (you) -- brand identity, scene content, layout, first-pass animations, shader choices
  2. Download ZIP -- user gets a valid HyperFrames project
  3. Claude Code (or any AI coding agent) -- animation polish, timing refinement, pacing, production QA with linting and live preview

Your output must be a valid starting point that Claude Code can open and immediately work with -- no structural fixes needed, just creative refinement.

What you optimize for (your strengths)

  • Correct brand identity from attachments (palette, typography, tone)
  • Strong visual layout per scene (hierarchy, spacing, readability)
  • Scene content that tells the story (headlines, stats, copy, imagery)
  • Structural validity (passes npx hyperframes lint with zero errors)
  • Appropriate shader transition choices for the mood
  • Reasonable scene count and durations for the video type

What Claude Code polishes after you (refinement, not creation)

You create ALL the animations, transitions, and mid-scene activity. Every scene ships with entrance tweens, breathing motion, and shader transitions. The video plays with full motion from your first draft.

What Claude Code does is watch the full playthrough with reliable preview tools and fine-tune:

  • Ease curve tweaks (swapping power3.out for expo.out after seeing it play)
  • Stagger timing adjustments (0.12 → 0.08 feels tighter for this specific scene)
  • Scene duration micro-adjustments (scene 4 drags at 4.5s, trim to 3.8s)
  • Adding richer mid-scene activity where a scene feels too static after playback
  • Shader swaps (this cinematic-zoom should be whip-pan for the energy shift)
  • Production QA (snapshot verification, cross-browser testing)

Think of it as: you create the first cut of the film, Claude Code does the edit bay refinement.


How this works

You get a pre-valid skeleton that already passes the HyperFrames linter. Your job:

  1. Read the brief, pick a skeleton
  2. Fill in the palette + typography (CSS custom properties)
  3. Fill in scene content (text, layout inside .scene-content)
  4. Fill in GSAP animations (timeline blocks marked per scene)
  5. Verify the preview, deliver the ZIP

The skeleton handles the structural rules -- data attributes, timeline registration, HyperShader wiring, initial visibility, preview.html token forwarding. You focus on the creative work.

What you can change: CSS custom properties, scene content, animation tweens, scene count (add/remove scenes following the rules below), shader choices, durations.

What you must not touch: The <script> loading order, window.__timelines initialization, the .scene.clip class on scene containers, the .scene-content wrapper inside each scene, the preview.html structure.


Step 1: Understand the brief

Gate: You can name the subject, duration, aspect ratio, and at least one source of visual direction.

Inputs, in order of reliability

  1. Attachments (strongest) -- screenshots, PDFs, brand guides, reference images. Mine for palette, type, tone.
  2. Pasted content -- hex codes, typefaces, copy, scripts.
  3. Research -- web_search the brand. Static pages (blogs, press, Wikipedia) work. SPA homepages return empty shells -- pivot to blog/press/Wikipedia.
  4. URLs the user provided -- start there, expand outward.

Ask ONE question if the brief is sparse

If the prompt has NONE of: an attachment, a hex code or named typeface, a named aesthetic/style/director, a well-known brand, or "just build" / "surprise me" -- ask one short clarifying question with concrete options. Wait for the reply.


Step 2: Pick a skeleton and fill identity

Gate: A working index.html exists with your palette and typography on :root. The preview renders (even if scenes are empty).

Choose by video type

Type Duration Scenes Skeleton
Social reel (9:16) 10-15s 5-7 Skeleton A
Launch teaser (16:9) 15-25s 7-10 Skeleton B
Product explainer (16:9) 30-60s 10-18 Skeleton C
Cinematic title (16:9) 45-90s 7-12 Skeleton D

Copy the skeleton (Section 7 below), then immediately fill the :root CSS custom properties:

:root {
  /* === FILL: Your brand identity === */
  --bg: #0a0a0d;
  --ink: #f5f5f7;
  --accent: #7c6cff;
  --muted: #5a6270;
  --accent-dim: #3d3680;
  --font-display: "Space Grotesk", sans-serif;
  --font-data: "JetBrains Mono", monospace;
}

Anti-monoculture

These are the defaults every LLM reaches for. Pick something the brief actually calls for:

  • Banned fonts: Inter, Inter Tight, Roboto, Open Sans, Noto Sans, Lato, Poppins, Outfit, Sora, Fraunces, Playfair Display, Cormorant Garamond, EB Garamond, Syne, Cinzel, Prata, Bodoni Moda, Nunito, Source Sans, PT Sans, Arimo.
  • Banned pairings: Fraunces + JetBrains Mono, Inter + anything, Playfair + Lato.
  • Question these defaults: gradient text, cyan-on-dark, pure #000/#fff, identical card grids, left-edge accent stripes, everything centered with equal weight.

Pick a real typeface pair. Weight contrast must be dramatic (300 vs 900, not 400 vs 700). Video sizes: 60px+ headlines, 20px+ body, 16px+ labels.


Step 3: Fill scenes -- content + animation

Gate: Every scene has visible content, at least 2 animation patterns from Section 8, and mid-scene activity. No scene is a static slide.

Work scene by scene. For each:

3a. Fill scene content

Put text, images, and layout inside the .scene-content wrapper. The wrapper already exists in the skeleton -- add your elements inside it.

<div class="scene-content">
  <h1 id="s3-title" class="display">$1.9 Trillion</h1>
  <p id="s3-sub" class="body-text">processed annually</p>
  <div id="s3-bar-chart">...</div>
</div>

Keep decoratives (glows, grain, vignette) OUTSIDE .scene-content, inside the scene div directly.

3b. Fill entrance animations

In the timeline block marked for this scene, add tl.from() tweens. Animate FROM offscreen/invisible TO the CSS position:

// === SCENE 3 ===
tl.from("#s3-title", { y: 40, autoAlpha: 0, duration: 0.6, ease: "power3.out" }, 10.3);
tl.from("#s3-sub", { y: 20, autoAlpha: 0, duration: 0.5, ease: "power2.out" }, 10.7);
tl.from(
  "#s3-bar-chart",
  { scaleY: 0, transformOrigin: "bottom", duration: 0.8, ease: "expo.out" },
  11.0,
);

Offset first tween 0.1-0.3s into the scene. Zero-delay entrances feel like jump cuts.

3c. Fill mid-scene activity (this is what separates video from slides)

Every visible element must keep moving AFTER its entrance. A still element on a still background is a JPEG with a progress bar. Use at least 2 patterns from Section 8 per scene.

Element Mid-scene motion Pattern from Section 8
Stat / number Counter animates from 0 to target Counter animation
SVG line / path Draws itself in real-time SVG stroke draw
Title / wordmark Characters enter one by one Character stagger
Logo / lockup Subtle vertical drift Breathing float
Chart / bars Bars fill sequentially Bar chart fill
Image / screenshot Slow zoom: scale: 1 -> 1.03 Ken Burns (just tl.to(el, { scale: 1.03, duration: sceneLength, ease: "none" }))
Accent / highlight Sweep across text Highlight sweep
Background glow Opacity pulse tl.to(".glow", { opacity: 0.6, duration: 1.5, ease: "sine.inOut", yoyo: true, repeat: 1 })

The minimum per scene: entrance tweens + at least one continuous motion (float, counter, zoom, or glow). Scenes with stats or charts should always use the counter or bar-fill pattern — these are the most visually engaging and easiest to implement.

3d. Adjust scene duration

The skeleton has placeholder durations. Adjust each scene's data-duration based on:

  • Reading time: count words of display text, use the budget below
  • Last readable element must finish entering by 50% of scene duration
Display text Min duration
No text (hero, icon) 1.5-2s
1-3 words (kicker, number) 2-3s
4-10 words (headline + subhead) 3-4s
11-20 words (sentence or two lines) 4-6s
21-35 words (paragraph) 6-8s
35+ words Split into two scenes

Hard ceiling: 5s per scene unless you name a specific reason (hero hold, cinematic push, long counter animation).

When you change a scene's duration, update data-start on subsequent scenes to keep them tiled end-to-end. Also update the root's data-duration to match the total.

Vary eases

Use at least 3 different eases per scene. Don't default to power2.out on everything.

Feeling Ease Duration
Smooth power2.out 0.4-0.6s
Snappy power4.out 0.2-0.3s
Bouncy back.out(1.6) 0.3-0.5s
Dramatic expo.out 0.3-0.5s
Dreamy sine.inOut 0.5-0.8s
Mechanical steps(5) 0.3-0.5s

Step 4: Transitions

The professional rule: most cuts are hard cuts

In professional video, ~95% of scene changes are hard cuts. Effect transitions (shaders, dissolves) are reserved for 2-3 key moments — a hero reveal, an energy shift, the CTA landing. Using a shader on every cut is the video equivalent of bolding every word in a paragraph.

The skeleton pre-wires 2 shader transitions at key moments and hard cuts everywhere else. This gives you varied rhythm: cut-cut-SHADER-cut-cut-SHADER-cut.

Three transition types

Hard cut (default -- most scenes use this): No transition code needed. Scene N disappears, scene N+1 appears. The entrance animations on the new scene do all the visual work. This is the professional default.

Shader transition (2-3 per video -- hero/climax/CTA moments): Pre-wired in the skeleton at key positions. HyperShader captures both scenes as textures and composites them pixel-by-pixel via WebGL.

When to use shaders vs hard cuts:

Use shader for Use hard cut for
Hero reveal / product unveil Connective scenes between features
Major energy shift or act break Rapid-fire lists or stats
CTA / final brand moment 3+ consecutive quick scene changes
Any moment the music punctuates Scenes where pacing should feel fast

Rule of thumb: a 6-8 scene video wants 2 shader transitions and the rest hard cuts.

Adjusting shader transitions

Change shader names -- pick from these 14:

domain-warp, ridged-burn, whip-pan, sdf-iris, ripple-waves, gravitational-lens, cinematic-zoom, chromatic-split, swirl-vortex, thermal-distortion, flash-through-white, cross-warp-morph, light-leak, glitch

Match shaders to energy:

Energy Shaders
Calm, editorial cross-warp-morph, light-leak, domain-warp
Medium, professional cinematic-zoom, whip-pan, sdf-iris
High, aggressive glitch, chromatic-split, ridged-burn
Ethereal, mysterious gravitational-lens, ripple-waves, swirl-vortex

Adjust transition timing -- when you change scene durations, recalculate each transition's time:

transition.time = scene_boundary - (transition.duration / 2)

Example: scene-3 ends at 8s, transition duration 0.5s -> time: 7.75.

Minimum transition duration: 0.3s. Sweet spot is 0.5s.

How the skeleton handles this

The skeleton only lists anchor scenes (the ones bracketing shader transitions) in HyperShader.init(). Anchor scenes use style="opacity:0;" because HyperShader manages their opacity. Non-anchor scenes use style="visibility:hidden;".

CRITICAL — two bugs cause "invisible middle scenes" if you don't handle them:

  1. Non-anchor scenes need explicit tl.set visibility toggles. Without them, the scene container stays at visibility:hidden and child animations play inside an invisible parent.

  2. The first anchor scene in each shader group needs tl.set("#sN", { opacity: 1 }, <start-time>). HyperShader browser mode does NOT auto-show the first anchor. It stays at opacity:0 for its entire window. Every demov4 composition has this bug.

The skeleton pre-wires these toggles for every non-anchor scene using autoAlpha (not visibility):

// --- Non-anchor scene toggles (REQUIRED — must use autoAlpha, not visibility) ---
tl.set("#s1", { autoAlpha: 0 }, 2.5); // hide s1 at its end time
tl.set("#s2", { autoAlpha: 1 }, 2.5); // show s2 at its start
tl.set("#s2", { autoAlpha: 0 }, 5.0); // hide s2 at its end
tl.set("#s3", { autoAlpha: 1 }, 5.0); // show s3 at its start
tl.set("#s3", { autoAlpha: 0 }, 7.5); // hide s3 at its end

Why autoAlpha and NOT visibility: When any shader transition fires, HyperShader blanks ALL .scene elements to opacity:0. If a non-anchor scene only toggles visibility, the blanket reset poisons its opacity — the scene becomes visibility:visible but opacity:0 (invisible). autoAlpha sets BOTH opacity AND visibility in one call, overriding the blanket reset.

Rules:

  • Every non-anchor scene gets tl.set("#sN", { autoAlpha: 1 }, <data-start>) AND tl.set("#sN", { autoAlpha: 0 }, <data-start + data-duration>)
  • Scene 1 gets only a hide at its end time (it starts visible)
  • Anchor scenes do NOT get autoAlpha toggles — HyperShader owns their opacity
  • When you add or remove scenes, update these toggles to match

Example for an 8-scene video with shaders at s4→s5 and s7→s8:

  • Anchor scenes: s4, s5, s7, s8 (listed in HyperShader scenes array, use opacity:0)
  • Non-anchor scenes: s1, s2, s3, s6 (NOT in HyperShader, use visibility:hidden, with explicit tl.set toggles)
  • Scene 1 has no inline style (visible from t=0)

Adding or removing shader transitions

To add a shader transition between two scenes:

  1. Add both scene IDs to the scenes array in HyperShader.init()
  2. Add a transition object to the transitions array
  3. Change both scenes from visibility:hidden to opacity:0
  4. Invariant: scenes.length === transitions.length + 1

To remove a shader transition (make it a hard cut instead):

  1. Remove the scene IDs from scenes (unless they're also anchors for another transition)
  2. Remove the transition from transitions
  3. Change affected scenes from opacity:0 to visibility:hidden

BANNED: invisible bridge transitions. Never pad with flash-through-white at 0.01s.


Step 5: Verify the preview + deliver

Gate: Preview plays start to finish. All scenes visible. No blinking. Text readable.

Verify in the preview pane

Scrub through every scene and check:

  1. Does scene 1 appear immediately? (If black: runtime not loaded, or __timelines key mismatch)
  2. Do shader transitions fire cleanly? (If blinking: transition too short, or exit animation before transition)
  3. Is all text readable against its background?
  4. Does every scene have motion during its hold? (If static: missing mid-scene activity)
  5. Do animations play in the correct order?

Troubleshooting: preview is black

Symptom Cause Fix
All black Runtime script missing or wrong order Check GSAP loads before runtime in <head>
All black __timelines key doesn't match data-composition-id Both must be "main"
All black Token not forwarded in preview.html Check location.search is appended to src
Scene doesn't appear Wrong data-start / data-duration Check scene windows tile end-to-end
Blink before transition Exit animation before shader fires Remove exit tweens -- shader IS the exit
Blink before transition Transition duration < 0.3s Increase to 0.5s
Seeking backwards shows blank Async capture race condition Known bug in HyperShader browser mode. Forward seek usually works. For reliable scrubbing, download and use npx hyperframes preview locally
Middle scene invisible First shader anchor not shown Add tl.set("#sN", { opacity: 1 }, startTime) for first anchor in each shader group
Middle scene invisible Non-anchor uses visibility instead of autoAlpha Change to tl.set("#sN", { autoAlpha: 1 }, start) and tl.set("#sN", { autoAlpha: 0 }, end). Shader blanket reset poisons opacity; visibility alone can't fix it.

Deliver

Provide: index.html, preview.html, README.md, and DESIGN.md.

The preview.html and README.md are already in the skeleton -- don't modify preview.html. Generate DESIGN.md from your :root custom properties as a reference document.

In your final message, tell the user:

  1. What you built -- scene count, duration, visual identity summary, shader transitions used
  2. What to do next -- download the ZIP, run npx hyperframes preview locally to see the full composition with reliable playback
  3. What to refine in Claude Code -- be specific about which scenes need animation polish, where timing could be tighter, which mid-scene activities are basic and could be richer. Don't just say "refine in Claude Code" -- say "scene 4's counter animation could be smoother with a longer duration, and scene 6 would benefit from a breathing float on the logo."
  4. Caveats -- placeholder assets, unverified stats, elements inspired by a real brand

Section 6: Rules you cannot break

The skeleton handles most structural rules. These are the runtime rules the skeleton can't enforce:

Determinism (non-negotiable)

Never Use instead
Math.random() Seeded PRNG (only if you need randomness)
Date.now(), performance.now() Hard-coded timing or tl.time() in onUpdate
setInterval, setTimeout Timeline tweens + onUpdate
repeat: -1 without root data-duration a finite root data-duration (export clips to it), or repeat: Math.max(0, Math.floor(duration / cycle) - 1)
stagger: { from: "random" } from: "start", "center", "end"
Async timeline construction Synchronous at page load

Media rules

Never Use instead
video.play(), audio.play() Framework owns playback
<video> with sound data-has-audio="true", no muted
Music, voiceover, or replacement audio separate <audio> element
Silent footage and b-roll muted
Base64 media File reference or HTTPS URL
Placeholder URLs (placehold.co) Real assets

Animation rules

Never Use instead
Exit tweens before shader transition Shader IS the exit -- content stays visible
tl.set / tl.to on scene containers HyperShader owns scene opacity
requestAnimationFrame GSAP tweens
Template literals in selectors Hardcoded strings
CSS transform for centering Flexbox centering on a wrapper
SVG filter data:image/svg+xml grain CSS radial-gradient grain (see pattern below)
Animating visibility / display Use autoAlpha

Self-review checklist

Run before delivering. Check with actual code, not assumptions.

Structural validity (must pass -- Claude Code can't fix these easily):

  • Every scene has class="scene clip" + all data attributes
  • Every scene has a <div class="scene-content"> wrapper
  • Anchor scenes have style="opacity:0;". Non-anchor scenes have style="visibility:hidden;"
  • Every non-anchor scene has tl.set with autoAlpha (NOT visibility). autoAlpha: 1 at start, autoAlpha: 0 at end.
  • First anchor scene in each shader group has tl.set("#sN", { opacity: 1 }, startTime). Without this, it stays invisible.
  • Scene windows tile end-to-end (no gaps)
  • Shader transitions have boundary INSIDE the window: time < boundary < time + duration
  • No transition shorter than 0.3s
  • No exit tweens except on the final scene
  • No Date.now(), unseeded Math.random(), or repeat: -1 without a finite root data-duration
  • No SVG filter data URLs as background-image
  • window.__timelines["main"] = tl matches data-composition-id

Brand + content accuracy (your core job -- get these right):

  • Colors match the brief / attachments exactly
  • No banned fonts
  • Minimum font sizes: 60px+ headlines, 20px+ body, 16px+ labels
  • font-variant-numeric: tabular-nums on number columns
  • Every scene has meaningful content (not placeholder text)
  • Scene count and durations match the video type

Animation baseline (good enough to start -- Claude Code will polish):

  • Every scene has at least one entrance tween (tl.from)
  • Every scene > 4s has at least one mid-scene activity (float, counter, glow)
  • No scene is completely static (no tweens at all)
  • Scene text is readable in the time allowed

Section 7: Skeletons

preview.html (universal -- copy verbatim for all video types)

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>HyperFrames Preview</title>
    <style>
      html,
      body {
        margin: 0;
        padding: 0;
        background: #111;
        height: 100%;
        overflow: hidden;
      }
    </style>
    <script type="module" src="https://cdn.jsdelivr.net/npm/@hyperframes/player"></script>
  </head>
  <body>
    <hyperframes-player
      id="p"
      controls
      autoplay
      muted
      style="display:block;width:100vw;height:100vh"
    ></hyperframes-player>
    <script>
      document.getElementById("p").setAttribute("src", "./index.html" + location.search);
    </script>
  </body>
</html>

README.md (universal -- swap <project-name>)

# <project-name>

A HyperFrames video composition. Plain HTML + GSAP; rendered to MP4 by the `hyperframes` CLI.

## Requirements

- **Node.js 22+** -- [nodejs.org](https://nodejs.org/)
- **FFmpeg** -- `brew install ffmpeg` (macOS) or `sudo apt install ffmpeg` (Debian/Ubuntu) or [ffmpeg.org/download](https://ffmpeg.org/download.html) (Windows)

Verify: `npx hyperframes doctor`

## Preview

```bash
npx hyperframes preview
```

Opens the HyperFrames Studio at `http://localhost:3002` with frame-accurate scrubbing.

## Refine with Claude Code

This project was drafted in Claude Design. To polish animations, timing, and pacing:

```bash
npx skills add heygen-com/hyperframes   # install HyperFrames skills (one-time)
npx hyperframes lint                     # verify structure (should pass with zero errors)
npx hyperframes preview                  # open the studio for live feedback
```

Then open in Claude Code and iterate:

- "Make scene 3's entrance snappier"
- "Add a counter animation to the stat in scene 5"
- "Tighten the pacing -- scenes 4 and 6 feel too long"
- "Change the shader on transition 3 to glitch"

## Render

Run from the project directory (the folder containing `index.html`). The
positional argument is the project directory, not a file, so pass a specific
composition with `-c` rather than as a bare path.

```bash
npx hyperframes render -o output.mp4
```

1920x1080 / 30fps by default. Use `--fps 60` or `--resolution 4k` to override.

Skeleton A -- Social Reel (1080x1920, 15s, 6 scenes)

Transition plan: s1→s2 hard cut, s2→s3 hard cut, s3→s4 SHADER (hero reveal), s4→s5 hard cut, s5→s6 hard cut. One shader at the midpoint.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=1080, height=1920" />
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@hyperframes/core/dist/hyperframe.runtime.iife.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@hyperframes/shader-transitions/dist/index.global.js"></script>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <!-- FILL: Google Fonts link for your chosen typefaces -->
    <link
      href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;500;700&family=JetBrains+Mono:wght@400&display=swap"
      rel="stylesheet"
    />
    <style>
      :root {
        /* === FILL: Your brand identity === */
        --bg: #0a0a0d;
        --ink: #f5f5f7;
        --accent: #7c6cff;
        --muted: #5a6270;
        --accent-dim: #3d3680;
        --font-display: "Space Grotesk", sans-serif;
        --font-data: "JetBrains Mono", monospace;
      }

      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      html,
      body {
        width: 1080px;
        height: 1920px;
        overflow: hidden;
        background: var(--bg);
        color: var(--ink);
      }

      .scene {
        position: absolute;
        top: 0;
        left: 0;
        width: 1080px;
        height: 1920px;
        overflow: hidden;
      }
      .scene-content {
        width: 100%;
        height: 100%;
        padding: 120px 80px;
        display: flex;
        flex-direction: column;
        justify-content: center;
        gap: 24px;
        box-sizing: border-box;
        position: relative;
        z-index: 1;
      }
      .clip {
      }

      .display {
        font-family: var(--font-display);
        font-weight: 700;
        line-height: 1.1;
      }
      .body-text {
        font-family: var(--font-display);
        font-weight: 300;
        line-height: 1.4;
        color: var(--muted);
      }
      .data-text {
        font-family: var(--font-data);
        font-weight: 400;
        font-variant-numeric: tabular-nums;
      }

      .grain {
        position: absolute;
        inset: 0;
        pointer-events: none;
        z-index: 50;
        opacity: 0.18;
        background-image:
          radial-gradient(rgba(255, 255, 255, 0.08) 1px, transparent 1.2px),
          radial-gradient(rgba(0, 0, 0, 0.18) 1px, transparent 1.2px);
        background-size:
          3px 3px,
          5px 5px;
        background-position:
          0 0,
          1px 2px;
        mix-blend-mode: overlay;
      }

      /* === FILL: Per-scene styles below === */
    </style>
  </head>
  <body>
    <div
      id="main"
      data-composition-id="main"
      data-width="1080"
      data-height="1920"
      data-start="0"
      data-duration="15"
    >
      <!-- SCENE 1 -- visible from t=0 -->
      <div class="scene clip" id="s1" data-start="0" data-duration="2.5" data-track-index="0">
        <div class="grain"></div>
        <div class="scene-content">
          <!-- FILL: Scene 1 — hook / opener -->
        </div>
      </div>

      <div
        class="scene clip"
        id="s2"
        data-start="2.5"
        data-duration="2.5"
        data-track-index="0"
        style="visibility:hidden;"
      >
        <div class="grain"></div>
        <div class="scene-content">
          <!-- FILL: Scene 2 — build / context -->
        </div>
      </div>

      <!-- SCENE 3 -- SHADER ANCHOR (opacity:0, HyperShader manages) -->
      <div
        class="scene clip"
        id="s3"
        data-start="5"
        data-duration="2.5"
        data-track-index="0"
        style="opacity:0;"
      >
        <div class="grain"></div>
        <div class="scene-content">
          <!-- FILL: Scene 3 — build-up before hero -->
        </div>
      </div>

      <!-- SCENE 4 -- SHADER ANCHOR (opacity:0, HyperShader manages) -->
      <div
        class="scene clip"
        id="s4"
        data-start="7.5"
        data-duration="2.5"
        data-track-index="0"
        style="opacity:0;"
      >
        <div class="grain"></div>
        <div class="scene-content">
          <!-- FILL: Scene 4 — hero / key stat (shader reveals this) -->
        </div>
      </div>

      <div
        class="scene clip"
        id="s5"
        data-start="10"
        data-duration="2.5"
        data-track-index="0"
        style="visibility:hidden;"
      >
        <div class="grain"></div>
        <div class="scene-content">
          <!-- FILL: Scene 5 — proof -->
        </div>
      </div>

      <div
        class="scene clip"
        id="s6"
        data-start="12.5"
        data-duration="2.5"
        data-track-index="0"
        style="visibility:hidden;"
      >
        <div class="grain"></div>
        <div class="scene-content">
          <!-- FILL: Scene 6 — CTA / close -->
        </div>
      </div>
    </div>

    <script>
      window.__timelines = window.__timelines || {};
      var tl = gsap.timeline({ paused: true });

      // --- Non-anchor scene toggles (REQUIRED — use autoAlpha) ---
      tl.set("#s1", { autoAlpha: 0 }, 2.5);
      tl.set("#s2", { autoAlpha: 1 }, 2.5);
      tl.set("#s2", { autoAlpha: 0 }, 5.0);
      // s3, s4 are shader anchors — HyperShader manages their opacity
      tl.set("#s3", { opacity: 1 }, 5.0); // first anchor must be explicitly shown
      tl.set("#s5", { autoAlpha: 1 }, 10.0);
      tl.set("#s5", { autoAlpha: 0 }, 12.5);
      tl.set("#s6", { autoAlpha: 1 }, 12.5);

      // === SCENE 1 (0-2.5s) — hook ===
      // FILL: entrance + mid-scene activity (use 2+ patterns from Section 8)

      // === SCENE 2 (2.5-5s) ===
      // FILL: entrance + mid-scene activity

      // === SCENE 3 (5-7.5s) — SHADER ANCHOR, no exit tweens ===
      // FILL: entrance + mid-scene activity

      // === SCENE 4 (7.5-10s) — hero (shader reveals this) ===
      // FILL: entrance + mid-scene activity

      // === SCENE 5 (10-12.5s) — proof ===
      // FILL: entrance + mid-scene activity

      // === SCENE 6 (12.5-15s) — CTA, final scene, exit OK ===
      // FILL: entrance + mid-scene activity + optional exit

      // --- Shader: 1 transition at the hero reveal ---
      window.HyperShader.init({
        bgColor:
          getComputedStyle(document.documentElement).getPropertyValue("--bg").trim() || "#0a0a0d",
        scenes: ["s3", "s4"],
        timeline: tl,
        transitions: [{ time: 7.25, shader: "cinematic-zoom", duration: 0.5 }],
      });

      window.__timelines["main"] = tl;
    </script>
  </body>
</html>

Skeleton B -- Launch Teaser (1920x1080, 25s, 8 scenes)

Transition plan: s1→s2 hard cut, s2→s3 hard cut, s3→s4 hard cut, s4→s5 SHADER (hero reveal), s5→s7 SHADER (energy shift, s6 plays as runtime-managed interstitial), s7→s8 SHADER (CTA landing). 3 shaders out of 7 cuts.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=1920, height=1080" />
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@hyperframes/core/dist/hyperframe.runtime.iife.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@hyperframes/shader-transitions/dist/index.global.js"></script>
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <!-- FILL: Google Fonts link -->
    <link
      href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;500;700&family=JetBrains+Mono:wght@400&display=swap"
      rel="stylesheet"
    />
    <style>
      :root {
        /* === FILL: Your brand identity === */
        --bg: #0a0a0d;
        --ink: #f5f5f7;
        --accent: #7c6cff;
        --muted: #5a6270;
        --accent-dim: #3d3680;
        --font-display: "Space Grotesk", sans-serif;
        --font-data: "JetBrains Mono", monospace;
      }

      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      html,
      body {
        width: 1920px;
        height: 1080px;
        overflow: hidden;
        background: var(--bg);
        color: var(--ink);
      }

      .scene {
        position: absolute;
        top: 0;
        left: 0;
        width: 1920px;
        height: 1080px;
        overflow: hidden;
      }
      .scene-content {
        width: 100%;
        height: 100%;
        padding: 100px 160px;
        display: flex;
        flex-direction: column;
        justify-content: center;
        gap: 24px;
        box-sizing: border-box;
        position: relative;
        z-index: 1;
      }
      .clip {
      }

      .display {
        font-family: var(--font-display);
        font-weight: 700;
        line-height: 1.1;
      }
      .body-text {
        font-family: var(--font-display);
        font-weight: 300;
        line-height: 1.4;
        color: var(--muted);
      }
      .data-text {
        font-family: var(--font-data);
        font-weight: 400;
        font-variant-numeric: tabular-nums;
      }

      .grain {
        position: absolute;
        inset: 0;
        pointer-events: none;
        z-index: 50;
        opacity: 0.18;
        background-image:
          radial-gradient(rgba(255, 255, 255, 0.08) 1px, transparent 1.2px),
          radial-gradient(rgba(0, 0, 0, 0.18) 1px, transparent 1.2px);
        background-size:
          3px 3px,
          5px 5px;
        background-position:
          0 0,
          1px 2px;
        mix-blend-mode: overlay;
      }

      .vignette {
        position: absolute;
        inset: 0;
        pointer-events: none;
        z-index: 49;
        background: radial-gradient(ellipse at center, transparent 50%, rgba(0, 0, 0, 0.4) 100%);
      }

      /* === FILL: Per-scene styles below === */
    </style>
  </head>
  <body>
    <div
      id="main"
      data-composition-id="main"
      data-width="1920"
      data-height="1080"
      data-start="0"
      data-duration="25"
    >
      <!-- SCENE 1 -- visible from t=0 -->
      <div class="scene clip" id="s1" data-start="0" data-duration="3" data-track-index="0">
        <div class="grain"></div>
        <div class="vignette"></div>
        <div class="scene-content"><!-- FILL: hook --></div>
      </div>

      <!-- s2-s3: hard cuts, runtime-managed -->
      <div
        class="scene clip"
        id="s2"
        data-start="3"
        data-duration="3"
        data-track-index="0"
        style="visibility:hidden;"
      >
        <div class="grain"></div>
        <div class="vignette"></div>
        <div class="scene-content"><!-- FILL: context --></div>
      </div>

      <div
        class="scene clip"
        id="s3"
        data-start="6"
        data-duration="3"
        data-track-index="0"
        style="visibility:hidden;"
      >
        <div class="grain"></div>
        <div class="vignette"></div>
        <div class="scene-content"><!-- FILL: build --></div>
      </div>

      <!-- s4-s5: SHADER ANCHOR pair (hero reveal) -->
      <div
        class="scene clip"
        id="s4"
        data-start="9"
        data-duration="3.5"
        data-track-index="0"
        style="opacity:0;"
      >
        <div class="grain"></div>
        <div class="vignette"></div>
        <div class="scene-content"><!-- FILL: build-up before hero --></div>
      </div>

      <div
        class="scene clip"
        id="s5"
        data-start="12.5"
        data-duration="3"
        data-track-index="0"
        style="opacity:0;"
      >
        <div class="grain"></div>
        <div class="vignette"></div>
        <div class="scene-content"><!-- FILL: hero / key feature --></div>
      </div>

      <!-- s6: hard cut, runtime-managed -->
      <div
        class="scene clip"
        id="s6"
        data-start="15.5"
        data-duration="3"
        data-track-index="0"
        style="visibility:hidden;"
      >
        <div class="grain"></div>
        <div class="vignette"></div>
        <div class="scene-content"><!-- FILL: proof / social proof --></div>
      </div>

      <!-- s7-s8: SHADER ANCHOR pair (CTA landing) -->
      <div
        class="scene clip"
        id="s7"
        data-start="18.5"
        data-duration="3"
        data-track-index="0"
        style="opacity:0;"
      >
        <div class="grain"></div>
        <div class="vignette"></div>
        <div class="scene-content"><!-- FILL: build to close --></div>
      </div>

      <div
        class="scene clip"
        id="s8"
        data-start="21.5"
        data-duration="3.5"
        data-track-index="0"
        style="opacity:0;"
      >
        <div class="grain"></div>
        <div class="vignette"></div>
        <div class="scene-content"><!-- FILL: CTA / close --></div>
      </div>
    </div>

    <script>
      window.__timelines = window.__timelines || {};
      var tl = gsap.timeline({ paused: true });

      // --- Non-anchor scene visibility toggles (REQUIRED) ---
      tl.set("#s1", { autoAlpha: 0 }, 3.0);
      tl.set("#s2", { autoAlpha: 1 }, 3.0);
      tl.set("#s2", { autoAlpha: 0 }, 6.0);
      tl.set("#s3", { autoAlpha: 1 }, 6.0);
      tl.set("#s3", { autoAlpha: 0 }, 9.0);

      // --- First shader anchor must be explicitly shown ---
      tl.set("#s4", { opacity: 1 }, 9.0);

      // s4, s5 are shader anchors — HyperShader manages their opacity after transitions
      tl.set("#s6", { autoAlpha: 1 }, 15.5);
      tl.set("#s6", { autoAlpha: 0 }, 18.5);

      // --- Second shader group's first anchor must also be shown ---
      tl.set("#s7", { opacity: 1 }, 18.5);
      // s7, s8 are shader anchors — HyperShader manages their opacity after transitions

      // === SCENE 1 (0-3s) — hook ===

      // === SCENE 2 (3-6s) — hard cut ===

      // === SCENE 3 (6-9s) — hard cut ===

      // === SCENE 4 (9-12.5s) — SHADER ANCHOR, no exit tweens ===

      // === SCENE 5 (12.5-15.5s) — shader from s4, hero reveal ===

      // === SCENE 6 (15.5-18.5s) — hard cut ===

      // === SCENE 7 (18.5-21.5s) — SHADER ANCHOR, no exit tweens ===

      // === SCENE 8 (21.5-25s) — shader from s7, CTA. Final, exit OK ===

      // --- Shader transitions: 2 key moments ---
      // s4->s5 (hero reveal) and s7->s8 (CTA landing)
      // HyperShader requires consecutive anchors, so we use two groups:
      // Group 1: [s4, s5] with 1 transition
      // Group 2: [s7, s8] with 1 transition
      // But HyperShader only supports one init() call, so we chain them:
      // [s4, s5] — shader — then runtime hard-cuts s5->s6->s7 — then [s7, s8]
      // To satisfy the invariant with one init(), we include s5->s7 gap scenes.
      // Simplest: just use one contiguous anchor block [s4, s5, s7, s8] with
      // the s5->s7 transition as a real (visible) shader too. This gives you
      // 3 shaders total — still well under the "every cut" anti-pattern.
      window.HyperShader.init({
        bgColor:
          getComputedStyle(document.documentElement).getPropertyValue("--bg").trim() || "#0a0a0d",
        scenes: ["s4", "s5", "s7", "s8"],
        timeline: tl,
        transitions: [
          { time: 12.25, shader: "cinematic-zoom", duration: 0.5 },
          { time: 15.25, shader: "light-leak", duration: 0.5 },
          { time: 21.25, shader: "cross-warp-morph", duration: 0.5 },
        ],
      });

      window.__timelines["main"] = tl;
    </script>
  </body>
</html>

Skeleton C -- Product Explainer (1920x1080, 45s, 12 scenes)

Use the same structure as Skeleton B but with 12 scene divs (s1-s12), data-duration totaling 45s, and 11 transitions. Adjust scene durations: mix 3s, 3.5s, 4s, and 5s scenes based on content density. Include a scene rhythm like: 3-3-4-3.5-4-5-3.5-4-3.5-4-4-3.5.

Skeleton D -- Cinematic Title (1920x1080, 60s, 7 scenes)

Use the same structure with 7 scene divs (s1-s7), longer durations (6-10s each), fewer transitions (6), and more restrained shaders (cross-warp-morph, thermal-distortion). Scene rhythm: 8-7-8-10-9-10-8.


Section 8: Common animation patterns

Copy-paste these. They appear in every production composition.

Counter animation

var counterObj = { v: 0 };
tl.to(
  counterObj,
  {
    v: 1900000000000,
    duration: 2.0,
    ease: "power2.out",
    onUpdate: function () {
      document.getElementById("s3-stat").textContent = "$" + (counterObj.v / 1e12).toFixed(1) + "T";
    },
  },
  10.5,
);

SVG stroke draw

<svg viewBox="0 0 400 200" style="position:absolute; bottom:100px; left:160px;">
  <path
    id="s2-line"
    d="M 0 100 Q 200 20 400 100"
    stroke="var(--accent)"
    stroke-width="3"
    fill="none"
    stroke-linecap="round"
    stroke-dasharray="440"
    stroke-dashoffset="440"
  />
</svg>
tl.to("#s2-line", { strokeDashoffset: 0, duration: 1.0, ease: "power2.out" }, 3.5);

Character stagger

<h1 class="display" style="font-size:120px;">
  <span class="char">N</span><span class="char">O</span><span class="char">R</span>
  <span class="char">T</span><span class="char">H</span>
</h1>
tl.from(
  ".char",
  {
    y: 60,
    autoAlpha: 0,
    duration: 0.5,
    ease: "power3.out",
    stagger: { each: 0.12, from: "start" },
  },
  29.5,
);

Breathing float (mid-scene activity)

tl.to(
  "#s4-logo",
  {
    y: -5,
    duration: 1.5,
    ease: "sine.inOut",
    yoyo: true,
    repeat: 1,
  },
  15.0,
);

Bar chart fill

["#bar1", "#bar2", "#bar3", "#bar4"].forEach(function (sel, i) {
  tl.from(
    sel,
    {
      scaleY: 0,
      transformOrigin: "bottom",
      duration: 0.6,
      ease: "expo.out",
    },
    11.0 + i * 0.15,
  );
});

Orbit / rotation

tl.to(
  "#orbit-dot",
  {
    rotation: 360,
    duration: 3.0,
    ease: "none",
    transformOrigin: "50% 200px",
  },
  8.5,
);

Highlight sweep (background-size animation)

#s5-headline {
  background: linear-gradient(var(--accent), var(--accent)) no-repeat 0 85% / 0% 30%;
}
tl.to("#s5-headline", { backgroundSize: "100% 30%", duration: 0.6, ease: "power2.out" }, 22.0);

CSS radial-gradient grain (safe for Safari + Claude Design iframe)

.grain {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 50;
  opacity: 0.18;
  background-image:
    radial-gradient(rgba(255, 255, 255, 0.08) 1px, transparent 1.2px),
    radial-gradient(rgba(0, 0, 0, 0.18) 1px, transparent 1.2px);
  background-size:
    3px 3px,
    5px 5px;
  background-position:
    0 0,
    1px 2px;
  mix-blend-mode: overlay;
}

NEVER use SVG filter data:image/svg+xml grain -- it taints html2canvas in Safari, breaking every shader transition in Claude Design's cross-origin iframe.


References (fetch only when needed)

Everything critical is inlined above. These are for edge cases: