mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 00:56:23 +00:00
style(skills): apply oxfmt to music-to-video and router docs
Fixes the Format / Preflight CI checks on the new skill files. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f012b8b846
commit
265b02738e
@@ -141,8 +141,14 @@ Contrast with **standalone** compositions, which put the root directly in `<body
|
||||
<!-- ❌ WRONG — class on the root, stylesheet keyed off it -->
|
||||
<template>
|
||||
<style>
|
||||
.frame { position: absolute; inset: 0; background: #faf9f5; }
|
||||
.frame .title { font-size: 120px; }
|
||||
.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>
|
||||
@@ -152,8 +158,14 @@ Contrast with **standalone** compositions, which put the root directly in `<body
|
||||
<!-- ✅ RIGHT — root styled by #root, descendants by plain selectors -->
|
||||
<template>
|
||||
<style>
|
||||
#root { position: absolute; inset: 0; background: #faf9f5; }
|
||||
.title { font-size: 120px; }
|
||||
#root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: #faf9f5;
|
||||
}
|
||||
.title {
|
||||
font-size: 120px;
|
||||
}
|
||||
</style>
|
||||
<div id="root" data-composition-id="03-scene" ...>
|
||||
<div class="title">…</div>
|
||||
|
||||
@@ -52,7 +52,7 @@ Routing needs to know **what the video is about** — its input and subject. If
|
||||
## Workflow cheat-sheet
|
||||
|
||||
| Workflow | Use it for |
|
||||
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `/product-launch-video` | Marketing / launching / promoting a **product** — from its URL, a brief, or a script (even if the site is only named) |
|
||||
| `/website-to-video` | Turning a **general website** into a video — site tour, portfolio / landing-page showcase, social clip from the site's visuals |
|
||||
| `/faceless-explainer` | **Explaining a topic / concept** from text — no product, no URL; every visual is LLM-invented |
|
||||
|
||||
@@ -16,7 +16,7 @@ Workflow: Step 0 setup → `hyperframes.json` + `assets/bgm.mp3`; Step 1 analyze
|
||||
## Two ideas that shape everything
|
||||
|
||||
- **One analyzer, and you trust it.** `analyze-beatgrid.py` is the only beat analyzer — never re-measure beats with another tool or by ear. Its energy / density / rolls / onsets / silences are always reliable. Its `bpm` and `beats_sec` are reliable **only when the music is genuinely rhythmic**; on calm music the grid is a metronome the tracker imposed, so pace by phrases and energy instead and never hard-cut to it. Deciding which case you're in is each frame's `pacing` (Step 2).
|
||||
- **One frame = one file; groups live inside.** Step 2 cuts the track into **frames**, and each frame becomes one composition file `compositions/frames/NN-<frame_id>.html`, built by one frame-worker. A frame can subdivide into **groups** (each a template or a motion-primitives combo). Extra density goes *inside* a group, so **frame count tracks distinct treatments, not beats** — a fast track does not blow up the number of sub-agents.
|
||||
- **One frame = one file; groups live inside.** Step 2 cuts the track into **frames**, and each frame becomes one composition file `compositions/frames/NN-<frame_id>.html`, built by one frame-worker. A frame can subdivide into **groups** (each a template or a motion-primitives combo). Extra density goes _inside_ a group, so **frame count tracks distinct treatments, not beats** — a fast track does not blow up the number of sub-agents.
|
||||
|
||||
---
|
||||
|
||||
@@ -167,7 +167,7 @@ Inspect at `t=0`, each frame start, the strongest DROP / SURGE, every `hard_stop
|
||||
**Scripts** under `scripts/`: `analyze-beatgrid.py` (the one analyzer), `validate-plan.mjs` (plan check), `assemble-index.mjs` (index assembly), `stage-assets.mjs` (stage user media), `lib/storyboard.mjs` (vendored parser). Everything else is the `hyperframes` CLI.
|
||||
|
||||
| Read | When |
|
||||
| ------------------------------------------------------------------------------------------ | ----------------------------------------------------- |
|
||||
| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|
||||
| [`references/frame-skeleton.md`](references/frame-skeleton.md) | Step 2: read the music, lay out the frames, set pacing |
|
||||
| [`references/planning.md`](references/planning.md) · [`storyboard-format.md`](references/storyboard-format.md) | Step 3: pick the brand, fill each frame, write the plan |
|
||||
| [`references/template-catalog.md`](references/template-catalog.md) | Step 3: pick a template per group |
|
||||
|
||||
@@ -18,9 +18,9 @@ edits the same file.
|
||||
reliable only when the music is **actually rhythmic**. This decides each frame's `pacing`:
|
||||
|
||||
| Field | Trust |
|
||||
| --- | --- |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `energy_phases[]` (level / energy / density / feel), `events[]` + `onset_rate`, `rolls[]` (and their **absence**), `silences[]`, `hard_stops[]`, `key_moments[]`, `phrases[]`, `audio.duration_sec` | **Always** — robust measurements |
|
||||
| `tempo.bpm`, `grid.beats_sec` / `downbeats_sec` **precision** | **Only when the music is rhythmic.** On calm / sparse material the beat grid is a metronome the tracker *imposes* (often octave-doubled) — usually **more grid beats than real onsets**. Do **not** anchor cuts to it there. |
|
||||
| `tempo.bpm`, `grid.beats_sec` / `downbeats_sec` **precision** | **Only when the music is rhythmic.** On calm / sparse material the beat grid is a metronome the tracker _imposes_ (often octave-doubled) — usually **more grid beats than real onsets**. Do **not** anchor cuts to it there. |
|
||||
|
||||
- **Grid is reliable** when: rolls present, and/or dense phases, and/or high `onset_rate` with a steady grid.
|
||||
- **Grid is fictional** when: `rolls`≈0, mostly `sparse` phases, low `onset_rate` → pace by `phrases[]` + `energy_phases[]`, not beats.
|
||||
@@ -65,23 +65,29 @@ build_notes: ["one paused timeline per frame", "no remote assets"]
|
||||
---
|
||||
|
||||
## Frame 1 — f1
|
||||
|
||||
- src: compositions/frames/01-f1.html
|
||||
- duration: 7.198s # = span length; assembler sums these for cumulative data-start
|
||||
- span_sec: [0.0, 7.198] # track seconds; frames tile the track
|
||||
- pacing: beat_cut
|
||||
- mood: [hype]
|
||||
- feel: accelerating onset stream building into a held downbeat
|
||||
|
||||
### Groups
|
||||
|
||||
- TBD (Step 3)
|
||||
|
||||
## Frame 2 — f2
|
||||
|
||||
- src: compositions/frames/02-f2.html
|
||||
- duration: 10.4s
|
||||
- span_sec: [7.198, 17.598]
|
||||
- pacing: phrase_flow
|
||||
- mood: [warm, cinematic]
|
||||
- feel: calm held pad, one long onset desert
|
||||
|
||||
### Groups
|
||||
|
||||
- TBD (Step 3)
|
||||
```
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ realizes it inside the frame file (HOW). **Obey the frame's `pacing`.**
|
||||
## The three treatments
|
||||
|
||||
### `beat_cut` — one clip per anchor (only on a `beat_cut` frame)
|
||||
|
||||
The asset-driven analogue of a per-onset typographic group: cut to a new clip on each anchor
|
||||
(the frame's beats/onsets from the audiomap). Each clip is a `class="clip"` element
|
||||
(`<img>` for a photo, **muted** `<video>` for a motion clip) placed at its anchor with
|
||||
@@ -21,12 +22,14 @@ crossfade the outgoing content to `opacity:0` ending **at** the next anchor, the
|
||||
Cut on the **strong** anchors; land a hero clip on a `key_moment`/downbeat.
|
||||
|
||||
### `ken_burns` — slow push on one clip (fits a `phrase_flow` frame)
|
||||
|
||||
For calm frames: one clip held over the span with a slow scale/translate push (e.g. scale
|
||||
1.0→1.08 + a small drift) eased across the whole `span_sec` — paced by the frame, not by
|
||||
beats. No hard cuts. Crossfade in/out at the frame edges. This is the right asset treatment
|
||||
when the beat grid is unreliable (calm music).
|
||||
|
||||
### `bg_under_text` — clip dimmed behind a template/free group
|
||||
|
||||
A full-bleed clip dimmed ~30–50% as the background of a group whose foreground is a template
|
||||
or free-compose typographic treatment. The text rides on the same anchors; the clip is the
|
||||
bed. Use when the user wants their footage present but the message must stay readable.
|
||||
|
||||
@@ -30,7 +30,7 @@ Each runnable recipe is self-contained and lint-clean: showcase chrome stripped,
|
||||
fallback, shared `../assets/gsap.min.js`, one paused timeline on `window.__timelines["main"]`.
|
||||
|
||||
| id | anchor | what it does | recipe |
|
||||
| -------------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
|
||||
| --------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- |
|
||||
| `hypercut-whip` | beat / hard_stop | fast whip-pan hard cut between frames | ✓ |
|
||||
| `kinetic-letter-in` | downbeat / phrase | per-letter kinetic entrance | ✓ |
|
||||
| `braam-punch` | drop / surge | big "braam" impact — scale + weight slam | ✓ |
|
||||
|
||||
@@ -51,19 +51,23 @@ frame is usually ONE group whose template absorbs the density internally (a meta
|
||||
## Step C — per group, pick a treatment (exactly one of three)
|
||||
|
||||
### A. Match a template
|
||||
|
||||
Read [`template-catalog.md`](template-catalog.md). Match the group's `feel` + `mood` + `pacing`
|
||||
to a template's **Reach for it when**; take the closest fit. Then bind it:
|
||||
|
||||
- Fill `params` (keys from the catalog entry) — your copy into text slots, palette from the brand spine, `duration` = the group's span length.
|
||||
- Fill `role_bindings` with this group's **real anchor seconds** read from `audiomap.json` over its span (not example times).
|
||||
- If the template's natural stop and the group's span end disagree, snap to the nearest anchor.
|
||||
|
||||
### B. Free-compose (no template fits)
|
||||
|
||||
Write a `free_design` — one visual thesis from [`motion-primitive-catalog.md`](motion-primitive-catalog.md)
|
||||
(a dominant system + the named L0 primitives + a density topology) + `anchors` (the real
|
||||
beat / onset seconds the moves ride). Free-compose is a **first-class** choice, written as
|
||||
carefully as a matched group — never a failure.
|
||||
|
||||
### C. Asset treatment (only when the user supplied assets and they fit)
|
||||
|
||||
Make it an `asset` group ([`montage.md`](montage.md)). **Obey `pacing`:** on a `beat_cut`
|
||||
frame use `beat_cut` (one clip per anchor) or `bg_under_text`; on a `phrase_flow` frame use
|
||||
`ken_burns` or a slow crossfade — **never** per-onset hard cuts. Assets are additive: if none
|
||||
|
||||
@@ -31,6 +31,7 @@ avoid: ["generic slideshow", "tiny unreadable hero text"]
|
||||
---
|
||||
|
||||
## Frame 1 — f1
|
||||
|
||||
- src: compositions/frames/01-f1.html # worker writes here; assembler refs it; stem (01-f1) = composition id
|
||||
- duration: 7.198s # = span length; the assembler reads this for cumulative data-start
|
||||
- span_sec: [0.0, 7.198] # track seconds; frames tile the track
|
||||
@@ -39,6 +40,7 @@ avoid: ["generic slideshow", "tiny unreadable hero text"]
|
||||
- feel: accelerating onset stream into a held downbeat
|
||||
|
||||
### Groups
|
||||
|
||||
- **g1** — template: `intro-kinetic-cascade`
|
||||
- span_sec: [0.0, 4.017] # frame-LOCAL build is 0-based; these are TRACK seconds (worker subtracts frame start)
|
||||
- params: { theme: "light", icon: "bolt", phrases: "[…]", climax: "{…}" }
|
||||
@@ -51,13 +53,14 @@ avoid: ["generic slideshow", "tiny unreadable hero text"]
|
||||
- copy: ["BUILD", "SHIP", "REPEAT"]
|
||||
|
||||
## Frame 2 — f2
|
||||
|
||||
…
|
||||
```
|
||||
|
||||
## Frame block — required fields
|
||||
|
||||
| field | meaning |
|
||||
| --- | --- |
|
||||
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| heading `## Frame N — <frame_id>` | `frame_id` matches the `src` stem; `N` = the 1-based index. |
|
||||
| `src` | `compositions/frames/NN-<frame_id>.html` — where the worker writes; the assembler references it. Stem = `data-composition-id`. |
|
||||
| `duration` | the frame span length in seconds (e.g. `7.198s`) — **required**; the assembler sums these for cumulative `data-start`. |
|
||||
|
||||
@@ -25,46 +25,55 @@ Pacing tag: every template below is **beat_cut** except `held-message-living-fie
|
||||
---
|
||||
|
||||
### card-flyby
|
||||
|
||||
- **What** — a depth column of cards rolls forward through perspective; each landing beat tumbles the next card into the front slot with a solid colored wipe, the old front falls toward camera, dwells shrink card-to-card so the deck accelerates into a held final card.
|
||||
- **Reach for it when** — a stream of discrete onsets that **accelerate** (gaps shrinking / a build into a downbeat) and you want to flash a **sequence of items** — titles, projects, posters, tiles — one per hit, climaxing on a held card.
|
||||
- **Params** — `theme`, `bgColor`, `cards`, `landings`, `yaw`
|
||||
|
||||
### held-message-living-field · phrase_flow
|
||||
|
||||
- **What** — a readable mark (logo / word / title) held dead still over a soft, color-shifting blurred field; only the field breathes.
|
||||
- **Reach for it when** — a **calm / sparse** stretch with an onset desert — energy present but few or no onsets (a held pad or riser); you have one word or mark to hold and let breathe.
|
||||
- **Params** — `markText`, `titleText`, `tagText`, `palette`, `flowSpeed`, `duration`
|
||||
|
||||
### held-text-strobe-burst
|
||||
|
||||
- **What** — a dead-still word whose letters flip through texture-filled frames (texture-clipped fill + per-frame tint + bg color) every ~3 frames, in short bursts pinned to a roll.
|
||||
- **Reach for it when** — a **dense, hard-hitting roll / fill** and a single word you want to strobe through textures for a few bars. (Ships texture-mask PNGs under `assets/`.)
|
||||
- **Params** — `markText`, `fontStyle`, `markScale`, `idleColor`, `idleInk`, `frames`, `strobePlan`, `decor`, `duration`
|
||||
|
||||
### intro-kinetic-cascade
|
||||
|
||||
- **What** — a line laid out as a sequence of big editorial **phrases** (each a stacked poster with one enlarged hero word), revealed word-by-word on its anchors, hard cut between phrases, climaxing on a phrase that slides in with a swappable ringing **icon** (bell / cursor / sparkle / emoji / SVG).
|
||||
- **Reach for it when** — an **intro / opening statement**: a short line to land word-by-word as big type, climaxing on one keyword + an icon. Medium-or-more energy, steady grid.
|
||||
- **Params** — `theme`, `icon`, `phrases`, `climax`
|
||||
|
||||
### logo-split-lockup-pulse
|
||||
|
||||
- **What** — a two-part mark joined at center splits left↔right to open a gap, grows a center word-lockup one word per onset (key word lands on the downbeat surge), snap-closes on a hit, then pulses with the beat.
|
||||
- **Reach for it when** — a short **logo / brand sting** (not a typed sentence): fast dense onsets + a sustained roll bed to pulse on, with a left/right bracketing mark.
|
||||
- **Params** — `bgColor`, `markColor`, `textColor`, `leftMark`, `rightMark`, `word1`, `word2`, `word3`, `word4`
|
||||
|
||||
### poster-tile-mosaic
|
||||
|
||||
- **What** — a packed **mosaic** of different-sized colored tiles that tessellate to fill the frame (no overlap), driven by interchangeable beat-synced operations: staggered enter/exit, locked global recolor, snake-fill + overlay.
|
||||
- **Reach for it when** — many discrete, individually-placeable onsets (a hit for every tile move) with distinct sub-phases you want articulated differently (accumulate → recolor-on-roll → fill-then-drop). A dense section best held as **one** rich tile program rather than split.
|
||||
- **Params** — `bgColor`, `tiles`, `bands`, `gap`, `showText`, `labels`, `program`
|
||||
|
||||
### roll-flipbook-word-cycle
|
||||
|
||||
- **What** — a hi-hat roll drives a centred word that flips every 16th-note through a word list; optionally the flicker resolves and locks into a final phrase.
|
||||
- **Reach for it when** — a **fast sustained-fill roll** (hundreds of hits/min, ~16th-note) with no single readable message — fill the roll with a rapidly-cycling word flipbook.
|
||||
- **Params** — `bgColor`, `textColor`, `accentColor`, `flipWords`, `resolveText`, `periodChar`
|
||||
|
||||
### split-anchor-word-slot
|
||||
|
||||
- **What** — a held left anchor column of fixed-word rows beside a torn-paper word-slot box on the right, driven by beat-synced operators: anchor lock-in, slot word-group cycle (in/out + per-line color), full-scene background flip, per-beat jitter, box-zoom exit wipe. Row count + number of flips are data.
|
||||
- **Reach for it when** — a short section with a **held idea** (a brand / name to anchor on the left) **and** a stream of onsets popping separate words on the right, plus a dense run to ride a shake on and a strong downbeat to wipe out into.
|
||||
- **Params** — `bgColor`, `anchors`, `theme`, `showText`, `program`
|
||||
|
||||
### typewriter-phrase-keyword-shuffle
|
||||
|
||||
- **What** — words type in one-per-onset to spell a phrase, then one keyword cycles typefaces on the beat while everything else holds dead still.
|
||||
- **Reach for it when** — a steady grid with a **continuous onset stream** (no desert): a phrase to type out, then a keyword to shuffle. The inverse of `held-message-living-field` (which wants an onset desert).
|
||||
- **Params** — `bgColor`, `textColor`, `accentColor`, `lead1`, `lead2`, `lead3`, `keyword`, `periodChar`
|
||||
|
||||
@@ -61,7 +61,9 @@ if (G.canvas) {
|
||||
if (Number.isFinite(c.w)) WIDTH = c.w;
|
||||
if (Number.isFinite(c.h)) HEIGHT = c.h;
|
||||
} catch {
|
||||
anomalies.push(`could not JSON.parse canvas frontmatter: ${G.canvas} — using ${WIDTH}×${HEIGHT}`);
|
||||
anomalies.push(
|
||||
`could not JSON.parse canvas frontmatter: ${G.canvas} — using ${WIDTH}×${HEIGHT}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +90,9 @@ for (const f of manifest.frames) {
|
||||
const compId = basename(f.src).replace(/\.html?$/i, "");
|
||||
const inner = readFileSync(compAbs, "utf8");
|
||||
if (!inner.trim() || !/<\w/.test(inner))
|
||||
die(`${label}: ${f.src} is empty/blank — the frame-worker wrote a partial file. Re-dispatch it.`);
|
||||
die(
|
||||
`${label}: ${f.src} is empty/blank — the frame-worker wrote a partial file. Re-dispatch it.`,
|
||||
);
|
||||
if (
|
||||
!inner.includes(`data-composition-id="${compId}"`) &&
|
||||
!inner.includes(`data-composition-id='${compId}'`)
|
||||
|
||||
@@ -25,7 +25,8 @@ function die(m) {
|
||||
const fromDir = flag("from", null);
|
||||
if (!fromDir) die("missing --from <srcDir>");
|
||||
const fromAbs = resolve(fromDir);
|
||||
if (!existsSync(fromAbs) || !statSync(fromAbs).isDirectory()) die(`--from is not a directory: ${fromAbs}`);
|
||||
if (!existsSync(fromAbs) || !statSync(fromAbs).isDirectory())
|
||||
die(`--from is not a directory: ${fromAbs}`);
|
||||
const hyperframesDir = resolve(flag("hyperframes", "."));
|
||||
const into = flag("into", "");
|
||||
const destDir = join(hyperframesDir, "assets", into);
|
||||
@@ -50,6 +51,8 @@ for (const name of readdirSync(fromAbs)) {
|
||||
landed.push(join("assets", into, basename(name)));
|
||||
}
|
||||
|
||||
console.log(`✓ stage-assets: ${staged} copied, ${skipped} already present → ${join("assets", into)}/`);
|
||||
console.log(
|
||||
`✓ stage-assets: ${staged} copied, ${skipped} already present → ${join("assets", into)}/`,
|
||||
);
|
||||
for (const l of landed) console.log(` ${l}`);
|
||||
if (staged === 0 && skipped === 0) console.log(` (no media files found in ${fromAbs})`);
|
||||
|
||||
@@ -73,7 +73,9 @@ for (const f of frames) {
|
||||
sum = r3(sum);
|
||||
const tileTarget = Number.isFinite(declaredDur) ? declaredDur : audioDur;
|
||||
if (tileTarget != null && Math.abs(sum - tileTarget) > 0.1)
|
||||
errors.push(`frame durations sum to ${sum}s but the track is ${tileTarget}s — frames must tile it gap-free`);
|
||||
errors.push(
|
||||
`frame durations sum to ${sum}s but the track is ${tileTarget}s — frames must tile it gap-free`,
|
||||
);
|
||||
|
||||
// ---------- group checks (warns) — parse RAW text ----------
|
||||
const FRAME_HEAD = /^##\s+(?:frame|scene|section)\b/i;
|
||||
@@ -116,7 +118,9 @@ const framesWithGroups = new Set(blocks.map((b) => b.frameLabel));
|
||||
for (const f of frames) {
|
||||
const lbl = `Frame ${f.number ?? ""} — ${f.title ?? f.index}`.replace(/\s+—\s+$/, "");
|
||||
if (![...framesWithGroups].some((s) => s.includes(String(f.title ?? "")))) {
|
||||
warns.push(`${lbl}: no parseable groups (expected \`- **gN** — template|free_design|asset …\`)`);
|
||||
warns.push(
|
||||
`${lbl}: no parseable groups (expected \`- **gN** — template|free_design|asset …\`)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +135,9 @@ for (const b of blocks) {
|
||||
warns.push(`${gid}: template \`${id}\` not found at templates/${id}/index.html`);
|
||||
}
|
||||
if (b.kind === "asset" && b.pacing === "phrase_flow" && /beat_cut/.test(blockText))
|
||||
warns.push(`${gid}: beat_cut asset treatment on a phrase_flow frame — use ken_burns/crossfade instead`);
|
||||
warns.push(
|
||||
`${gid}: beat_cut asset treatment on a phrase_flow frame — use ken_burns/crossfade instead`,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- report ----------
|
||||
@@ -141,4 +147,6 @@ if (errors.length) {
|
||||
console.error(`\nvalidate-plan: ${errors.length} error(s), ${warns.length} warning(s)`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`✓ validate-plan: ${frames.length} frames tile ${sum}s; ${blocks.length} groups; ${warns.length} warning(s)`);
|
||||
console.log(
|
||||
`✓ validate-plan: ${frames.length} frames tile ${sum}s; ${blocks.length} groups; ${warns.length} warning(s)`,
|
||||
);
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
You build one frame's composition file: `compositions/frames/<frame_id>.html`. Siblings build
|
||||
the other frames in parallel. The generic HyperFrames law — sub-composition shape, timeline
|
||||
registration, determinism, layout — lives in `hyperframes-core` (`references/sub-compositions.md`
|
||||
+ `determinism-rules.md` + `data-attributes.md`); read it first. This file covers the
|
||||
|
||||
- `determinism-rules.md` + `data-attributes.md`); read it first. This file covers the
|
||||
music-specific part.
|
||||
|
||||
Your job: **follow the manual, fetch the materials, assemble.** The storyboard tells you WHAT
|
||||
|
||||
Reference in New Issue
Block a user