chore: import upstream snapshot with attribution
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: hyperframes-core
|
||||
description: The HyperFrames composition contract — build one renderable project. Use for composition structure, the `data-*` timing attributes, `class="clip"`, tracks, sub-compositions, variables, framework-owned media playback, deterministic-render rules, and validation. Also covers Tailwind projects and the STORYBOARD.md / SCRIPT.md plan formats. Read before writing composition HTML.
|
||||
---
|
||||
|
||||
# HyperFrames Core
|
||||
|
||||
HyperFrames renders video from HTML. A composition is an HTML file whose DOM declares timing with `data-*` attributes, whose animation runtime is seekable, and whose media playback is owned by the framework.
|
||||
|
||||
This skill is the **technical contract** — how to build one hyperframes project. The body below is the build guide; per-topic detail lives in `references/` (index next), read on demand. Other concerns live in the sibling domain skills — `hyperframes-animation`, `hyperframes-creative`, `media-use`, `hyperframes-cli`, `hyperframes-registry`. The capability map in `/hyperframes` says what each one covers.
|
||||
|
||||
## References
|
||||
|
||||
| File | Read it to… |
|
||||
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
|
||||
| `references/minimal-composition.md` | start from the smallest renderable composition skeleton |
|
||||
| `references/composition-patterns.md` | choose monolithic vs modular; structure a modular `index.html`; pick a sub-comp archetype |
|
||||
| `references/data-attributes.md` | look up any `data-*` (root / clip / sub-comp host / legacy aliases); use `class="clip"` |
|
||||
| `references/tracks-and-clips.md` | pick `data-track-index`, handle same-track overlap / z-index, time a clip relative to another |
|
||||
| `references/sub-compositions.md` | wire a sub-composition (host attrs, `<template>`, per-instance vars) and animate inside it |
|
||||
| `references/variables-and-media.md` | declare variables; place `<video>`/`<audio>`, set volume, trim |
|
||||
| `references/determinism-rules.md` | build a seekable timeline; determinism bans; the animatable-property allowlist; layout / text fit |
|
||||
| `references/full-screen-motion.md` | author full-frame motion with shared backgrounds |
|
||||
| `references/storyboard-format.md` | author a `STORYBOARD.md` plan (+ the parsed manifest) |
|
||||
| `references/brief-contract.md` | conduct a creation workflow's intake — interaction mode (collaborative / autonomous), shared brief fields, asking rules |
|
||||
| `references/script-format.md` | author the optional `SCRIPT.md` locked narration |
|
||||
| `references/subagent-dispatch.md` | map subagent dispatch verbs (parallel fan-out / background / wait) to your harness |
|
||||
| `references/tailwind.md` | work in a Tailwind v4 project (`init --tailwind`; runtime contract differs from Studio's v3) |
|
||||
|
||||
For animation runtime specifics (GSAP API, Lottie, Three.js, etc.) go to `hyperframes-animation` → `adapters/<runtime>.md`.
|
||||
|
||||
## Building a composition
|
||||
|
||||
### Two root forms (not interchangeable)
|
||||
|
||||
- **Standalone** (top-level `index.html`) — root `<div data-composition-id="…">` sits directly in `<body>`, **no `<template>` wrapper** (wrapping it hides all content and breaks rendering).
|
||||
- **Sub-composition** (loaded via `data-composition-src`) — root **must** be wrapped in `<template>`.
|
||||
|
||||
> ⚠ Transport rule: the runtime **only clones `<template>` contents**; everything outside (incl. `<head>` styles/scripts) is discarded — put `<style>`/`<script>` **inside** the template.
|
||||
> ⚠ Host-id rule: the host slot's `data-composition-id` must **exactly equal** the inner template's `data-composition-id` **and** the `window.__timelines["<id>"]` key — no `-mount`/`-slot`/`-host` suffix.
|
||||
|
||||
File shape, host wiring, and the pre-render checklist → `references/sub-compositions.md`.
|
||||
|
||||
### Root must be sized (silent layout bug)
|
||||
|
||||
The standalone root needs an explicit **sized box** (`width`/`height` in px), and every ancestor down to a `height:100%` element must have a resolved height — otherwise a flex/`100%` child collapses to ~0 and content piles into the top-left corner. `lint`/`validate`/`inspect` do **not** catch this. Skeleton → `references/minimal-composition.md`.
|
||||
|
||||
### One paused timeline
|
||||
|
||||
Each composition registers **exactly one** `gsap.timeline({ paused: true })` at `window.__timelines["<id>"]` (key = root `data-composition-id`), built **synchronously** at page load. Render duration = root `data-duration`, not timeline length. Don't manually nest sub-timelines into the host. Full contract (incl. non-GSAP runtimes) → `references/determinism-rules.md` + `hyperframes-animation/adapters/`.
|
||||
|
||||
### Non-negotiable rules (silent bugs `lint`/`validate`/`inspect` won't catch)
|
||||
|
||||
Surfaced here; full rationale in the linked reference. Do not violate:
|
||||
|
||||
- No render-time clocks / unseeded `Math.random` / network / input-state; no `repeat: -1` (use a finite count). → `determinism-rules.md`
|
||||
- Animate only the visual-property allowlist; never `display`/`visibility`; no `gsap.set` on later-scene clips. → `determinism-rules.md`
|
||||
- No `<br>` in body text; transformed elements must be block-level + sized; pulsing absolute decoratives need peak clearance. → `determinism-rules.md`
|
||||
- `<video>`/`<audio>` must be a **direct child of the host root** (never inside a sub-comp `<template>`/wrapper); the framework owns playback. → `variables-and-media.md`
|
||||
- Every `id` must be unique across the **assembled** page; inside a sub-comp, prefix ids with the composition id (`#<id>-hero`). Duplicate `<video>`/`<img>` ids render **blank** — the producer injects frames by `getElementById`, and cross-file dupes slip past `lint`. → `composition-patterns.md`
|
||||
- A full-screen scene fill goes on a full-bleed **child** (`position:absolute; inset:0`), never on the composition root itself — the producer's frame compositing can drop the root element's own `background` (the frame renders **black**) even though preview/`snapshot` show it correctly. → `composition-patterns.md`
|
||||
|
||||
## Editing existing compositions
|
||||
|
||||
- Read the files first. Preserve unrelated timing, tracks, IDs, variables, media paths.
|
||||
- Match existing composition IDs and timeline keys.
|
||||
- Adding a clip: pick a non-overlapping `data-track-index` or adjust surrounding timing intentionally.
|
||||
- `data-hidden` on any composition element hides it in BOTH preview and render, overriding its time window; it is non-destructive/reversible and toggled by Studio's timeline eye icon.
|
||||
- Adding a sub-composition: verify its internal `data-composition-id` before wiring the host.
|
||||
|
||||
## Validation
|
||||
|
||||
Use `hyperframes-cli` for command details
|
||||
|
||||
- [ ] `npx hyperframes check` passes (0 findings across lint, runtime, layout, motion, and contrast)
|
||||
- [ ] Projects with sub-compositions: `npx hyperframes snapshot --at <midpoints>` and eyeball each frame
|
||||
- [ ] `npx hyperframes preview` for review (the user can edit anything in Studio's timeline)
|
||||
- [ ] `npx hyperframes render` only after the user approves
|
||||
@@ -0,0 +1,48 @@
|
||||
# Brief contract — interaction mode, shared brief fields, and question rules
|
||||
|
||||
Every creation workflow runs its intake step (Step 0 / brief) against this contract. It defines three things: the **interaction mode** (which controls all later gates, not just the brief), the **shared brief fields**, and the **question rules**. Each workflow maps these fields to its own values in its SKILL.md — including its enums, recommendation logic, and extra inputs. This file never includes workflow-specific content. Workflows without a real brief, such as `/motion-graphics`, use only § 1.
|
||||
|
||||
## 1. Interaction mode
|
||||
|
||||
There are two modes. Default: **collaborative**.
|
||||
|
||||
**Signals.**
|
||||
|
||||
- **Ongoing autonomous signals** — "autonomous", "surprise me", "decide for me", "just build it", "don't ask, just go", "LFG": the whole flow switches to autonomous from this point on.
|
||||
- **One-time acceptance** — a bare "go" / "looks good" at a gate accepts only that gate's defaults; the mode does not change.
|
||||
- The mode is set **once** — by a signal in the request, or by the brief's first question (§ 3) — and **carries forward. No later step asks again.** Once a storyboard exists, record it in `STORYBOARD.md` frontmatter (`mode: autonomous`). When resuming an existing project, read `mode` from that frontmatter first — a recorded mode counts as already set, so don't ask again.
|
||||
- **Mid-run switch**: "stop asking / just finish it" → autonomous for the rest of the run. Clear feedback on a heads-up → collaborative resumes at the next gate.
|
||||
|
||||
**Gate types.** Autonomous mode changes only the first two types:
|
||||
|
||||
1. **Preference gates** (which preset, voice, caption identity, want a preview?) — autonomous: decide yourself and state the decision with a one-line reason. Never stay silent.
|
||||
2. **Checkpoint gates** (storyboard approval, pre-render review) — autonomous: post the same summary you would have asked about as an inline heads-up, then continue. One exception: before rendering, ask once — preview first, or render (§ 3).
|
||||
3. **Quality gates** (`hyperframes check`, capture completeness, fetch failures, workflow-specific verification checklists) — never skip these in any mode. Errors still stop the run. Reasoning like "autonomous means bias toward action, so I'll skip verification" misuses the mode — bias toward action applies to deciding _what to build_, not _whether to verify_.
|
||||
4. **Routing and sign-in decisions** — wrong routing is a quality problem: an ambiguous-intent confirmation, such as `/slideshow`'s "is this a deck?", still happens in autonomous mode. Auth sign-in follows `/media-use` → Preflight: show the status as-is; collaborative waits for the user's choice, while autonomous notes it and continues offline.
|
||||
|
||||
**Autonomous is not silent.** Every question absorbed by the mode becomes a decision with a receipt — state the choice and its one-line reason inline as you go. Final delivery always includes the contact sheet, so review happens after the fact instead of not happening at all.
|
||||
|
||||
## 2. Field registry
|
||||
|
||||
The shared brief fields. Each workflow's SKILL.md declares which fields it uses, its own value set, how it derives recommendations, and — decisively — marks each field **ask** (always gets its own question) or **state** (stated in the intro text, never asked). The binding table's ask/state marking is authoritative; the default policies below apply only when a binding doesn't say otherwise. If a workflow does not use a field, such as `/music-to-video` having no narration, that field is simply absent from its binding — don't ask about it.
|
||||
|
||||
| Field | Meaning | Default policy |
|
||||
| ------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `mode` | collaborative / autonomous (§ 1) | detect signals; never ask again |
|
||||
| `destination` | where the video will play (X / LinkedIn feed, YouTube, TikTok, embed) | infer from the request; if unknown **and** it would change aspect or type scale, include ONE question in the brief |
|
||||
| `aspect` | canvas | derive from destination — social **feed** (X / LinkedIn / Instagram) → square `1080x1080`; TikTok / Reels / Shorts → `1080x1920`; YouTube / website embed / unknown-desktop → `1920x1080`. State the derivation; never ask twice |
|
||||
| `length` | target duration | the workflow derives its own recommendation and states the reason |
|
||||
| `language` | narration + captions | use the user's language — state it, don't ask |
|
||||
| `audience` | who will watch | infer from the input; confirm only when it would change the beats |
|
||||
| `message` | the ONE thing the video must communicate | derive it and echo it in the brief — if the message cannot be stated in one sentence, the video is not ready for storyboarding |
|
||||
| `angle` | what kind of story (workflow enum) | workflow-specific values; recommend one with a receipt |
|
||||
| `narration` | yes / minimal / no (+ workflow slots such as `VO_MODE`) | workflow-specific |
|
||||
|
||||
## 3. Question rules
|
||||
|
||||
The executable question script lives in each workflow's Step 0 as a literal two-round template. This section defines only the invariants that script satisfies:
|
||||
|
||||
- **Round 1 asks the mode** — one question, skipped when the request already carried a signal. Autonomous → no further questions: state the locked brief (all fields + receipts) as a heads-up and build straight through; the one remaining question, before render, is "preview first, or render?". Collaborative → Round 2.
|
||||
- **Round 2 asks the workflow's ask-marked fields** — one question per field, recommended option first, each with its receipt. Skip a question only when the user already answered that field in their request; inference is not an answer.
|
||||
- **Receipts.** Every recommended option states its basis — "~40s — small change, +44/−13 across 12 files"; "square 1:1 — you named the X/LinkedIn feed as the destination".
|
||||
- **Channel.** Native question UI when the environment has one; otherwise plain text as one numbered list. "go" accepts all defaults.
|
||||
@@ -0,0 +1,263 @@
|
||||
# Composition Patterns
|
||||
|
||||
How to architect a project — when to inline everything in one HTML, when to split into sub-compositions, what the `index.html` orchestrator looks like at scale, and the common sub-composition archetypes seen in real projects. Pair with `minimal-composition.md` (single-file shape) and `sub-compositions.md` (mechanics of a sub-comp file).
|
||||
|
||||
## Two Architectures
|
||||
|
||||
| | Monolithic (single file) | Modular (sub-compositions) |
|
||||
| --------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| Project layout | `index.html` only | `index.html` + `compositions/<scene>.html` per scene |
|
||||
| Where scenes live | Inline `<section class="clip">` siblings under the root | Each scene is a separate file wrapped in `<template>` |
|
||||
| Timeline registration | One timeline keyed at the root's `data-composition-id` | Root timeline (often near-empty) + one timeline per sub-comp, each keyed by its `id` |
|
||||
| Routing entry | `references/minimal-composition.md` | `references/sub-compositions.md` |
|
||||
|
||||
Both architectures use the same runtime contract — `data-*` attributes + `window.__timelines[id]`. The choice is structural, not behavioral.
|
||||
|
||||
### Pick monolithic when
|
||||
|
||||
- The whole video is one continuous scene with no hard cuts.
|
||||
- Scenes share heavy state (one canvas/WebGL context spanning the whole video, a single SVG that morphs across all beats).
|
||||
- Total scope is small (~200–400 lines of markup + script).
|
||||
- No scene is reused across projects.
|
||||
|
||||
### Pick modular when
|
||||
|
||||
- The video has clear scene cuts — each scene is its own segment of the timeline.
|
||||
- Some scenes are large (>100 lines of markup or significant scripted animation).
|
||||
- A scene is reusable (kinetic intro, end-card logo lockup, a transition).
|
||||
- The video has a continuous audio track over multiple visual segments. Keep audio at the root, visual segments as sub-comps.
|
||||
- You want to author/iterate on scenes in isolation (preview a single sub-comp file directly).
|
||||
|
||||
### Refactor between them
|
||||
|
||||
Conversion is mechanical and reversible. To lift a monolithic scene into a sub-comp: wrap the scene's markup + scoped CSS + its slice of the parent timeline into a `<template>`, save as `compositions/<scene>.html`, replace the inline content in `index.html` with a slot `<div data-composition-src="compositions/<scene>.html">`, and have the sub-comp register its own timeline at `window.__timelines["<scene>"]`. The parent timeline shrinks accordingly.
|
||||
|
||||
If a monolithic project is approaching three or more scene cuts, prefer modularizing _before_ adding the next scene. Mixed projects where some scenes are inline and siblings are in `compositions/` are the hardest to maintain.
|
||||
|
||||
## Modular Orchestrator Pattern
|
||||
|
||||
When using sub-compositions, `index.html` should be **thin**. Its job is to declare slots, lay them out in time, mount the audio track, and register a (usually empty) root timeline. All scene animation lives inside the sub-comps.
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #000;
|
||||
}
|
||||
#root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* Sub-comp slots stretch to fill the root. */
|
||||
[data-composition-id="root"] > div[data-composition-src] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="root"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="30"
|
||||
>
|
||||
<!-- Sequential scenes — each one a sub-composition slot. -->
|
||||
<div
|
||||
id="el-intro"
|
||||
data-composition-id="intro"
|
||||
data-composition-src="compositions/intro.html"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-track-index="1"
|
||||
></div>
|
||||
|
||||
<div
|
||||
id="el-body"
|
||||
data-composition-id="body"
|
||||
data-composition-src="compositions/body.html"
|
||||
data-start="6"
|
||||
data-duration="18"
|
||||
data-track-index="1"
|
||||
></div>
|
||||
|
||||
<div
|
||||
id="el-outro"
|
||||
data-composition-id="outro"
|
||||
data-composition-src="compositions/outro.html"
|
||||
data-start="24"
|
||||
data-duration="6"
|
||||
data-track-index="1"
|
||||
></div>
|
||||
|
||||
<!-- Continuous audio at the root — survives scene cuts. -->
|
||||
<audio
|
||||
id="el-bgm"
|
||||
src="assets/bgm.mp3"
|
||||
data-start="0"
|
||||
data-duration="30"
|
||||
data-track-index="10"
|
||||
data-volume="0.6"
|
||||
></audio>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["root"] = gsap.timeline({ paused: true });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Key properties of this layout:
|
||||
|
||||
- **Visual scenes on the same `data-track-index`** (e.g. `1`). Sequential — they cannot overlap on the same track. For a cross-fade between two scenes, put one on a higher track and overlap their times by the fade duration.
|
||||
- **Audio on a separate, higher track index** (e.g. `10`). Keeps the linter's overlap rules clear of any visual collisions.
|
||||
- **Root timeline is near-empty.** All animation lives in the sub-comps. A root-level fade-to-black at the very end is fine; do not stage a parallel animation track from the root.
|
||||
- **Host slot ids** use `el-<name>` or `<scene-id>`. The slot's `data-composition-id` must still equal the sub-comp's internal id (see `sub-compositions.md`).
|
||||
|
||||
## Sub-Composition Archetypes
|
||||
|
||||
### A. Content scene (default)
|
||||
|
||||
The sub-comp contains the scene's full DOM, scoped CSS, and timeline. This is the standard pattern in `sub-compositions.md` — most scenes are this.
|
||||
|
||||
### B. Host media + main-timeline driver (REQUIRED for any `<video>`/`<audio>`)
|
||||
|
||||
Media playback only works when the `<video>`/`<audio>` is a **direct child of the host root** — never inside a sub-comp `<template>` (it would render blank/black). This is not optional or "for media that spans scenes"; it applies to every clip, including a scene-specific one. The scene's sub-comp keeps the frame/shell; the media is a host sibling positioned over it.
|
||||
|
||||
A sub-comp timeline **cannot** drive host elements (a global selector or `document.querySelector` does not resolve across the boundary). So author the media's per-scene motion (scale/opacity/morph/tilt/breathing) on the **main timeline** in `index.html`, at **global time** = scene-local time + the scene slot's `data-start`.
|
||||
|
||||
```html
|
||||
<!-- index.html (host) -->
|
||||
<div
|
||||
id="el-final"
|
||||
data-composition-id="final-anim"
|
||||
data-composition-src="compositions/final-anim.html"
|
||||
data-start="20"
|
||||
data-duration="6"
|
||||
data-track-index="1"
|
||||
></div>
|
||||
|
||||
<!-- media is a DIRECT root child; sits over the sub-comp's frame -->
|
||||
<video
|
||||
id="final-video"
|
||||
class="clip"
|
||||
src="assets/final.mp4"
|
||||
data-start="20"
|
||||
data-duration="6"
|
||||
data-track-index="2"
|
||||
muted
|
||||
playsinline
|
||||
style="position:absolute; left:360px; top:100px; width:1200px; height:680px; object-fit:cover; border-radius:24px;"
|
||||
></video>
|
||||
|
||||
<script>
|
||||
// MAIN timeline drives the host video. Global time: scene starts at 20.
|
||||
window.__timelines = window.__timelines || {};
|
||||
const main = window.__timelines["main"];
|
||||
main.fromTo(
|
||||
"#final-video",
|
||||
{ scale: 1.4, filter: "blur(14px)" },
|
||||
{ scale: 1.0, filter: "blur(0px)", duration: 0.9, ease: "power3.out" },
|
||||
20,
|
||||
); // = slot data-start (+ any scene-local offset)
|
||||
</script>
|
||||
|
||||
<!-- compositions/final-anim.html — frame/shell only, no <video>, no host-element animation -->
|
||||
<template>
|
||||
<div
|
||||
data-composition-id="final-anim"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="6"
|
||||
style="position:absolute; inset:0; pointer-events:none;"
|
||||
>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
// animate ONLY this sub-comp's own elements here (labels, frame, overlays)
|
||||
window.__timelines["final-anim"] = tl;
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
Caveats:
|
||||
|
||||
- The host media must be a direct root child and exist in the DOM (static in `index.html`) — it always is.
|
||||
- Clip lifecycle owns the media element's visibility across its `[data-start, data-start+data-duration]` window. The main-timeline opacity/scale tweens compose with it fine; for an opacity reveal/crossfade prefer a host **wrapper** so you are not fighting the lifecycle on the media element itself.
|
||||
- Two media elements sharing the same `src` + `data-start` trigger `duplicate_media_discovery_risk` (benign — both still render).
|
||||
|
||||
### C. Multi-scene merge
|
||||
|
||||
When several beat-level scenes share continuous state — a chat thread that grows, a persistent headline word that carries across the cut, a single canvas with internal phase changes — collapse them into one sub-comp and use **internal phase divs** rather than multiple sub-comp slots.
|
||||
|
||||
```html
|
||||
<!-- compositions/act2-merged.html -->
|
||||
<template>
|
||||
<div data-composition-id="act2-merged" data-width="1920" data-height="1080" data-duration="9">
|
||||
<style>
|
||||
[data-composition-id="act2-merged"] .phase {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
<div class="phase" id="phase-a">…</div>
|
||||
<div class="phase" id="phase-b">…</div>
|
||||
<div class="phase" id="phase-c">…</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
tl.set("#phase-a", { opacity: 1 }, 0);
|
||||
tl.to("#phase-a", { opacity: 0, duration: 0.4 }, 3.0);
|
||||
tl.set("#phase-b", { opacity: 1 }, 3.0);
|
||||
// …
|
||||
window.__timelines["act2-merged"] = tl;
|
||||
</script>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
Reach for this over multiple sequential slots when scenes share DOM, share a canvas, or need to cross-fade with persistent elements (a headline that survives the cut between phases). Each phase is just a div inside the same sub-comp — the parent timeline never has to know about the internal phase boundaries.
|
||||
|
||||
### D. Audio at root, reactive visual inside
|
||||
|
||||
Audio always lives at the host (`index.html`) as a root-level `<audio>` so playback survives scene cuts. A sub-comp that visualizes audio should read a **pre-baked** frequency curve at init, then sample the baked curve from its timeline — the visual must still be a deterministic function of `tl.time()`, not of `audio.currentTime`. See `determinism-rules.md` and `hyperframes-creative` for the authoring pattern.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
| Thing | Convention | Example |
|
||||
| ----------------------------------- | ------------------------------------------- | ------------------------------------------ |
|
||||
| Sub-comp file | `compositions/<scene-id>.html` | `compositions/act0-intro-bell.html` |
|
||||
| Sub-comp `<template>` id (optional) | `<scene-id>-template` | `<template id="act0-intro-bell-template">` |
|
||||
| Sub-comp root `data-composition-id` | `<scene-id>` (must match host slot) | `data-composition-id="act0-intro-bell"` |
|
||||
| Timeline registry key | matches `data-composition-id` | `window.__timelines["act0-intro-bell"]` |
|
||||
| Host slot `id` | `el-<short>` or `<scene-id>` | `id="el-intro"`, `id="act0"` |
|
||||
| Element ids inside a sub-comp | prefix with the scene id | `#act0-bell`, `#b1-tape` |
|
||||
| Audio at root | `data-track-index` well above visual tracks | `10` while visuals use `1` |
|
||||
|
||||
The `-template` suffix on `<template>` is conventional but not required — the runtime extracts contents from whichever `<template>` is in `<body>`, regardless of id. The prefix on inner element ids is the only safeguard against id collisions when multiple sub-comps are mounted into the same host page at once.
|
||||
|
||||
## Editing Existing Projects
|
||||
|
||||
Before adding or modifying scenes, identify which architecture is in use:
|
||||
|
||||
```bash
|
||||
ls compositions/ 2>/dev/null && echo "modular" || echo "monolithic"
|
||||
```
|
||||
|
||||
- In a **monolithic** project, add new scenes as inline `<section class="clip">` elements with a non-overlapping `data-start` and a sensible `data-track-index`, and extend the existing single timeline.
|
||||
- In a **modular** project, match the pattern: add a new file under `compositions/`, add a slot in `index.html`, keep the root timeline thin. Do **not** start inlining new scenes into `index.html` when sibling scenes are sub-comps — the inconsistency is the worst of both worlds.
|
||||
- If a monolithic project needs a third or fourth scene cut, lift each scene into a sub-comp before adding more. The conversion is mechanical (see "Refactor between them" above).
|
||||
|
||||
When picking the slot's `data-start`/`data-duration`, prefer continuing the existing sequencing convention (adjoining starts, deliberate overlaps for cross-fades). Don't introduce a new track index unless you actually need parallel visual layers — most sequential-scene projects use exactly one visual track.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Data Attributes Reference
|
||||
|
||||
Every HyperFrames composition uses `data-*` attributes to declare timing and structure to the framework. This is the full attribute table — pair with `tracks-and-clips.md` for the rules behind `data-track-index`.
|
||||
|
||||
## Composition Root
|
||||
|
||||
Every renderable composition needs one root element:
|
||||
|
||||
| Attribute | Required | Meaning |
|
||||
| ---------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `data-composition-id` | Yes | Unique ID. Must match the animation registry key on `window.__timelines`. |
|
||||
| `data-width` / `data-height` | Yes | Pixel frame size. Common values: `1920x1080`, `1080x1920`, `1080x1080`. |
|
||||
| `data-duration` | Conditional\* | Render duration in seconds (total length / frame count), not the GSAP timeline length. **Read once at compile time, like `data-width` / `data-height`**: a static root `data-duration` is locked before scripts run, so a script (`root.setAttribute("data-duration", ...)`) or a `--variables`-driven value cannot change the render length. To vary length per render, author the root `data-duration` directly. (A clip's `data-duration` is different: re-read from the live DOM, so scripts/variables can drive it.) Only when the root omits `data-duration` does the renderer derive total length from the live DOM / timeline after scripts run. |
|
||||
| `data-fps` | No | Optional frame rate hint. CLI render flags can override output fps. |
|
||||
| `data-composition-variables` | No | JSON array of variable declarations (on `<html>`). See `variables-and-media.md`. |
|
||||
|
||||
\*`data-duration` is optional whenever the runtime can auto-infer duration: a registered GSAP timeline, a finite CSS animation, a finite WAAPI `element.animate()`, or a registered Lottie animation. It is **required** for Three.js (no auto-inference), for infinite/unbounded CSS or WAAPI animations, and for any composition with no GSAP timeline and no animation signal at all. `npx hyperframes lint` enforces this (`root_composition_missing_duration_source`). See `determinism-rules.md` → "Duration Contract For Non-GSAP Runtimes" for the per-runtime breakdown.
|
||||
|
||||
The root should be `position: relative`, have explicit pixel dimensions, and hide overflow unless intentionally composing outside the frame.
|
||||
|
||||
## Clip Attributes
|
||||
|
||||
Timed child elements are clips. **`class="clip"` is required on visible timed elements** (`<div>`, `<img>`, etc.) — without it the runtime keeps the element visible for the whole composition, ignoring `data-start` / `data-duration`. Omit on `<video>` (framework manages visibility directly) and `<audio>` (no visual).
|
||||
|
||||
**Clips must be DIRECT children of the composition root.** A clip nested inside a wrapper `<div>` is not registered — most visibly, a `<video>` in a wrapper is never seeked/decoded and renders black. To wrap/transform a clip, put the wrapper _inside_ the clip, or animate the clip element itself; do not wrap the clip. (`<video>`/`<audio>` additionally must be at the **host** root, never in a sub-comp `<template>` — see `variables-and-media.md`.)
|
||||
|
||||
| Attribute | Required | Meaning |
|
||||
| ------------------ | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | Yes | Stable DOM ID for linting, timeline targets, and debugging. |
|
||||
| `data-start` | Yes | Start time in seconds, or a supported clip-time reference. |
|
||||
| `data-duration` | Required for `div`, `img`, and sub-compositions | Duration in seconds. Video/audio can default to media duration when known. |
|
||||
| `data-track-index` | Yes | Timeline track. Clips on the same track must not overlap. |
|
||||
| `data-media-start` | No | Offset into the media source, in seconds. |
|
||||
| `data-volume` | No | Static audio volume, `0` to `1`, default `1`. For fades, animate `volume` on the timeline instead (see `variables-and-media.md`). |
|
||||
| `data-has-audio` | No (`<video>` only) | `"true"` to declare the video carries an audio track when auto-detection would miss it. |
|
||||
|
||||
**Visibility window is inclusive of both ends.** A clip shows while `start ≤ t ≤ start + duration` — it still renders at exactly `t = start + duration`, so the final frame holds the animation's resolved end state (the runtime does not hide it one frame early). A reveal/entrance that lands on `data-duration` is therefore visible on the last frame; you do not need to finish it _before_ `data-duration` just to guarantee the end state renders. (Climax-dwell guidance in `/hyperframes-animation` is about pacing, not this boundary.)
|
||||
|
||||
## Sub-Composition Host Attributes
|
||||
|
||||
When a clip is a sub-composition host (loads another composition file):
|
||||
|
||||
| Attribute | Required | Meaning |
|
||||
| ---------------------------- | -------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| `data-composition-id` | Yes | The internal composition ID of the loaded file. |
|
||||
| `data-composition-src` | Yes | Path to the sub-composition HTML file. |
|
||||
| `data-width` / `data-height` | Yes | Render dimensions for the sub-composition instance. |
|
||||
| `data-variable-values` | No | Per-instance variable overrides as JSON. See `variables-and-media.md`. |
|
||||
| `data-var-src` | No | Binds the element's `src` to a declared variable id (media/image substitution, authored src = fallback). |
|
||||
| `data-var-text` | No | Binds the element's own text to a scalar variable id; children are preserved. |
|
||||
|
||||
See `sub-compositions.md` for the full wiring pattern.
|
||||
|
||||
## Authoring Hints
|
||||
|
||||
- `id="root"` — template convention used by scaffolds and the transition catalog so CSS can target the composition root with `#root` instead of `[data-composition-id="main"]`. Not required by the runtime, but consistent with the rest of the ecosystem.
|
||||
- `class="clip"` — required runtime visibility marker on visible timed elements (`<div>`, `<img>`, …). See Clip Attributes above.
|
||||
- `data-layout-allow-overflow` — tells `hyperframes check` that overflow on this element (or its descendants) is intentional. Notes:
|
||||
- `inspect` measures `getBoundingClientRect` at sampled timestamps, not rendered pixels — `overflow: hidden` clips the visual but does **not** suppress an `inspect` overflow finding. This attribute is the escape hatch; CSS overflow is not.
|
||||
- Can be set on the composition **root** as well as on any child. When the cited offender is `div.<comp>-root inside div.<comp>-root` (the root reports its own children's union as overflowing), the fix goes on the root, not on individual text descendants — shrinking font sizes will not converge.
|
||||
- In a multi-scene `group_wN.html` (continue runs), every scene-local element stays in the DOM during the other scenes' time windows; the layout-box union almost always overflows the canvas during morph seams. Mark the root and every scene-local primary/supporting element with this attribute **at construction**, not after `inspect` flags it.
|
||||
- **Blast radius — it silences more than `inspect`.** The attribute is inherited down the subtree (the perception probe walks ancestors), so it also suppresses the rendered-perception checks `text-clipping`, `content-cramped-container`, and `foreground-over-panel` for every descendant. Putting it on a persistent panel that also hosts real foreground content disables collision checks on that content for the panel's whole lifetime. Prefer the narrowest opt-out: scope it to the smallest decorative wrapper, or use per-element `data-layout-bleed="true"` for one intentional primary-text crop. The two canvas/edge checks `primary-offscreen` and `foreground-over-panel` deliberately run **even under** allow-overflow, so it cannot hide a wordmark sliced by the frame or text bleeding onto a panel edge.
|
||||
- `data-layout-ignore` — exclude this element from layout audits entirely.
|
||||
|
||||
## Legacy / Removed Attributes
|
||||
|
||||
These names appear in older projects and examples. Use the current names when authoring or editing:
|
||||
|
||||
| Legacy name | Use instead |
|
||||
| ------------ | ------------------ |
|
||||
| `data-layer` | `data-track-index` |
|
||||
| `data-end` | `data-duration` |
|
||||
@@ -0,0 +1,68 @@
|
||||
# Determinism, Animation Runtime, and Layout
|
||||
|
||||
HyperFrames seeks compositions frame-by-frame. Every frame must be reproducible from its time value alone — same input time → same pixels. Three contracts enforce this: the **animation runtime contract**, the **determinism rules**, and the **layout contract**.
|
||||
|
||||
## Animation Runtime Contract
|
||||
|
||||
GSAP is the primary runtime. The core requirement is generic: animation state must be seekable from HyperFrames time.
|
||||
|
||||
For GSAP:
|
||||
|
||||
- Create the timeline **synchronously** during page initialization.
|
||||
- Use `gsap.timeline({ paused: true })`.
|
||||
- Register it on `window.__timelines["<composition-id>"]`.
|
||||
- The key must match `data-composition-id` on the composition root.
|
||||
- **Do not** call `tl.play()` for render-critical motion.
|
||||
- **Do not** build timelines inside `async`, `Promise`, `setTimeout`, or event handlers — the renderer can sample before they finish.
|
||||
- **Do not** create empty tweens only to set duration; use `data-duration` on the clip instead.
|
||||
- **Do not** `gsap.set()` clip elements from later scenes — they are not in the DOM at page load. Use `tl.set(selector, vars, time)` inside the timeline at or after the clip's `data-start`.
|
||||
|
||||
Use the `hyperframes-animation` skill for tween syntax, position parameters, eases, and performance rules.
|
||||
|
||||
### Duration Contract For Non-GSAP Runtimes
|
||||
|
||||
The render engine needs a positive total duration before it will capture a single frame — without one, capture fails outright with "Composition has zero duration." A GSAP timeline supplies this automatically. CSS, WAAPI, and Lottie compositions have no timeline object, so the runtime infers duration itself:
|
||||
|
||||
- **CSS**: longest `animation-delay` + `animation-duration` × finite `animation-iteration-count` across animated elements (offset by each element's `data-start`). `animation-iteration-count: infinite` cannot be inferred.
|
||||
- **WAAPI**: longest `element.animate()` effect's `getComputedTiming().endTime`. Infinite `iterations` cannot be inferred.
|
||||
- **Lottie**: the registered animation's native length (`totalFrames / frameRate`, or the dotLottie player's own `duration`) — always finite regardless of `loop`.
|
||||
- **Three.js**: **not inferable**. The `three` adapter only forwards time via `hf-seek` — it has no `AnimationClip`/`AnimationMixer` inspection.
|
||||
|
||||
`data-duration` on the root `[data-composition-id]` element is therefore optional whenever every non-GSAP animation on the page is finite (CSS/WAAPI with finite iteration counts, or Lottie). It is **required** when: the composition has an infinite/unbounded CSS or WAAPI animation, the composition uses Three.js, or there is no GSAP timeline and no animation signal at all for any adapter to discover. `npx hyperframes lint` enforces exactly this (`root_composition_missing_duration_source`) — see the runtime/adapter-specific docs under `hyperframes-animation/adapters/` for the full contract per runtime.
|
||||
|
||||
## Determinism Rules
|
||||
|
||||
Rendered frames must be reproducible from the requested time. Do **not** use any of the following for visual state:
|
||||
|
||||
- `Date.now()`, `performance.now()`, or any render-time clock.
|
||||
- Unseeded `Math.random()`. Use a seeded PRNG if random-looking placement is needed.
|
||||
- Render-time network fetches for required assets. Inline or pre-bundle them.
|
||||
- Hover, scroll, pointer, or focus state. The renderer has no input events.
|
||||
- Infinite loops such as `repeat: -1`. Compute a finite count: `repeat: Math.max(0, Math.floor(duration / cycleDuration) - 1)` — **`floor`, not `ceil`** (`ceil` overshoots `data-duration` and trips the `gsap_repeat_ceil_overshoot` lint; `max(0, …)` avoids a negative repeat = infinite).
|
||||
|
||||
Also avoid:
|
||||
|
||||
- Animating anything outside the visual-property allowlist: `opacity`, `x`, `y`, `scale`, `rotation`, `color`, `backgroundColor`, `borderRadius`, transforms. Never animate `display` or `visibility` — use opacity/transforms and timed clip visibility instead.
|
||||
- Animating the same property on the same element from multiple timelines at the same time — GSAP's overwrite behavior is order-dependent and can flip between renders.
|
||||
|
||||
## Layout Contract
|
||||
|
||||
Build the visible end-state in static HTML and CSS first, then animate from/to that state.
|
||||
|
||||
- The composition root has fixed pixel frame dimensions.
|
||||
- **The root composition's total duration (render length / frame count) is fixed at compile time**, read once from the static root `data-duration` before scripts run, like `data-width` / `data-height`. A script or `--variables` value that rewrites the root `data-duration` afterward is ignored. To vary render length per output, author the root `data-duration` directly. (A _clip's_ own `data-duration` is re-read from the live DOM, so scripts/variables can still drive clip lengths. Only when the root omits `data-duration` does the renderer probe the live DOM / timeline for total length.)
|
||||
- Scene containers should fill the scene with `width: 100%; height: 100%; box-sizing: border-box`.
|
||||
- Use padding, flex, grid, and `max-width` for layout. Avoid positioning main content with hardcoded `top`/`left` offsets when a layout container can do it.
|
||||
- Use `position: absolute` for layers and decorative elements, not as the default content-layout strategy.
|
||||
- Prefer transforms and opacity for animation.
|
||||
- Keep text inside its intended container. For dynamic text, use `max-width`, wrapping, or `window.__hyperframes.fitTextFontSize(text, { maxWidth, fontFamily, fontWeight })`.
|
||||
- For text measurement without DOM reflow, use `window.__hyperframes.pretext`: `pretext.prepare(text, font)` then `pretext.layout(prepared, maxWidth, lineHeight)`. Pure arithmetic, ~0.0002 ms per call — safe for per-frame text reflow, shrinkwrap containers, and computing layout before render. `fitTextFontSize` is built on it.
|
||||
- **Do not** use `<br>` in body text. Forced breaks ignore the actual rendered font width and produce an extra break when the line already wraps naturally, causing overlap. Let text wrap via `max-width`. Exception: short display titles where each word is deliberately on its own line.
|
||||
- **Transformed elements must be block-level + sized.** `transform`/`scaleX`/`scaleY` is a no-op on an inline `<span>`, and scaling an auto-width (0px) element shows nothing → invisible bars/fills. Give them `display: block`/`inline-block`/flex-item **and** a real `width`/`height` (e.g. `width: 100%` inside a sized parent). _(silent — lint/inspect miss it.)_
|
||||
- **Absolutely-positioned decoratives that pulse or overshoot** (`yoyo` scale, `back.out`) need clearance at their **peak** size and must not straddle an `overflow: hidden` edge — else they overlap a neighbor or get clipped. Position for the largest frame, not the resting one. _(silent.)_
|
||||
|
||||
## Why This Matters
|
||||
|
||||
The renderer takes a time value and produces a pixel buffer. There is no notion of "playback" — every frame is a fresh seek. Any state that depends on having reached this frame _through_ a prior frame (timers, accumulated state, event-driven animations) will desync when the renderer samples out of order or in parallel.
|
||||
|
||||
If you find yourself reaching for `setTimeout`, `requestAnimationFrame`, or `addEventListener` to drive a visual, rebuild it as a tween on the timeline instead.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Full-Screen Motion Pattern
|
||||
|
||||
For full-frame motion (continuous backgrounds, color washes, full-bleed visual states that span multiple clips), prefer a **shared background layer + transparent timed content layers** over stacked opaque scene backgrounds.
|
||||
|
||||
## Why
|
||||
|
||||
Stacking opaque scene divs means every scene change has to repaint the entire frame, every cross-scene visual continuity has to be faked, and every "global" state (a hue shift, a vignette, a film grain) has to be duplicated on every scene. A shared background layer driven by the seekable timeline gives you one continuous visual surface and makes scenes themselves cheap and transparent.
|
||||
|
||||
## Pattern
|
||||
|
||||
```html
|
||||
<div id="root" data-composition-id="main" data-width="1920" data-height="1080" data-duration="20">
|
||||
<!-- Shared background — NOT a clip. Always visible. Driven by the timeline. -->
|
||||
<div id="bg" class="full-bleed"></div>
|
||||
|
||||
<!-- Timed content layers — transparent backgrounds. -->
|
||||
<section
|
||||
id="scene1"
|
||||
class="clip transparent"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-track-index="1"
|
||||
>
|
||||
<!-- content -->
|
||||
</section>
|
||||
<section
|
||||
id="scene2"
|
||||
class="clip transparent"
|
||||
data-start="6"
|
||||
data-duration="14"
|
||||
data-track-index="1"
|
||||
>
|
||||
<!-- content -->
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Drive the shared background from the seekable timeline.
|
||||
tl.to("#bg", { backgroundColor: "#0a1530", duration: 6, ease: "sine.inOut" }, 0);
|
||||
tl.to("#bg", { backgroundColor: "#1a0a30", duration: 14, ease: "sine.inOut" }, 6);
|
||||
|
||||
// Scene-local animations stay transparent on top.
|
||||
tl.from("#scene1 h1", { y: 48, opacity: 0, duration: 0.6 }, 0.2);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- **The background is not a clip.** No `data-start` / `data-duration` / `data-track-index`. It exists for the whole composition.
|
||||
- **Content scenes have transparent backgrounds.** Whatever you put in the shared `#bg` shows through.
|
||||
- **Drive global state from the shared layer.** Hue shifts, vignettes, grain, film-look filters — animate them once on the shared layer, not per-scene.
|
||||
- **Do not animate visibility on `.clip` elements.** HyperFrames already shows/hides clips based on `data-start` and `data-duration`. Animating `display` / `visibility` on the clip itself races with the framework's own show/hide. Animate a _child wrapper_ inside the clip instead.
|
||||
- **Verify intentional overflow with snapshots.** Before adding `data-layout-allow-overflow` to silence an inspect warning, run `npx hyperframes snapshot` and confirm the overflow is what you want.
|
||||
|
||||
## When Not to Use This Pattern
|
||||
|
||||
If scenes really are visually disjoint — hard cuts between distinct color worlds with no continuity — the stacked-opaque pattern is fine. The shared-background pattern is for compositions where the background **is part of the motion language**, not just backdrop.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Minimal Composition
|
||||
|
||||
The smallest renderable HyperFrames composition — a standalone (top-level) root with one clip and one tween:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Minimal HyperFrames Composition</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #0b0f14;
|
||||
color: white;
|
||||
font-family: Inter, system-ui, sans-serif;
|
||||
}
|
||||
#root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.clip {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 96px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="5"
|
||||
>
|
||||
<section id="title-card" class="clip" data-start="0" data-duration="5" data-track-index="1">
|
||||
<h1 id="title">Hello HyperFrames</h1>
|
||||
</section>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
tl.from("#title", { y: 48, opacity: 0, duration: 0.6, ease: "power3.out" }, 0.2);
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Required elements:
|
||||
|
||||
- Root `<div>` with `data-composition-id`, `data-start="0"`, `data-width`, `data-height`, `data-duration`
|
||||
- At least one clip (any element with `data-start`, `data-duration`, `data-track-index`)
|
||||
- GSAP timeline created paused, registered on `window.__timelines["<composition-id>"]`
|
||||
|
||||
This pattern is **standalone** (top-level `index.html`) — no `<template>` wrapper around the root. For sub-compositions (files loaded by `data-composition-src`), see `sub-compositions.md`.
|
||||
@@ -0,0 +1,49 @@
|
||||
# `SCRIPT.md` — locked narration (optional)
|
||||
|
||||
The **locked narration** for a project: the final spoken lines + voice + delivery. It is an _optional_ plan-layer file — a video with no narration (bgm-only, silent overlay) has none. The storyboard's per-frame `voiceover` is the lighter, editable _guide_; `SCRIPT.md` is the _commit_. (Storyboard format → `references/storyboard-format.md`.)
|
||||
|
||||
This file defines the SCRIPT.md **shape** only. Synthesizing the spoken lines into audio is a capability owned by `media-use` → `references/tts.md`.
|
||||
|
||||
Free-form markdown — there is no strict parser; the Studio renders it read-only beside the Storyboard board, and the TTS step extracts the indented spoken lines.
|
||||
|
||||
## Shape
|
||||
|
||||
A header block, then one section per spoken line.
|
||||
|
||||
| Part | Holds |
|
||||
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Header | `**Voice:**` (provider + voice), `**Voice settings:**` (e.g. stability / similarity / style), `**Voice direction:**` (overall delivery) |
|
||||
| `## Line N — <label> (Frame N)` | one spoken line, tied to its storyboard frame |
|
||||
| `**Time:**` | the board's rough window — a _guide_, not authoritative (real timing comes from TTS word timestamps) |
|
||||
| `**Delivery:**` | per-line delivery note |
|
||||
| indented block | the **spoken text** — the only part fed to TTS |
|
||||
|
||||
## Example
|
||||
|
||||
```markdown
|
||||
# SCRIPT — acme-launch
|
||||
|
||||
**Voice:** Rachel (ElevenLabs)
|
||||
**Voice settings:** stability 0.35 · similarity 0.75 · style 0.20
|
||||
**Voice direction:** Confident, warm, a little playful.
|
||||
|
||||
---
|
||||
|
||||
## Line 1 — Hook (Frame 1)
|
||||
|
||||
**Time:** 0.0 – 3.0s
|
||||
**Delivery:** Land the promise on the beat.
|
||||
|
||||
Ship a launch video in an afternoon.
|
||||
|
||||
## Line 2 — The problem (Frame 2)
|
||||
|
||||
**Time:** 3.0 – 7.0s
|
||||
**Delivery:** Wry, a touch tired.
|
||||
|
||||
The old way? Prompt, wait twenty minutes, get something that misses.
|
||||
```
|
||||
|
||||
## To TTS
|
||||
|
||||
Feed each line's spoken text to `npx hyperframes tts` (pin `--voice` / `--provider` from the header; capture word timestamps for captions). Real per-word timing replaces the `**Time:**` guides. CLI contract → `media-use/audio/references/tts.md`.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Storyboard format — `STORYBOARD.md` + parsed manifest
|
||||
|
||||
Defines the storyboard's **base data format** only: the `STORYBOARD.md` file shape and the `StoryboardManifest` it parses into. How a workflow _generates_ a storyboard lives in that workflow; the optional narration/TTS file (`SCRIPT.md`) is a separate concern owned by the TTS step, not here.
|
||||
|
||||
A storyboard is the **plan layer** for a video — an ordered set of **frames** (key moments) in one markdown file. HyperFrames Studio renders it as a contact sheet (the Storyboard view, available by default in every Studio session). Parser: `@hyperframes/core/storyboard` → `StoryboardManifest`; read API: `GET /api/projects/<id>/storyboard`.
|
||||
|
||||
## Frontmatter (global direction)
|
||||
|
||||
YAML block at the top. Unknown keys are kept under `globals.extra`.
|
||||
|
||||
| Key | Meaning | Example |
|
||||
| ---------- | ----------------------------------------------------------------- | ----------------------------------------- |
|
||||
| `format` | Canvas size | `1920x1080` |
|
||||
| `message` | One-line thesis | `Ship a launch video in an afternoon` |
|
||||
| `arc` | Narrative arc | `Hook → Problem → Solution → Proof → CTA` |
|
||||
| `audience` | Who it's for | `indie devs on X` |
|
||||
| `mode` | Interaction mode (see `brief-contract.md`; default collaborative) | `autonomous` |
|
||||
|
||||
## Per-frame sections
|
||||
|
||||
One `## Frame N — Title` heading per frame (`Frame` / `Beat` / `Scene` accepted at H2/H3). Metadata as `- key: value` bullets; everything below them until the next heading is the free-form **narrative**.
|
||||
|
||||
| Key | Meaning |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| `status` | `outline` → `built` → `animated` (defaults `outline`) |
|
||||
| `src` | project-relative path to the frame's HTML sub-composition (the tile poster renders from it) |
|
||||
| `duration` | e.g. `4s` |
|
||||
| `transition_in` | `crossfade` / `cut` / `wipe` … (alias `transition`) |
|
||||
| `scene` | one-line contact-sheet caption (aliases `description` / `summary` / `caption`) |
|
||||
| `voiceover` | the frame's narration _guide_ (aliases `vo` / `voice_over` / `narration`) |
|
||||
| `poster` | seconds to seek for the tile poster (past the intro animation) |
|
||||
| _any other key_ | kept verbatim under the frame's `extra` — a workflow carries its own per-frame data (effects, assets, …) here |
|
||||
|
||||
## Parsed manifest
|
||||
|
||||
The parser is **lenient**: it never throws and records anything surprising as a `warning`.
|
||||
|
||||
```
|
||||
StoryboardManifest {
|
||||
globals: { format?, message?, arc?, audience?, extra: {…} }
|
||||
frames: Array<{
|
||||
index, number?, title?,
|
||||
status, // "outline" | "built" | "animated"
|
||||
src?, duration? / durationSeconds?, transitionIn?,
|
||||
scene?, voiceover?, poster?,
|
||||
narrative, // markdown below the metadata
|
||||
extra: {…} // unknown keys, preserved
|
||||
}>
|
||||
warnings: Array<{ message, line?, frameIndex? }>
|
||||
}
|
||||
```
|
||||
|
||||
The read API also adds `srcExists` per frame and attaches the optional `SCRIPT.md` payload when present.
|
||||
|
||||
## `SCRIPT.md` (out of scope here)
|
||||
|
||||
Optional, free-form, **not parsed into the manifest** — the locked-narration file that drives TTS. Its format is defined in `references/script-format.md`, and it is absent for videos with no narration/TTS. The per-frame `voiceover` above is the storyboard's own narration guide.
|
||||
|
||||
## Example
|
||||
|
||||
```markdown
|
||||
---
|
||||
format: 1920x1080
|
||||
message: "Ship a launch video in an afternoon"
|
||||
arc: Hook → Problem → Solution → Proof → CTA
|
||||
audience: indie devs on X
|
||||
---
|
||||
|
||||
## Frame 1 — Hook
|
||||
|
||||
- scene: Big type punches in on the beat
|
||||
- duration: 3s
|
||||
- poster: 2s
|
||||
- transition_in: cut
|
||||
- status: animated
|
||||
- voiceover: "Ship a launch video in an afternoon."
|
||||
- src: compositions/frames/01-hook.html
|
||||
|
||||
Open cold on the promise. This is the thesis — everything after pays it off.
|
||||
|
||||
## Frame 2 — The problem
|
||||
|
||||
- scene: A 20-minute timer spins on a stack of rejected takes
|
||||
- duration: 4s
|
||||
- transition_in: crossfade
|
||||
- status: built
|
||||
- voiceover: "The old way? Prompt, wait twenty minutes, get something that misses."
|
||||
- src: compositions/frames/02-problem.html
|
||||
|
||||
The old way: prompt, wait, get something that misses. Establish the pain we remove.
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- A frame with `status: outline` and no built `src` renders as an outline placeholder.
|
||||
- Multi-line `voiceover` values collapse to one line on save.
|
||||
@@ -0,0 +1,237 @@
|
||||
# Sub-Compositions
|
||||
|
||||
A sub-composition is a separate HTML file embedded in a host composition. HyperFrames loads it, seeks it independently, and composites the result into the host at `data-start`.
|
||||
|
||||
## Host Wiring
|
||||
|
||||
In the host composition, the sub-composition appears as a clip with `data-composition-src`:
|
||||
|
||||
```html
|
||||
<div
|
||||
id="chart"
|
||||
data-composition-id="data-chart"
|
||||
data-composition-src="compositions/data-chart.html"
|
||||
data-start="2"
|
||||
data-duration="8"
|
||||
data-track-index="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
- `data-composition-id` on the host must match the internal `data-composition-id` of the file at `data-composition-src`.
|
||||
- The host clip needs its own `data-start`, `data-duration`, `data-track-index`, `data-width`, `data-height`.
|
||||
|
||||
## Sub-Composition File Structure
|
||||
|
||||
### Mental model — what the runtime actually does
|
||||
|
||||
When a host loads a sub-composition via `data-composition-src`, the runtime:
|
||||
|
||||
1. `fetch`es the HTML file.
|
||||
2. Parses it with `DOMParser`.
|
||||
3. **Finds the `<template>` element and clones ONLY its contents into the host slot.**
|
||||
4. Everything **outside** the `<template>` (including the entire `<head>`) is **discarded**.
|
||||
|
||||
So `<template>` is not just a wrapper — it is the **transport container**. If a node needs to exist in the live render, it must be inside `<template>`. Full stop.
|
||||
|
||||
### File shape
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- head is metadata for the source file only; the runtime ignores it -->
|
||||
</head>
|
||||
<body>
|
||||
<template>
|
||||
<!-- EVERYTHING the runtime needs goes here: styles, markup, scripts -->
|
||||
<style>
|
||||
/* Root: style by #root, never a class. (At render the CSS is scoped to
|
||||
data-composition-id, so a class on the root stops matching — see Pitfall 3.) */
|
||||
#root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
color: #fff;
|
||||
}
|
||||
/* .label, #bar, … — descendants, plain selectors */
|
||||
</style>
|
||||
|
||||
<div id="root" data-composition-id="data-chart" data-width="1920" data-height="1080">
|
||||
<!-- sub-composition markup -->
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
// ... build timeline ...
|
||||
window.__timelines["data-chart"] = tl;
|
||||
</script>
|
||||
</template>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Contrast with **standalone** compositions, which put the root directly in `<body>` with no `<template>` wrapper.
|
||||
|
||||
## Common pitfalls that pass static checks but break at render
|
||||
|
||||
`lint`, `validate`, and `inspect` evaluate each file in isolation. These two failures live in the **cross-file mount contract** and cannot be caught until the runtime actually tries to mount the sub-composition. Watch for them at author time and verify with the pre-render checklist below.
|
||||
|
||||
### Pitfall 1 — `<style>` in `<head>` instead of inside `<template>`
|
||||
|
||||
```html
|
||||
<!-- ❌ WRONG — looks normal, ships catastrophically broken -->
|
||||
<head>
|
||||
<style>
|
||||
#root { font-size: 88px; ... }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<template>
|
||||
<div id="root" data-composition-id="data-chart" ...>...</div>
|
||||
</template>
|
||||
</body>
|
||||
|
||||
<!-- ✅ RIGHT — styles are inside the template, root styled by #root (see Pitfall 3) -->
|
||||
<head></head>
|
||||
<body>
|
||||
<template>
|
||||
<style>
|
||||
#root { font-size: 88px; ... }
|
||||
</style>
|
||||
<div id="root" data-composition-id="data-chart" ...>...</div>
|
||||
</template>
|
||||
</body>
|
||||
```
|
||||
|
||||
**Why this happens:** standard HTML conventions tell you to put `<style>` in `<head>`. In a standalone HTML file that's correct. In a HyperFrames sub-composition it is **not** — the runtime only clones `<template>` contents, so `<head><style>` is dropped on the floor.
|
||||
|
||||
**Symptom:** the composition lints / validates / inspects clean, the render completes, but every text element shows up as tiny unstyled default text in the top-left and SVGs blow up to canvas-size because none of the CSS reached the live DOM. The same trap applies to `<script>` blocks, `<link rel="stylesheet">`, custom-element registrations — anything that needs to execute or apply in the render must be inside `<template>`.
|
||||
|
||||
### Pitfall 2 — Host `data-composition-id` ≠ inner template `data-composition-id`
|
||||
|
||||
```html
|
||||
<!-- ❌ WRONG — host renames the slot; runtime can't find the timeline -->
|
||||
<!-- host file (e.g. index.html) -->
|
||||
<div data-composition-id="chart-mount" data-composition-src="compositions/chart.html" ...></div>
|
||||
|
||||
<!-- chart.html -->
|
||||
<template>
|
||||
<div data-composition-id="data-chart" ...>...</div>
|
||||
<script>
|
||||
window.__timelines["data-chart"] = tl;
|
||||
</script>
|
||||
</template>
|
||||
|
||||
<!-- ✅ RIGHT — both ids match, and the timeline key matches them too -->
|
||||
<div data-composition-id="data-chart" data-composition-src="compositions/chart.html" ...></div>
|
||||
<!-- chart.html template root: data-composition-id="data-chart" -->
|
||||
<!-- timeline: window.__timelines["data-chart"] = tl; -->
|
||||
```
|
||||
|
||||
**Why this happens:** it feels natural to give the host slot a different name like `chart-mount` ("the mount point") vs `data-chart` ("the actual chart"). HyperFrames does not work that way — **the host's `data-composition-id` is the lookup key the framework uses to find the registered timeline**. Lint passes because each file's ids are individually valid; the cross-file mismatch only blows up at render.
|
||||
|
||||
**Symptom:** the render logs `Sub-composition timelines not registered after 45000ms: <host-id>` for every mismatched slot, waits 45s per scene, then captures static initial-state frames (so the video is full-length but no animation plays).
|
||||
|
||||
### Pitfall 3 — Styling the root by a class instead of `#root`
|
||||
|
||||
```html
|
||||
<!-- ❌ WRONG — class on the root, stylesheet keyed off it -->
|
||||
<template>
|
||||
<style>
|
||||
.frame {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #faf9f5;
|
||||
}
|
||||
.frame .title {
|
||||
font-size: 120px;
|
||||
}
|
||||
</style>
|
||||
<div id="root" class="frame" data-composition-id="03-scene" ...>
|
||||
<div class="title">…</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- ✅ RIGHT — root styled by #root, descendants by plain selectors -->
|
||||
<template>
|
||||
<style>
|
||||
#root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #faf9f5;
|
||||
}
|
||||
.title {
|
||||
font-size: 120px;
|
||||
}
|
||||
</style>
|
||||
<div id="root" data-composition-id="03-scene" ...>
|
||||
<div class="title">…</div>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
**Why this happens:** when sub-compositions are inlined into one composited render, the compiler **scopes each file's CSS to its own `data-composition-id`** so scenes can't leak styles into each other — every rule `S` becomes `[data-composition-id="<id>"] S` (a _descendant_ selector). A rule whose leftmost selector is the **root's own class** (`.frame`) therefore becomes `[data-composition-id="<id>"] .frame`, which cannot match the root (the root _is_ the scoped element, not a descendant of it), so **every `.frame…` rule silently drops**. `#root` is special-cased by the scoper and keeps matching the root; plain descendant selectors (`.title`) match normally. The per-scene class namespace is also just redundant — the `data-composition-id` scope already isolates each scene's styles.
|
||||
|
||||
**Symptom:** _identical_ to Pitfall 1 — tiny unstyled text in the top-left, images at natural size, inline styles (e.g. a card background) the only thing surviving. The trap: this passes `lint`/`validate`/`inspect` (they evaluate the file in isolation, unscoped) **and looks perfect in `preview`** (Studio mounts each scene in its own iframe, also unscoped) — it only breaks in the composited MP4 render. Lint rule `subcomposition_root_styled_by_class` flags it; the registry blocks (e.g. `apple-money-count`) model the `#root` pattern.
|
||||
|
||||
### Verification checklist before render
|
||||
|
||||
```bash
|
||||
# For every sub-composition file in compositions/:
|
||||
# 1) <style> + <script> + main markup all live INSIDE <template>
|
||||
grep -n "<style\|<script\|<template" compositions/<scene>.html
|
||||
# → first <style>/<script>/<div data-composition-id> should be AFTER <template>, before </template>
|
||||
|
||||
# 2) host data-composition-id == internal data-composition-id == window.__timelines key
|
||||
grep "data-composition-id" index.html
|
||||
grep "data-composition-id\|__timelines\[" compositions/<scene>.html
|
||||
# → all three strings must match exactly per scene
|
||||
|
||||
# 3) the root is styled by #root, not by a class on the data-composition-id element
|
||||
grep -n 'data-composition-id=' compositions/<scene>.html
|
||||
# → that element should NOT also carry a class="…" that the <style> keys off
|
||||
# (e.g. `.frame { … }`); scoping drops it. Style the root via #root. See Pitfall 3.
|
||||
```
|
||||
|
||||
For the runtime end-to-end check (a fast `snapshot` pass + per-scene frame eyeball), see the **Visual smoke test** step in `hyperframes-cli`'s Minimum Completion Gate — that is the only gate that catches these three pitfalls.
|
||||
|
||||
## What HyperFrames Does With the Sub-Composition
|
||||
|
||||
- Loads the file and registers its timeline under its internal `data-composition-id`.
|
||||
- Seeks the sub-composition's timeline independently from the host's playhead.
|
||||
- Plays the sub-composition's content from `data-start` of the host clip, for `data-duration` seconds.
|
||||
|
||||
**Do not** manually `master.add(child)` a sub-composition timeline into the host timeline. HyperFrames already drives them independently — nesting them in GSAP causes double-seeks.
|
||||
|
||||
### The host clip's `data-duration` is the slot's visible window
|
||||
|
||||
`data-duration` on the host clip defines **how long the slot is visible**, and it takes precedence over the sub-composition's internal GSAP timeline length. Two consequences follow:
|
||||
|
||||
- **Internal timeline shorter than the slot → the slot holds.** If the sub-composition's GSAP timeline finishes before `data-duration` elapses, the slot keeps showing its final frame for the rest of the window. You do **not** need to pad the timeline with empty tweens.
|
||||
- **`data-duration` shorter than the host composition → the slot ends (and goes blank) when its own `data-duration` elapses.** This is intended: the clip is a fixed-length window on the timeline, not "fill until the composition ends." To keep a sub-composition visible for the whole composition, set its `data-duration` to span the host window (or add another clip to cover the remaining time). Leaving a single full-bleed sub-composition shorter than the composition is almost always a mistake — the linter flags it as `subcomposition_blanks_before_host`.
|
||||
|
||||
## Animations Inside Sub-Compositions
|
||||
|
||||
Prefer `gsap.fromTo()` over `gsap.from()` for entrance tweens. The host re-seeks the sub-composition every time its clip becomes visible; `gsap.from()` records the starting state at registration and can desync on seek-back, while `gsap.fromTo()` declares both endpoints explicitly and replays cleanly.
|
||||
|
||||
## Per-Instance Variables
|
||||
|
||||
If the sub-composition declares variables on its `<html>` element (`data-composition-variables`), the host can override values per instance:
|
||||
|
||||
```html
|
||||
<div
|
||||
data-composition-id="data-chart"
|
||||
data-composition-src="compositions/data-chart.html"
|
||||
data-variable-values='{"title":"Q4 Revenue","accent":"#66d9ef"}'
|
||||
data-start="2"
|
||||
data-duration="8"
|
||||
data-track-index="2"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
```
|
||||
|
||||
The host can render the same sub-composition multiple times with different `data-variable-values` to produce per-instance variations. See `variables-and-media.md` for variable declaration syntax.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Subagent dispatch — harness adapter
|
||||
|
||||
The video workflows (`product-launch-video` / `faceless-explainer` / `pr-to-video` / `motion-graphics` / `general-video`) describe subagent dispatch in harness-neutral verbs. This file maps those verbs to the primitives of whatever agent harness you are running on. Read it once per run, before the first dispatch; everything else in the workflows (dispatch packets, file artifacts, exit-code gates, Resume tables) is harness-independent and needs no translation.
|
||||
|
||||
## The contract (identical on every harness)
|
||||
|
||||
- **DISPATCH(role_file, dispatch_context)** — start one child agent whose prompt is the **full contents of the named `agents/<role>.md` file** followed by the `## Dispatch context` block from the workflow, copied **verbatim** (never digested or paraphrased). Every harness below accepts arbitrary task text, so this works everywhere; never rely on the child seeing your conversation, memory, or skills — the prompt and the files on disk are its entire world.
|
||||
- **Parallel fan-out** — when a step says "start N workers in parallel", the workers are mutually independent (no ordering, no shared state beyond the filesystem). Run as many concurrently as your harness allows.
|
||||
- **WAIT** — a step's completion criterion is always **the expected artifact existing on disk** (e.g. `compositions/<scene-id>.html`), never the harness's completion notification (some harnesses deliver results best-effort). After waiting, verify the artifacts; a missing artifact means that child failed — re-dispatch it once with the same prompt before surfacing an error.
|
||||
|
||||
## Concurrency cap → batching rule (cap never changes scope)
|
||||
|
||||
A harness concurrency limit **reduces parallelism, not work**: every scene still gets built, one scene per dispatch, with the available slots chewing through the full list.
|
||||
|
||||
- Harness queues excess children internally (Claude Code; Hermes `delegate_task` beyond `max_concurrent_children`) → submit **all N at once** and let the queue drain.
|
||||
- Harness hard-caps active children (e.g. OpenClaw `maxChildrenPerAgent`) → dispatch in **waves of the cap size**: start `cap` workers, wait for their artifacts, start the next wave, until all N scenes exist. Example: 9 scenes on a cap-3 harness = 3 waves of 3 — never drop scenes, never merge scenes into one worker to fit the cap.
|
||||
|
||||
## Primitive map
|
||||
|
||||
| Verb in the workflows | Claude Code | Codex CLI | OpenClaw | Hermes Agent |
|
||||
| -------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| DISPATCH | `Agent` tool, `subagent_type: "general-purpose"` | `spawn_agent` (built-in `worker` role; prompt carries the role file) | `sessions_spawn` (role file rides in `task`; child only auto-loads AGENTS.md/TOOLS.md) | `delegate_task` — put role file + context into each task's `context` (child system prompt is not customizable) |
|
||||
| parallel fan-out | multiple Agent calls in one message, each `run_in_background: true` | multiple `spawn_agent` calls (async by design) or `spawn_agents_on_csv` for uniform batches | repeated `sessions_spawn` (always non-blocking) | one `delegate_task(tasks=[...])` batch call |
|
||||
| WAIT | background-task completion notifications arrive automatically | `wait_agent` with all child IDs | `sessions_yield` (never poll session lists) | `delegate_task` returns when all tasks finish (blocking the turn is normal) |
|
||||
| default concurrency | min(16, cores − 2), excess queues | 6 (`agents.max_threads`) | 5 active per session, 8 global | 3 (`delegation.max_concurrent_children`) |
|
||||
| re-dispatch after a gate failure | new Agent call, same prompt + the error/`## Repair context` | new `spawn_agent`, same prompt + the error | new `sessions_spawn`, same `task` + the error | new `delegate_task`, same `context` + the error |
|
||||
|
||||
Harness-specific notes:
|
||||
|
||||
- **Codex**: children inherit the parent cwd and cannot change it — exactly what these pipelines need (shared `PROJECT_DIR`). Prefer prompt-carried roles over `.codex/agents/*.toml` registration; the workflows already deliver roles in the prompt. **Codex policy-gates spawning on the user explicitly requesting delegation — a skill instruction alone does not satisfy it, and you will be tempted to skip the tool without even trying.** Do not silently degrade: on Codex, if the user has not granted delegation this session, **ask one line** — "OK to run this workflow's workers as parallel subagents?" — **folded into the workflow's first existing user pause** (e.g. the API-key question) so the run pauses only once; at the latest, before the first dispatch step. An affirmative — or a standing grant in the workspace `AGENTS.md` or the kickoff prompt — unlocks native dispatch for the whole session. Only without a grant: prefer ladder rung 1 below (headless `codex exec` children) for the scene fan-out; inline serial is the last resort.
|
||||
- **OpenClaw**: result announcement is best-effort (lost on gateway restart) — the artifact-on-disk WAIT rule above is the source of truth, not the announce message.
|
||||
- **Hermes**: children get a fresh conversation and only see what you put in `context`, and only the final summary returns — both already match the workflows' design (dispatch packets carry everything; results land on disk). Requires a **local execution backend** (the pipelines exchange data through `PROJECT_DIR` and packet files on the shared filesystem; Docker/remote backends break that).
|
||||
- **Any other harness**, or subagents unavailable/disabled — degrade down this ladder:
|
||||
1. Headless CLI children: write each child prompt to a file, start the CLI per worker as a background shell process (redirect stdin from /dev/null, output to a log), then collect artifacts from disk.
|
||||
2. Inline serial execution: do each role yourself, one at a time — read the role file, follow it with the dispatch context as if you were the child, finish, then return to the runbook. Load only one role's resources at a time (a scene's role file + its packet), and still run every per-role self-check before moving on.
|
||||
|
||||
## Vocabulary mapping (older phrasing you may meet in agent prompts)
|
||||
|
||||
- "in the same message" / `run_in_background: true` — Claude Code idioms for "concurrently / as a background child"; apply your harness's equivalent from the table.
|
||||
- "Skill `X`" / "loaded with the Skill tool" — load skill X; on harnesses without a skill-loading tool, read `<skills-root>/X/SKILL.md` directly (the skills root is the directory containing this skill family; derive it from `SKILL_DIR` in your dispatch context).
|
||||
- `Read` / `Write` / `Edit` / `Bash` — capability names (read file / write file / edit in place / run shell); map to your harness's tools.
|
||||
@@ -0,0 +1,123 @@
|
||||
# HyperFrames Tailwind
|
||||
|
||||
HyperFrames `init --tailwind` uses the Tailwind browser runtime pinned by the scaffold. Treat it as Tailwind v4, not Studio's Tailwind v3 setup.
|
||||
|
||||
## When To Use
|
||||
|
||||
- The project was scaffolded with `npx hyperframes init --tailwind`.
|
||||
- `index.html` contains `window.__tailwindReady`.
|
||||
- The task asks for Tailwind utility classes, `@theme`, custom utilities, or v3-to-v4 fixes in a composition.
|
||||
- Rendered frames have missing Tailwind styles or frame-0 flashes.
|
||||
|
||||
## Version Contract
|
||||
|
||||
- **Pinned: `@tailwindcss/browser@4.2.4`** (source of truth: `packages/cli/src/commands/init.ts` `TAILWIND_BROWSER_VERSION`).
|
||||
- Do not replace the scaffolded runtime with `cdn.tailwindcss.com` (unpinned, defeats reproducibility).
|
||||
- Keep the readiness shim deterministic; HyperFrames waits for `window.__tailwindReady` before frame 0 capture.
|
||||
- For offline / locked-down / production-stable renders, compile Tailwind to CSS and ship the stylesheet instead of the browser runtime.
|
||||
|
||||
## v4 Browser Runtime Rules
|
||||
|
||||
Tailwind v4 is CSS-first:
|
||||
|
||||
```html
|
||||
<style type="text/tailwindcss">
|
||||
@theme {
|
||||
--color-brand: oklch(0.68 0.2 252);
|
||||
--font-display: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
@utility headline-balance {
|
||||
text-wrap: balance;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
Avoid v3-only patterns in browser-runtime compositions:
|
||||
|
||||
```css
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
```
|
||||
|
||||
Do not add `tailwind.config.js` only for composition colors, fonts, spacing, or utilities. Use `@theme` and `@utility`.
|
||||
|
||||
**Migrating from v3?** Load an existing JS config explicitly: put `@config "./tailwind.config.js";` inside a `text/tailwindcss` block. v4 does **not** auto-detect v3 config files.
|
||||
|
||||
## Composition Pattern
|
||||
|
||||
Use Tailwind for static layout and style. Keep render-critical timing in GSAP or another seekable HyperFrames adapter.
|
||||
|
||||
```html
|
||||
<section
|
||||
id="hero"
|
||||
class="clip absolute inset-0 grid place-items-center bg-zinc-950 text-white"
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
data-track-index="1"
|
||||
>
|
||||
<div class="w-[1280px] max-w-[82vw] text-center">
|
||||
<h1 class="text-7xl font-black leading-none text-balance">Render-ready Tailwind</h1>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
For repeated items, **parameterize via CSS variables** — keep the class list static so the runtime sees every utility:
|
||||
|
||||
```html
|
||||
<span class="translate-y-[calc(var(--i)*6px)] opacity-80" style="--i: 0"></span>
|
||||
<span class="translate-y-[calc(var(--i)*6px)] opacity-80" style="--i: 1"></span>
|
||||
<span class="translate-y-[calc(var(--i)*6px)] opacity-80" style="--i: 2"></span>
|
||||
```
|
||||
|
||||
## Dynamic Class Safety
|
||||
|
||||
The browser runtime scans classes it can see. Do not build render-critical class names only at seek time:
|
||||
|
||||
```js
|
||||
// Risky: the runtime may never see every generated class.
|
||||
element.className = `bg-${color}-500`;
|
||||
```
|
||||
|
||||
Prefer complete class tokens in HTML, data variants, or explicit CSS:
|
||||
|
||||
```html
|
||||
<div data-tone="blue" class="bg-blue-500 data-[tone=rose]:bg-rose-500"></div>
|
||||
```
|
||||
|
||||
If a generated class is unavoidable, make sure the full class token appears in a `text/tailwindcss` block before validation.
|
||||
|
||||
## Video-Specific Guardrails
|
||||
|
||||
v4 + render-mode footguns. Every bullet is a hard rule:
|
||||
|
||||
- **Stable dimensions only** — use `w-[…]` / `h-[…]` / `aspect-video` / grid / flex. **No `md:` / `lg:` breakpoints** (renderer is fixed-viewport).
|
||||
- **Animate via transforms / opacity** — `translate-*`, `scale-*`, `opacity-*` are seek-safe; animating Tailwind sizing utilities is not.
|
||||
- **No `transition-*` for render-critical motion** — a seekable runtime (GSAP) must own the state.
|
||||
- **No interaction variants** — `hover:` / `focus:` / `active:` / `group-*:` / `peer-*:` / scroll / pointer variants never fire during render.
|
||||
- **Bare `border` is broken in v4** — v4 default is `currentColor` (v3 was `gray-200`). Always write the color: `border border-white/20`.
|
||||
- **v4 utility renames** — `shadow-sm` → `shadow-xs`, `rounded-sm` → `rounded-xs`, `outline-none` → `outline-hidden`, `flex-shrink-*` → `shrink-*`, `flex-grow-*` → `grow-*`.
|
||||
- **Modern CSS is fine** — `color-mix()`, container queries, logical properties work; the renderer is current Chrome.
|
||||
|
||||
## Validation
|
||||
|
||||
```bash
|
||||
npx hyperframes check
|
||||
|
||||
# Render proof — frame 0 must NOT flash unstyled content. Preview alone can hide this.
|
||||
npx hyperframes render . --workers 1 --quality draft --output tailwind-proof.mp4
|
||||
```
|
||||
|
||||
## Quick Debug Checklist
|
||||
|
||||
When Tailwind styles don't apply in a render, check in order:
|
||||
|
||||
1. Project scaffolded with `npx hyperframes init --tailwind`?
|
||||
2. `index.html` `<head>` has `<script src="…@tailwindcss/browser@4.2.4…">` (not `cdn.tailwindcss.com`)?
|
||||
3. `window.__tailwindReady` Promise present in `<head>`?
|
||||
4. No v3 directives (`@tailwind base/components/utilities`) in the file?
|
||||
5. Tokens moved from `tailwind.config.js` to `@theme` (or `@config` reference for v3 migration)?
|
||||
6. Every render-critical class appears as a complete static token (no `bg-${color}-500` style assembly)?
|
||||
7. Re-run `npx hyperframes check`, then the render proof above.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Tracks and Clips
|
||||
|
||||
Clips are timed children of the composition root. Tracks are a temporal-overlap concept, not a visual-stacking concept.
|
||||
|
||||
## What is a Clip
|
||||
|
||||
A clip is any DOM element with `data-start`, `data-duration` (where required), and `data-track-index`. Common kinds:
|
||||
|
||||
- **Visual `<div>` clips** — scenes, cards, overlays. Always require `data-duration`.
|
||||
- **Sub-composition hosts** — `<div>` with `data-composition-src`. Always require `data-duration`.
|
||||
- **Video clips** — `<video>` with `muted` and `playsinline`. Duration can default to media length.
|
||||
- **Audio clips** — `<audio>`. Duration can default to media length.
|
||||
- **Image clips** — `<img>`. Always require `data-duration`.
|
||||
|
||||
Add `class="clip"` to authored visual clips so tooling and examples can find them.
|
||||
|
||||
## Tracks Are Temporal, Not Visual
|
||||
|
||||
`data-track-index` controls **temporal overlap**, not paint order:
|
||||
|
||||
- **Two clips on the same `data-track-index`** must NOT overlap in time. `hyperframes lint` flags this.
|
||||
- **Visual layering (front/back)** is controlled by CSS `z-index`, not by track index.
|
||||
|
||||
A clip on track `5` is not "above" a clip on track `1` — it's just on a different audio/visual lane in time. Use CSS for layering, tracks for sequencing.
|
||||
|
||||
## Picking a Track Index
|
||||
|
||||
There's no fixed convention, but common patterns:
|
||||
|
||||
- **Track 0** — base video (e.g. an A-roll).
|
||||
- **Track 1+** — visual scenes, overlays, captions.
|
||||
- **Higher tracks (e.g. 10+)** — audio clips, separated from visual tracks to keep linting clear.
|
||||
|
||||
When adding a new clip to an existing composition:
|
||||
|
||||
1. Find an existing track that has no overlap with your new clip's `[data-start, data-start + data-duration)` range.
|
||||
2. Or pick a fresh track index.
|
||||
3. Never overlap two clips on the same track — the linter will fail and the render is undefined.
|
||||
|
||||
## Clip Time Inside the Composition
|
||||
|
||||
`data-start` is in seconds, measured from the start of the _composition_. For sub-compositions, the sub-composition's internal timeline (its own `data-duration` and child clips) runs from `data-start` to `data-start + data-duration` of the host.
|
||||
|
||||
`data-media-start` (on `<video>`/`<audio>`) is an offset _into the source media_. Use it to skip the first few seconds of a media file without trimming the file itself.
|
||||
|
||||
## Relative Timing
|
||||
|
||||
`data-start` accepts a clip ID instead of a number, meaning "start when that clip ends". Add `+ N` / `- N` to offset; negative produces overlap (useful for crossfades).
|
||||
|
||||
```html
|
||||
<video id="intro" data-start="0" data-duration="10" data-track-index="0" src="..."></video>
|
||||
<video id="main" data-start="intro" data-duration="20" data-track-index="0" src="..."></video>
|
||||
<video
|
||||
id="scene-a"
|
||||
data-start="intro + 2"
|
||||
data-duration="20"
|
||||
data-track-index="0"
|
||||
src="..."
|
||||
></video>
|
||||
<video
|
||||
id="scene-b"
|
||||
data-start="intro - 0.5"
|
||||
data-duration="20"
|
||||
data-track-index="1"
|
||||
src="..."
|
||||
></video>
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- References resolve **inside the same composition only** — cannot reach into a parent or sibling sub-composition.
|
||||
- The referenced clip must have a **known duration** (explicit `data-duration` or inferred from media). Otherwise the reference cannot resolve.
|
||||
- **No circular references** — `A → B → A` is rejected. Cycles are detected and error out.
|
||||
- A value that parses as a number is always treated as absolute seconds. Otherwise the resolver expects `<id>`, `<id> + <number>`, or `<id> - <number>` (whitespace optional).
|
||||
- References can chain (`A → B → C`). Keep chains under 3-4 levels for readability.
|
||||
- Negative offsets create overlap; overlapping clips must be on **different tracks**, same-track overlap is rejected.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Variables and Media
|
||||
|
||||
Two separate concerns, grouped because both control "what flows in from outside the HTML": runtime parameters (variables) and external media files (video/audio).
|
||||
|
||||
## Variables
|
||||
|
||||
Declare variables on the `<html>` element with `data-composition-variables`. Each declaration needs `id`, `type`, `label`, and `default`:
|
||||
|
||||
```html
|
||||
<html
|
||||
data-composition-variables='[
|
||||
{"id":"title","type":"string","label":"Title","default":"Hello"},
|
||||
{"id":"accent","type":"color","label":"Accent","default":"#66d9ef"}
|
||||
]'
|
||||
></html>
|
||||
```
|
||||
|
||||
**Prefer declarative bindings — no script needed** for direct substitution:
|
||||
|
||||
```html
|
||||
<img class="clip" data-start="0" data-duration="5" data-var-src="heroImage" src="fallback.jpg" />
|
||||
<h1 class="clip" data-start="0" data-duration="5" data-var-text="title">Fallback</h1>
|
||||
<style>
|
||||
.card {
|
||||
color: var(--accent);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
- `data-var-src="id"` substitutes the element's `src` (URL string or image `{url}`); the authored `src` is the fallback.
|
||||
- `data-var-text="id"` substitutes the element's own text; element children (nested clips, animated spans) are preserved.
|
||||
- Every scalar variable is applied automatically as a `--{id}` CSS custom property on the composition root, so `var(--id)` CSS responds to overrides — no `setProperty` boilerplate.
|
||||
- Bindings resolve identically in preview and render, and per-instance for sub-compositions.
|
||||
- Caveat: media with audio should keep a real fallback `src` — render audio extraction reads the authored attribute (lint: `media_variable_src_no_fallback`).
|
||||
|
||||
For logic beyond direct substitution (loops, conditionals, derived values), read values once during initialization:
|
||||
|
||||
```js
|
||||
const { title, accent } = window.__hyperframes.getVariables();
|
||||
document.getElementById("title").textContent = title;
|
||||
```
|
||||
|
||||
### Variable Rules
|
||||
|
||||
- Supported types and their extra options (consumed by Studio's editing UI):
|
||||
- `string` — optional `placeholder`, `maxLength`
|
||||
- `number` — optional `min`, `max`, `step`, `unit`
|
||||
- `color` — none
|
||||
- `boolean` — none
|
||||
- `enum` — **required** `options: [{ "value": "...", "label": "..." }, ...]`
|
||||
- Always provide useful `default` values so preview works without CLI overrides.
|
||||
- Use `data-variable-values='{"title":"Pro"}'` on sub-composition hosts for per-instance overrides.
|
||||
- Use `npx hyperframes render --variables '{"title":"Q4 Report"}'` or `--variables-file` for render-time overrides.
|
||||
- Add `--strict-variables` in CI: turns undeclared keys, type mismatches, and enum values not in `options` into errors instead of warnings.
|
||||
- Read values once during init, not on every animation tick — variables don't change mid-render.
|
||||
- Media color grading can use exact variable references inside `data-color-grading` JSON. Use `$gradingPreset` or `${gradingIntensity}` as the whole field value; the runtime resolves it from the current composition's variables before applying shader adjustments, finishing details, blur/pixelate effects, and custom LUTs.
|
||||
|
||||
### Two JSON Shapes (Easy to Confuse)
|
||||
|
||||
- `data-composition-variables` is an **array of declarations** (the schema): `[{id, type, label, default}, ...]`
|
||||
- `--variables` and `data-variable-values` are **objects keyed by id** (the values): `{ title: "Q4", accent: "#fff" }`
|
||||
|
||||
## Media
|
||||
|
||||
**NON-NEGOTIABLE: `<video>`/`<audio>` must be a DIRECT child of the host composition root (`index.html`).** The runtime only registers + drives media that is a direct root child. Media placed inside a sub-composition `<template>`, or wrapped in any intermediate `<div>`, is never seeked/decoded → renders blank (paper/white) or black. `lint`/`validate`/`inspect` do not catch this; per-frame `snapshot` shows the blank panel.
|
||||
|
||||
Consequences:
|
||||
|
||||
- A scene-specific clip still lives at the host root, not in the scene's sub-comp. The sub-comp keeps only the frame/shell; the media is a sibling host element positioned over it.
|
||||
- A sub-composition **cannot reach or animate host elements** — neither `document.querySelector("#host-id")` nor a gsap selector string (`tl.to("#host-id", …)`) resolves across the boundary; a sub-comp timeline only drives its own subtree. So **all per-scene motion on host media (scale/opacity/morph/tilt/breathing) must be authored on the MAIN timeline in `index.html`, at GLOBAL time** (scene-local time + the scene slot's `data-start`). For 3D tilt without a perspective parent, use gsap `transformPerspective` on the element. See `composition-patterns.md` archetype B.
|
||||
|
||||
Video elements must be muted and inline. Audio must be a separate `<audio>` element, even when it uses the same source file.
|
||||
|
||||
```html
|
||||
<video
|
||||
id="a-roll"
|
||||
class="clip"
|
||||
src="assets/demo.mp4"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-track-index="0"
|
||||
muted
|
||||
playsinline
|
||||
></video>
|
||||
|
||||
<audio
|
||||
id="a-roll-audio"
|
||||
src="assets/demo.mp4"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-track-index="10"
|
||||
data-volume="1"
|
||||
></audio>
|
||||
```
|
||||
|
||||
### Media Rules
|
||||
|
||||
- **Do not** call `video.play()`, `audio.play()`, pause, or seek in composition code. HyperFrames owns playback.
|
||||
- **Do not** place media inside a sub-comp `<template>` or any wrapper `<div>` — direct host-root child only (see above), else it never decodes.
|
||||
- **Do not** drive host media from a sub-comp timeline — it has no effect. Drive it from the main timeline at global time.
|
||||
- **Do not** animate timed media element dimensions; animate a non-timed wrapper instead.
|
||||
- **Do not** nest video inside a timed wrapper. Put timing on the media element or keep the wrapper untimed.
|
||||
- Add `crossorigin="anonymous"` for external media that needs canvas capture or pixel inspection.
|
||||
- Audio always lives on a separate `<audio>` element — even if its source file is the same as a `<video>`. The `<video>` is muted; the `<audio>` carries sound.
|
||||
- For volume fades/ducking, animate `volume` on the timeline (`tl.to("#bgm", { volume: 0, duration: 1 }, "outro")`) rather than swapping `data-volume`. The runtime probes the timeline's volume keyframes and applies them identically in preview and render; `data-volume` is the static baseline for elements no tween touches.
|
||||
|
||||
For media duration: `<video>` and `<audio>` can omit `data-duration` if the media's intrinsic length is known and you want the full clip. Otherwise provide `data-duration` explicitly.
|
||||
Reference in New Issue
Block a user