diff --git a/.claude/skills/choreo-create/SKILL.md b/.claude/skills/choreo-create/SKILL.md index 6a37ca3776d..82f08919a04 100644 --- a/.claude/skills/choreo-create/SKILL.md +++ b/.claude/skills/choreo-create/SKILL.md @@ -42,11 +42,11 @@ composition idea) in `packages/choreo-gallery/realm/demos/.json`. The test app keeps a copy in `packages/choreo-test-app/app/content/demo-lessons.json` for its guide pages; change both, since the realm check fails when they differ. The concept guides are `packages/choreo-test-app/app/content/guides/*.md`. -`packages/choreo-gallery/docs/api-inventory.json` maps public APIs to concept +`packages/choreo-gallery/tools/api-inventory.json` maps public APIs to concept guides; look up the relevant entries rather than loading it whole. For a tutorial, isolate one observable relationship and predict the result of a parameter change. The docs-only spatial/film studies in -`packages/choreo-gallery/docs/docs-studies-plan.md` are planned, not shipped. +`packages/choreo-gallery/realm/design/docs-studies-plan.md` are planned, not shipped. Every control must supply a named variable consumed by that example. Show units, use meaningful ranges, and retain source defaults. The test app's diff --git a/.claude/skills/choreo-create/references/composition-recipes.md b/.claude/skills/choreo-create/references/composition-recipes.md index 2d9020a08eb..d631119c1b6 100644 --- a/.claude/skills/choreo-create/references/composition-recipes.md +++ b/.claude/skills/choreo-create/references/composition-recipes.md @@ -24,4 +24,4 @@ Use the catalog to resolve the components named below. These are starting points - **Architectural film:** the `towers` and `sagrada` film routes and their film/score components. Reuse the Film graph: spine, chapters, shots, picture actors, type, voice, and joins. Distinguish exact seeking from the chased camera's cut mode. - **Guided gallery:** `packages/choreo-test-app/app/components/widget-room.gts`, and `widget-quick-tour.ts`, `widget-tour.ts` and `widget-narration.ts` in `packages/choreo-test-app/app/lib/`. Organize chapters around capabilities, score the camera and actions together, and budget live demos to the active view. Preserve a preview until the replacement is visibly ready. -Read `packages/choreo-gallery/docs/film.md` for as-built Film contracts, `packages/choreo-gallery/docs/demo-recording.md` for ownership, and `packages/choreo-gallery/docs/dom-in-3d.md` for the DOM projection boundary. Design documents sometimes describe proposed capabilities; use exported source to confirm availability. +Read `packages/choreo-gallery/realm/docs/film.md` for as-built Film contracts, `packages/choreo-gallery/realm/docs/demo-recording.md` for ownership, and `packages/choreo-gallery/realm/docs/dom-in-3d.md` for the DOM projection boundary. Design documents sometimes describe proposed capabilities; use exported source to confirm availability. diff --git a/.claude/skills/choreo-create/references/spatial-and-film.md b/.claude/skills/choreo-create/references/spatial-and-film.md index 73598aad219..9929dc9349b 100644 --- a/.claude/skills/choreo-create/references/spatial-and-film.md +++ b/.claude/skills/choreo-create/references/spatial-and-film.md @@ -4,17 +4,17 @@ Choose the boundary before choosing a demo. Read only the row needed for the tas Guide paths below are under `packages/choreo-test-app/app/content/guides/`; implementation exports remain the authority for accepted arguments. -| Intent | Read | Minimal source reference | -| -------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------- | -| Frame or pan across DOM | spatial-frame.md | examples/camera.gts; long-take/board.gts | -| Relative pose or coordinate conversion | spatial-relative.md; spatial-coordinates.md | `packages/choreo/src/choreo.gts` vocabulary; `app/lib/css3d.ts` | -| 3D camera path and look target | spatial-camera3d.md; spatial-paths.md | examples/mockup.gts; examples/long-take.gts | -| Place live DOM on a mesh plane | spatial-dom.md; spatial-compositing.md | examples/mockup.gts; `packages/choreo-gallery/docs/dom-in-3d.md` | -| Film graph, shots and picture actor | film-graph.md; film-picture.md | tower-film.gts; sagrada-film.gts | -| Seek, pause or capture | film-transport.md; film-schedule.md | `packages/choreo-gallery/docs/demo-recording.md`; `packages/choreo-player/src/` | -| Joins and readiness | film-joins.md; film-seam-readiness.md | `packages/choreo/src/film/` | -| Separate clips, audio and captions | film-audio.md; film-audio-mix.md; film-typography.md | `app/lib/widget-narration.ts`; `@cardstack/choreo/film` exports | -| World labels and adjustments | film-world-annotations.md; film-adjustments.md | picture capability implementation and its Film score | +| Intent | Read | Minimal source reference | +| -------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------- | +| Frame or pan across DOM | spatial-frame.md | examples/camera.gts; long-take/board.gts | +| Relative pose or coordinate conversion | spatial-relative.md; spatial-coordinates.md | `packages/choreo/src/choreo.gts` vocabulary; `app/lib/css3d.ts` | +| 3D camera path and look target | spatial-camera3d.md; spatial-paths.md | examples/mockup.gts; examples/long-take.gts | +| Place live DOM on a mesh plane | spatial-dom.md; spatial-compositing.md | examples/mockup.gts; `packages/choreo-gallery/realm/docs/dom-in-3d.md` | +| Film graph, shots and picture actor | film-graph.md; film-picture.md | tower-film.gts; sagrada-film.gts | +| Seek, pause or capture | film-transport.md; film-schedule.md | `packages/choreo-gallery/realm/docs/demo-recording.md`; `packages/choreo-player/src/` | +| Joins and readiness | film-joins.md; film-seam-readiness.md | `packages/choreo/src/film/` | +| Separate clips, audio and captions | film-audio.md; film-audio-mix.md; film-typography.md | `app/lib/widget-narration.ts`; `@cardstack/choreo/film` exports | +| World labels and adjustments | film-world-annotations.md; film-adjustments.md | picture capability implementation and its Film score | Component paths are relative to `packages/choreo-test-app/app/components/`; `app/` paths are relative to `packages/choreo-test-app`. Other paths are repository-relative. Use the catalog @@ -31,6 +31,6 @@ nested cameras; independent hard-coded durations drift. A render barrier means the requested picture is ready, not merely that an arbitrary delay expired. For a tutorial, prefer a tiny procedural scene that isolates one relationship. -Use `packages/choreo-gallery/docs/docs-studies-plan.md` for the planned docs-only studies. They are not +Use `packages/choreo-gallery/realm/design/docs-studies-plan.md` for the planned docs-only studies. They are not shipped examples. Keep full museum films as composition references rather than forcing their assets, narration and lighting into every camera exercise. diff --git a/.claude/skills/choreo-regions/SKILL.md b/.claude/skills/choreo-regions/SKILL.md index d3919bfd578..0ff366f0f65 100644 --- a/.claude/skills/choreo-regions/SKILL.md +++ b/.claude/skills/choreo-regions/SKILL.md @@ -108,4 +108,4 @@ barrier between tests — required, or passes leak across tests. Ground truth: `packages/choreo-test-app/app/components/examples/far-match.gts` (three bays, the on/off switch is just id scoping) and its Deep Dive note `packages/choreo-test-app/app/components/notes/far.gts`; design in -`packages/choreo-gallery/docs/nested-choreo.md`. +`packages/choreo-gallery/realm/docs/nested-choreo.md`. diff --git a/.claude/skills/choreo-scene/SKILL.md b/.claude/skills/choreo-scene/SKILL.md index a041c4f17b1..716f48e1476 100644 --- a/.claude/skills/choreo-scene/SKILL.md +++ b/.claude/skills/choreo-scene/SKILL.md @@ -102,7 +102,7 @@ document-global on purpose (chrome and outlet are separate regions). An inner `` is a separate scene — the outer region does not see its participants. Regions, far matching (an id leaving one region and appearing in another), and cross-region measurement: `choreo-regions`, and -`packages/choreo-gallery/docs/nested-choreo.md`. +`packages/choreo-gallery/realm/docs/nested-choreo.md`. ## When NOT @@ -113,4 +113,4 @@ One element, no ordering, no cross-element measurement → `motion-element` / Canonical demos, in `packages/choreo-test-app/app/components/examples/`: `inbox.gts` (beacons), `sequence.gts`, `interrupt.gts`, `slides.gts`, `far-match.gts`, `split-view.gts` (function values), -`lists.gts`. Design doc: `packages/choreo-gallery/docs/choreography.md`. +`lists.gts`. Design doc: `packages/choreo-gallery/realm/docs/choreography.md`. diff --git a/.claude/skills/motion-pattern/SKILL.md b/.claude/skills/motion-pattern/SKILL.md index ee976744c5b..f0328963cd8 100644 --- a/.claude/skills/motion-pattern/SKILL.md +++ b/.claude/skills/motion-pattern/SKILL.md @@ -68,10 +68,10 @@ Dives — the _reasoning_ behind each pattern choice (why the inbox uses beacons, how the interruption model holds, what the playhead samples). Read the note when modifying its demo. -Docs: `packages/choreo-gallery/docs/guide.md` (tutorial), the package READMEs +Docs: `packages/choreo-gallery/realm/docs/guide.md` (tutorial), the package READMEs `packages/glimmer-motion/README.md` and `packages/choreo/README.md` (API -reference), `packages/choreo-gallery/docs/choreography.md` + -`packages/choreo-gallery/docs/nested-choreo.md` (Choreo design). +reference), `packages/choreo-gallery/realm/docs/choreography.md` + +`packages/choreo-gallery/realm/docs/nested-choreo.md` (Choreo design). A card in a realm imports the same libraries through host's realm shims, under different rules (no direct motion-dom imports, state shared with host). For diff --git a/packages/choreo-film-app/app/components/films/sagrada-score.gts b/packages/choreo-film-app/app/components/films/sagrada-score.gts index 7745e8a209f..133da9c442e 100644 --- a/packages/choreo-film-app/app/components/films/sagrada-score.gts +++ b/packages/choreo-film-app/app/components/films/sagrada-score.gts @@ -2,7 +2,7 @@ * SAGRADA FAMÍLIA, AS A GRAPH — the film's score in the film vocabulary. * * Generated from the beat table in `lib/films/sagrada.ts` by - * `docs/film-graph/tograph.mjs` (PREFIX='@f.'), so every pose, phrase, + * `tools/tograph.mjs` (PREFIX='@f.'), so every pose, phrase, * stamp and trace is the table's; only the shape differs. It takes the * vocabulary as an argument so it can be rendered inside `` * and, in `tests/integration/film/graph-test.gts`, inside a bare diff --git a/packages/choreo-film-app/app/components/films/towers-score.gts b/packages/choreo-film-app/app/components/films/towers-score.gts index 28a1b620f14..097cbe7d6ad 100644 --- a/packages/choreo-film-app/app/components/films/towers-score.gts +++ b/packages/choreo-film-app/app/components/films/towers-score.gts @@ -2,7 +2,7 @@ * TOWERS, AS A GRAPH — the film's score in the film vocabulary. * * Generated from the beat table in `lib/films/towers.ts` by - * `docs/film-graph/tograph.mjs` (PREFIX='@f.'), so every pose, phrase, + * `tools/tograph.mjs` (PREFIX='@f.'), so every pose, phrase, * stamp and trace is the table's; only the shape differs. It takes the * vocabulary as an argument so it can be rendered inside `` * and, in `tests/integration/film/graph-test.gts`, inside a bare diff --git a/packages/choreo-film-app/app/lib/films/sagrada.ts b/packages/choreo-film-app/app/lib/films/sagrada.ts index cff09890b7e..94c2037466d 100644 --- a/packages/choreo-film-app/app/lib/films/sagrada.ts +++ b/packages/choreo-film-app/app/lib/films/sagrada.ts @@ -17,7 +17,7 @@ import { hex, RAD } from '@cardstack/choreo/film/math'; /** * SAGRADA — a film, at `/sagrada`. * - * The second reference film for the film construct (notes/film-construct.md), + * The second reference film for the film construct (packages/choreo-gallery/realm/design/film-construct.md), * and the first one cut ON it: everything here is the shot list, the * script, the chapters, the year clock, the traces sampled off the model, * and the front and back matter. The engine — the chased lens, the joins, @@ -167,7 +167,7 @@ export const VO_GAIN: Record = { * files in `public/sagrada/vo/`. The kinetic type is paced against the VOICE, * not against the beat: the last cue should land as the line is finishing, * whether the read is four seconds or thirteen. Estimating this went wrong - * once already (see notes/towers-vo.md) — so it is measured, and a re-record + * once already (see packages/choreo-gallery/realm/design/towers-vo.md) — so it is measured, and a re-record * means re-measuring. A beat missing from here paces against its own length. */ export const VO_SECS: Record = { diff --git a/packages/choreo-film-app/app/lib/films/towers.ts b/packages/choreo-film-app/app/lib/films/towers.ts index 510ee9d07cf..9aea6378bdd 100644 --- a/packages/choreo-film-app/app/lib/films/towers.ts +++ b/packages/choreo-film-app/app/lib/films/towers.ts @@ -10,7 +10,7 @@ import { hex, RAD } from '@cardstack/choreo/film/math'; /** * TOWERS — a film, at `/towers`. * - * The first reference film for the film construct (notes/film-construct.md), + * The first reference film for the film construct (packages/choreo-gallery/realm/design/film-construct.md), * now cut ON it: what is left here is the shot list, the script, the * chapters, the six climates, the traces sampled off the keep, and the * front and back matter in the film's own Japanese voice. The engine — @@ -226,7 +226,7 @@ export const VO_GAIN: Record = {}; * files in `public/towers/vo/`. The kinetic type is paced against the VOICE, * not against the beat: the last cue should land as the line is finishing, * whether the read is four seconds or thirteen. Estimating this went wrong - * once already (see notes/towers-vo.md) — so it is measured, and a re-record + * once already (see packages/choreo-gallery/realm/design/towers-vo.md) — so it is measured, and a re-record * means re-measuring. A beat missing from here paces against its own length. */ export const VO_SECS: Record = { diff --git a/packages/choreo-film-app/tests/integration/film/graph-test.gts b/packages/choreo-film-app/tests/integration/film/graph-test.gts index 244b101690e..16f359182a4 100644 --- a/packages/choreo-film-app/tests/integration/film/graph-test.gts +++ b/packages/choreo-film-app/tests/integration/film/graph-test.gts @@ -4,7 +4,7 @@ * Both reference films exist twice on this branch: as the beat tables the * engine has always run (`lib/films/*.ts`) and as graphs written in the * film vocabulary (`components/films/*-score.gts`, generated from those - * tables by `docs/film-graph/tograph.mjs`). These render each graph + * tables by `tools/tograph.mjs`). These render each graph * inside a bare `` and assert that what it compiles to is the * table — every row, every key — and the same measured reads. That is * the whole of Phase 2's first claim, and it is what lets the films diff --git a/packages/choreo-film-app/tools/tograph.mjs b/packages/choreo-film-app/tools/tograph.mjs new file mode 100644 index 00000000000..af89dc5227d --- /dev/null +++ b/packages/choreo-film-app/tools/tograph.mjs @@ -0,0 +1,398 @@ +// Turn a film's BEATS table (as written in the .gts) into the proposed +// graph syntax: chapters as blocks, shots as named steps, joins as +// siblings, everything else attached. Values are carried verbatim. +import fs from 'node:fs'; + +const [, , file] = process.argv; +// the prefix the vocabulary is reached by: `f.` inside , `@f.` in a component that takes it as an argument +const F = process.env.PREFIX ?? 'f.'; +const src = fs.readFileSync(file, 'utf8'); + +/* ---- a tiny tokenizer for the object literal ----------------------- */ +function skipWs(s, i) { + for (;;) { + while (i < s.length && /\s/.test(s[i])) { + i++; + } + if (s.startsWith('/*', i)) { + i = s.indexOf('*/', i) + 2; + continue; + } + if (s.startsWith('//', i)) { + i = s.indexOf('\n', i); + continue; + } + return i; + } +} +function readValue(s, i) { + i = skipWs(s, i); + const c = s[i]; + if (c === "'" || c === '"' || c === '`') { + let j = i + 1; + while (s[j] !== c) { + if (s[j] === '\\') { + j++; + } + j++; + } + return [s.slice(i, j + 1), j + 1]; + } + if (c === '{' || c === '[' || c === '(') { + const close = { '{': '}', '[': ']', '(': ')' }[c]; + let depth = 0, + j = i; + for (; j < s.length; j++) { + const d = s[j]; + if (d === "'" || d === '"' || d === '`') { + const [, k] = readValue(s, j); + j = k - 1; + continue; + } + if (s.startsWith('/*', j)) { + j = s.indexOf('*/', j) + 1; + continue; + } + if (d === c) { + depth++; + } else if (d === close) { + depth--; + if (depth === 0) { + break; + } + } + } + return [s.slice(i, j + 1), j + 1]; + } + let j = i; + while (j < s.length && !/[,}\]\n]/.test(s[j])) { + if (s[j] === '(') { + const [, k] = readValue(s, j); + j = k; + continue; + } + j++; + } + return [s.slice(i, j).trim(), j]; +} +function readObject(s) { + // s is '{ ... }' + const out = []; + let i = skipWs(s, 1); + while (i < s.length - 1) { + i = skipWs(s, i); + if (s[i] === '}') { + break; + } + const m = /^([A-Za-z_][A-Za-z0-9_]*)\s*:/.exec(s.slice(i)); + if (!m) { + i++; + continue; + } + i += m[0].length; + const [v, j] = readValue(s, i); + out.push([m[1], v]); + i = skipWs(s, j); + if (s[i] === ',') { + i++; + } + } + return out; +} +function readArray(s) { + const out = []; + let i = skipWs(s, 1); + while (i < s.length - 1) { + i = skipWs(s, i); + if (s[i] === ']') { + break; + } + const [v, j] = readValue(s, i); + out.push(v); + i = skipWs(s, j); + if (s[i] === ',') { + i++; + } + } + return out; +} + +/* ---- TS value → Glimmer expression --------------------------------- */ +function str(v) { + const inner = v.slice(1, -1).replace(/\\'/g, "'").replace(/"/g, '\\"'); + return `"${inner}"`; +} +function expr(v) { + v = v.trim(); + if (/^['"`]/.test(v)) { + return str(v); + } + if (v[0] === '{') { + return `(hash ${readObject(v) + .map(([k, x]) => `${k}=${expr(x)}`) + .join(' ')})`; + } + if (v[0] === '[') { + return `(array ${readArray(v).map(expr).join(' ')})`; + } + const call = /^([A-Za-z_][A-Za-z0-9_.]*)\((.*)\)$/s.exec(v); + if (call) { + const args = readArray('[' + call[2] + ']'); + return `(${call[1]} ${args.map(expr).join(' ')})`; + } + if (/^-?\d/.test(v) || v === 'true' || v === 'false' || v === 'null') { + return v; + } + if (/^[A-Z][A-Z0-9_]*$/.test(v)) { + return v; + } // a constant + if (/^-\s*[A-Z]/.test(v)) { + return `(neg ${v.slice(1).trim()})`; + } + if (/[-+*/]/.test(v)) { + return `(calc "${v}")`; + } // arithmetic: honest about it + return v; +} +function attr(k, v) { + return `@${k}={{${expr(v)}}}`; +} + +/* ---- the grouping: which key goes where ----------------------------- */ +// the shot's own facts; the head pose is flattened onto the shot, the +// tail pose is a child, an eye-level walk is a child +const SHOT = [ + 'id', + 'ticks', + 'cut', + 'lead', + 'lift', + 'bob', + 'hold', + 'follow', + 'to', + 'dissolve', + 'quality', +]; +const TYPE = ['mode', 'kicker', 'kanji', 'romaji', 'gloss', 'says', 'bare']; +// ADJUSTMENTS: parameter sets the PICTURE declares, yielded as contextual +// components under f.picture — the package knows none of these names. +// Look is a FILTER (it processes the frame); the rest hold values. +const FILTERS = [ + [F + 'picture.Look', ['grade', 'look', 'lut']], + [ + F + 'picture.Weather', + ['theme', 'hours', 'hoursOver', 'haze', 'rain', 'wx', 'wxCut', 'lightning'], + ], + [F + 'picture.Winter', ['winter']], + [F + 'picture.Sun', ['sun']], + [F + 'picture.Light', ['rim']], + [F + 'picture.Set', ['city', 'grass']], + [F + 'picture.Build', ['build', 'buildBy', 'settle', 'style']], +]; +const RENAME = { + kanji: 'word', + romaji: 'reading', + build: 'clock', + buildBy: 'by', + style: 'subject', + hoursOver: 'over', +}; +function pose(tag, v) { + return `${tag} ${readObject(v) + .map(([k, x]) => attr(k, x)) + .join(' ')}`; +} + +/* ---- walk the table ------------------------------------------------- */ +const start = src.indexOf('const BEATS: Beat[] = ['); +const body = readValue(src, start + 'const BEATS: Beat[] = '.length)[0]; +// beats plus the banner comments between them +const items = []; +{ + let i = 1; + while (i < body.length - 1) { + const ws = skipWs(body, i); + const gap = body.slice(i, ws); + const banner = /\/\*\s*-+\s*\*\n\s*\*\s*(.+?)\n/.exec(gap); + if (banner) { + items.push({ banner: banner[1].trim() }); + } + i = ws; + if (body[i] === ']' || i >= body.length - 1) { + break; + } + const [v, j] = readValue(body, i); + items.push({ beat: readObject(v) }); + i = skipWs(body, j); + if (body[i] === ',') { + i++; + } + } +} + +const chapters = JSON.parse(process.env.CHAPTERS); +const out = []; +const P = (s) => out.push(s); +let openCh = -1; +const ind = (n) => ' '.repeat(n); + +for (const it of items) { + if (it.banner) { + P(''); + P(`${ind(3)}{{! ${it.banner} }}`); + continue; + } + const b = Object.fromEntries(it.beat); + const ch = Number(b.ch); + if (ch !== openCh) { + if (openCh >= 0) { + P(`${ind(3)}`); + } + const c = chapters[ch]; + const tints = + (c.tint ? ` @tint="${c.tint}"` : '') + + (c.tintD ? ` @tintD="${c.tintD}"` : ''); + P( + `${ind(3)}<${F}Chapter @n="${c.n}" @title="${c.title}"${c.grade ? ` @grade="${c.grade}"` : ''}${c.lut ? ` @lut="${c.lut}"` : ''}${tints}>`, + ); + openCh = ch; + } + const id = b.id.slice(1, -1); + // the seam INTO this shot, as a sibling before it + if (b.join) { + const j = [`@presentation=${str(b.join)}`]; + if (b.dipTo) { + j.push(`@to=${str(b.dipTo)}`); + } + P(`${ind(4)}<${F}Join ${j.join(' ')} />`); + } + const shotArgs = SHOT.filter((k) => k in b).map((k) => + k === 'id' ? `@name="${id}"` : attr(k, b[k]), + ); + const head = b.cam ? readObject(b.cam).map(([k, x]) => attr(k, x)) : []; + P(`${ind(4)}<${F}Shot ${[...shotArgs, ...head].join(' ')}>`); + if (b.toCam) { + P(`${ind(5)}<${pose(F + 'To', b.toCam)} />`); + } + if (b.eye) { + const e = Object.fromEntries(readObject(b.eye)); + P( + `${ind(5)}<${F}Eye @from={{${expr(e.at)}}}${e.to ? ` @to={{${expr(e.to)}}}` : ''} @fov={{${e.fov}}} />`, + ); + } + const type = TYPE.filter((k) => k in b); + if (type.length) { + P( + `${ind(5)}<${F}Type ${type.map((k) => attr(RENAME[k] ?? k, b[k])).join(' ')} />`, + ); + } + if (b.vo) { + P( + `${ind(5)}<${F}Voice @line=${str(b.vo)} @read={{get VO_SECS "${id}"}}${b.hush ? ' @hush={{true}}' : ''} />`, + ); + } else if (b.hush) { + P(`${ind(5)}<${F}Voice @hush={{true}} />`); + } + for (const [tag, keys] of FILTERS) { + const have = keys.filter((k) => k in b); + if (!have.length) { + continue; + } + // an object-valued knob (sun: {az, el}; winter: {gust, pack}) is its own + // filter with the object's keys as flat, typed arguments; null clears it + if ( + have.length === 1 && + (b[have[0]].startsWith('{') || b[have[0]] === 'null') && + ['sun', 'winter'].includes(have[0]) + ) { + const v = b[have[0]]; + P( + `${ind(5)}<${tag} ${ + v === 'null' + ? '@off={{true}}' + : readObject(v) + .map(([k, x]) => attr(k, x)) + .join(' ') + } />`, + ); + continue; + } + P( + `${ind(5)}<${tag} ${have.map((k) => attr(RENAME[k] ?? k, b[k])).join(' ')} />`, + ); + } + if (b.mix) { + P( + `${ind(5)}<${F}sound.Mix ${readObject(b.mix) + .map(([k, v]) => attr(k, v)) + .join(' ')} />`, + ); + } + if (b.stamp) { + const s = Object.fromEntries(readObject(b.stamp)); + P( + `${ind(5)}<${F}Stamp @year={{${s.y}}}${s.at ? ` @at={{${s.at}}}` : ''} />`, + ); + } + if (b.sky) { + P( + `${ind(5)}<${F}Sky ${readObject(b.sky) + .map(([k, v]) => attr(k, v)) + .join(' ')} />`, + ); + } + if (b.mark) { + P( + `${ind(5)}<${F}Mark ${readObject(b.mark) + .map(([k, v]) => attr(k, v)) + .join(' ')} />`, + ); + } + if (b.trace) { + for (const t of readArray(b.trace)) { + P( + `${ind(5)}<${F}Trace ${readObject(t) + .map(([k, v]) => attr(k, v)) + .join(' ')} />`, + ); + } + } + if (b.cycle) { + P(`${ind(5)}<${F}Lineup @steps={{${expr(b.cycle)}}} />`); + } + if (b.photo) { + const p = Object.fromEntries(readObject(b.photo)); + P(`${ind(5)}<${F}Attach @lane={{1}}>`); + P( + `${ind(6)}<${F}Insert @src=${str(p.src)} @caption=${str(p.caption)} @credit=${str(p.credit)} />`, + ); + P(`${ind(5)}`); + } + if (b.clip) { + const c = Object.fromEntries(readObject(b.clip)); + const win = []; + if (c.at) { + win.push(`@at={{${c.at}}}`); + } + if (c.for) { + win.push(`@for={{${c.for}}}`); + } + if (c.end) { + win.push(`@end=${str(c.end)}`); + } + const kind = c.kind ? c.kind.slice(1, -1) : 'video'; + const comp = { freeze: 'Freeze', image: 'Still', video: 'Video' }[kind]; + const rest = Object.entries(c) + .filter(([k]) => !['at', 'for', 'end', 'kind'].includes(k)) + .map(([k, v]) => attr(k, v)); + P(`${ind(5)}<${F}Attach @lane={{2}} ${win.join(' ')}>`); + P(`${ind(6)}<${F}${comp} ${rest.join(' ')} />`); + P(`${ind(5)}`); + } + P(`${ind(4)}`); +} +if (openCh >= 0) { + P(`${ind(3)}`); +} +fs.writeFileSync(`/dev/stdout`, out.join('\n') + '\n'); diff --git a/packages/choreo-film-app/vendor/sylva-scene.ts b/packages/choreo-film-app/vendor/sylva-scene.ts index 26550261d42..7654f9a790f 100644 --- a/packages/choreo-film-app/vendor/sylva-scene.ts +++ b/packages/choreo-film-app/vendor/sylva-scene.ts @@ -19,7 +19,7 @@ * * WHY THIS SCENE. It was picked for its geometry, not its looks: a moss root * with an arch and a hollow under it is a shape that can stand in FRONT of a - * DOM card, which is exactly the case `docs/dom-in-3d.md` lists as unproven. + * DOM card, which is exactly the case `packages/choreo-gallery/realm/docs/dom-in-3d.md` lists as unproven. * A flat backdrop could not test occlusion at all. * * The one lucky alignment: its camera is already authored in pixels — @@ -3170,7 +3170,7 @@ export function createSylva(hosts: SylvaHosts): Sylva { * branch it is naming the moment the composition reflows. * * The proxy is the whole occlusion answer, and it is the piece - * `docs/dom-in-3d.md` calls unproven: a plane at the card's pose with + * `packages/choreo-gallery/realm/docs/dom-in-3d.md` calls unproven: a plane at the card's pose with * `NoBlending`, which writes its own alpha straight into the framebuffer and * so cuts a card-shaped hole for the DOM underneath. Being ordinary * geometry, it is depth-tested like everything else — so moss standing diff --git a/packages/choreo-gallery/.prettierignore b/packages/choreo-gallery/.prettierignore index 17c4b0670ef..362735eafbf 100644 --- a/packages/choreo-gallery/.prettierignore +++ b/packages/choreo-gallery/.prettierignore @@ -1,9 +1,8 @@ node_modules/ tmp/ out/ -# the film-graph sketches: proposals in a syntax that does not exist yet -docs/film-graph/*.gts -docs/film-graph/*.mjs +# the film-graph sketches, kept as written: proposals in a syntax that does not exist yet +realm/design/film-graph/*.gts.md # the film app's build (choreo-film-app), and the films' vendored and generated media realm/film-app/ realm/asset/ diff --git a/packages/choreo-gallery/.template-lintrc.cjs b/packages/choreo-gallery/.template-lintrc.cjs index eb34d497271..70c1a07e87e 100644 --- a/packages/choreo-gallery/.template-lintrc.cjs +++ b/packages/choreo-gallery/.template-lintrc.cjs @@ -3,9 +3,6 @@ module.exports = { extends: ['recommended', '@cardstack/template-lint:recommended'], plugins: ['../template-lint/plugin'], - // the film-graph sketches are proposals written in a syntax that does not - // exist yet: read them, don't lint them - ignore: ['docs/film-graph/**'], rules: { // Only a
element's is interactive, so buttons in its // open panel are not nested controls. diff --git a/packages/choreo-gallery/README.md b/packages/choreo-gallery/README.md index 85f28efdabf..6e307d83f26 100644 --- a/packages/choreo-gallery/README.md +++ b/packages/choreo-gallery/README.md @@ -5,30 +5,32 @@ Boxel realm content. Everything in [`realm/`](realm/) is the realm: cards, their data, and the modules they import. Cards import `glimmer-motion` and `@cardstack/choreo` like any other card; the host serves both to realms. -The rest of the package is tooling and source material that is not served: -`tools/` (the realm check and the film and widget tools), `scripts/`, -`docs/` and `notes/`. The agent skills for working on the libraries and the +The rest of the package is tooling that is not served: `tools/` (the realm +check and the film and widget tools) and `scripts/`. The agent skills for working on the libraries and the gallery are in the monorepo's `.claude/skills/` (`motion-pattern` routes to the rest). ## The realm -| Path | What it is | -| ------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `realm.json` | The realm's config. | -| `index.json` | The gallery: a `ChoreoGallery` card linking every demo, in the order the gallery shows them. | -| `gallery.gts` | `ChoreoGallery`. Its isolated format is the whole site; embedded and fitted are a summary. | -| `demo.gts` | `GalleryDemo`, one gallery entry: its title, group, lede, API pills, usage example, walkthrough and teaching lesson. | -| `demos/.json` | One `GalleryDemo` per demo. The catalog is this data. | -| `shell/` | The site's components: the frame, the grid, the demo page, the How panel and the pickers. | -| `stages/.gts` | A demo's own `GalleryDemo` subclass, supplying its live stage and its notes. | -| `notes/` | Each demo's long-form notes (its Deep Dive). | -| `asset/` | Media the demos load: the films' narration, looks, textures, picture pages, vendored three.js and posters. | -| `film-app/` | The films' own app, built from `packages/choreo-film-app` (see [The films](#the-films)). Not committed. | -| `reel/` | The feature reel: three demos, live, in one camera'd world under a title, brand and timecode plane. `feature-reel.json`. | -| `lib/` | The crossing between the grid and a demo page, tempo and theme preferences, syntax highlighting, and `onstage`/`restWhenOff`. | -| `theme.json` | The fonts the gallery is set in. The palette is declared by the gallery's root element, `shell/choreo-root.gts`. | -| `*.test.gts` | The gallery's tests, run by `boxel test`. | +| Path | What it is | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `realm.json` | The realm's config. | +| `index.json` | The gallery: a `ChoreoGallery` card linking every demo, in the order the gallery shows them. | +| `gallery.gts` | `ChoreoGallery`. Its isolated format is the whole site; embedded and fitted are a summary. | +| `demo.gts` | `GalleryDemo`, one gallery entry: its title, group, lede, API pills, usage example, walkthrough and teaching lesson. | +| `demos/.json` | One `GalleryDemo` per demo. The catalog is this data. | +| `shell/` | The site's components: the frame, the grid, the demo page, the How panel and the pickers. | +| `stages/.gts` | A demo's own `GalleryDemo` subclass, supplying its live stage and its notes. | +| `notes/` | Each demo's long-form notes (its Deep Dive). | +| `docs/` | The long-form documentation, as Markdown: the guide, the choreography model, the construct reference, Film. `docs/README.md` indexes it. | +| `design/` | Design history: design records, handoffs, measurements, and the films' scripts. Drafts by definition. | +| `licenses/` | Attribution for third-party material the demos use. | +| `asset/` | Media the demos load: the films' narration, looks, textures, picture pages, vendored three.js and posters. | +| `film-app/` | The films' own app, built from `packages/choreo-film-app` (see [The films](#the-films)). Not committed. | +| `reel/` | The feature reel: three demos, live, in one camera'd world under a title, brand and timecode plane. `feature-reel.json`. | +| `lib/` | The crossing between the grid and a demo page, tempo and theme preferences, syntax highlighting, and `onstage`/`restWhenOff`. | +| `theme.json` | The fonts the gallery is set in. The palette is declared by the gallery's root element, `shell/choreo-root.gts`. | +| `*.test.gts` | The gallery's tests, run by `boxel test`. | A demo's stage and its long-form notes are components, not data. A demo that has them adopts from its own subclass of `GalleryDemo`, in @@ -139,13 +141,21 @@ pnpm test # boxel test realm `pnpm lint:realm` (`tools/check-realm.mjs`) checks the realm's contents against the libraries it documents: -- `docs/api-inventory.json` names every public export of glimmer-motion, +- `tools/api-inventory.json` names every public export of glimmer-motion, Choreo and choreo-player, and every member of the `ChoreoContext` and `FilmVocabulary` vocabularies, and maps each to a guide; - every `demos/*.json` is well formed, `index.json` links exactly that set, and each demo has a complete teaching lesson naming a known guide; - every guide meets the prose floor, has a preamble, and links only to guides - that exist. + that exist; +- `docs/` and `design/` link only to realm files and headings that exist; +- every import written in `docs/`, the guides and the package READMEs names + an export its package has, so none still reaches Choreo or Film through + `glimmer-motion` (`design/` sketches APIs that were never built, so its + imports aren't checked); +- every `choreo.boxel.site` URL names a file in the realm, and every link into + the monorepo names a file that exists, along with the heading any `#fragment` + names. The guides themselves, and the guide pages that embed the demos, are still `choreo-test-app` sources; the check reads them from there. diff --git a/packages/choreo-gallery/docs/choreo-player.md b/packages/choreo-gallery/docs/choreo-player.md deleted file mode 100644 index 7a88aeafa0f..00000000000 --- a/packages/choreo-gallery/docs/choreo-player.md +++ /dev/null @@ -1,151 +0,0 @@ -# Shipping `choreo-player` 1.0 - -`choreo-player` is the small boundary between a public Choreo run and an -external clock. It lets preview tools and deterministic renderers control -Choreo without changing `glimmer-motion` or Choreo core. - -## Package boundary - -The npm package has two exports: - -- `choreo-player` — dependency-free play, pause, absolute render, rate, and - replacement-run synchronization; -- `choreo-player/hyperframes` — a dependency-free binding to HyperFrames' - native `hf-seek`/`waitUntil()` browser protocol. - -It does not ship UI, demo state reducers, DOM discovery, generated media, a -GSAP facade, or HyperFrames itself. - -The player accepts a structural run instead of importing the -`glimmer-motion` runtime: - -```ts -interface ChoreoRun { - readonly duration: number; - time: number; - speed: number; - play(): void; - pause(): void; -} -``` - -That is the complete surface it uses. A public `ChoreoRun` yielded by -`` satisfies the interface without an adapter. - -## Explicit ownership - -Every player requires a `runs()` provider: - -```ts -const player = createChoreoPlayer({ - duration: 7, - prepare: () => { - recording = true; - mountScoredPass(); - }, - runs: () => (recording && scoreRun === currentRun ? [scoreRun] : []), -}); -``` - -This is intentionally not a global registry. A recorder must not touch an -interactive run merely because both exist on the same page. The external -clock arms ownership through `prepare()` on its first call and retains it for -the page's recording session. - -Read [demo-recording.md](demo-recording.md) before making a demo recordable. -Its interactive-test requirement is part of the release contract. - -## A frame is an application transaction - -`player.renderAt(t)` controls Choreo runs. The application still owns the -tracked state that decides what Glimmer renders. A complete recording method -therefore reconstructs both: - -```ts -async function renderAt(time: number) { - await player.renderAt(time, { settle: false }); - - state = foldEventsThrough(time); - await glimmerRenderFinished(); - - await player.renderAt(time, { settle: false }); - paintImperativeReadouts(time); -} -``` - -The first seek makes a newly mounted score and its compiled cues available. -The state fold makes the application a function of time. The awaited Glimmer -pass commits that state, and the final seek reasserts the Choreo still on the -DOM that will be captured. - -## HyperFrames is optional - -Applications that only need an external transport install `choreo-player` -alone: - -```bash -pnpm add @cardstack/choreo-player -``` - -A video project installs its own exact HyperFrames CLI as a development tool: - -```bash -pnpm add --save-dev hyperframes@0.8.14 -``` - -The local scripts then use the pinned binary: - -```json -{ - "scripts": { - "preview": "hyperframes preview build", - "check": "hyperframes check build", - "render": "hyperframes render build --workers 10" - } -} -``` - -Do not install HyperFrames globally, bundle it into `choreo-player`, declare it -as a peer dependency, or use an unpinned `npx --yes hyperframes` in a durable -composition. - -Connect the complete application transaction to capture: - -```ts -import { bindHyperframes } from '@cardstack/choreo-player/hyperframes'; - -const disconnect = bindHyperframes({ renderAt }); -``` - -Parallel safety belongs to this `renderAt()` implementation. Ten workers are -a verified result for the Playhead fixture, not a package-wide guarantee. - -## Reference proof - -`videos/playhead-recording` is the installation and render fixture. It: - -- builds the real Glimmer gallery application; -- copies the published-shape `choreo-player/hyperframes` module into the - composition; -- mounts the real Playhead Glimmer component with no added border; -- binds native `hf-seek` to its awaited frame transaction; -- pins HyperFrames 0.8.14 locally; and -- renders 1920×1080, 30fps, with ten workers. - -The proof was compared with a same-build one-worker render. FFmpeg measured -54.15 dB average PSNR and 0.999583 SSIM, and found no post-opening frame that -matched the opening frame at any worker boundary. - -## 1.0 release gate - -Before publishing: - -1. Build and run the package contract tests. -2. Run `pnpm pack` and inspect the tarball contents. -3. Install the tarball in the reference fixture. -4. Run the complete interactive Ember suite. -5. Run HyperFrames check and render with one and ten workers. -6. Scan every worker boundary for an opening-frame insertion. - -The npm artifact should contain only `dist`, `README.md`, `LICENSE`, and -`package.json`. diff --git a/packages/choreo-gallery/docs/film-graph/tograph.mjs b/packages/choreo-gallery/docs/film-graph/tograph.mjs deleted file mode 100644 index 4837f056a8d..00000000000 --- a/packages/choreo-gallery/docs/film-graph/tograph.mjs +++ /dev/null @@ -1,219 +0,0 @@ -// Turn a film's BEATS table (as written in the .gts) into the proposed -// graph syntax: chapters as blocks, shots as named steps, joins as -// siblings, everything else attached. Values are carried verbatim. -import fs from 'node:fs'; - -const [, , file, name] = process.argv; -// the prefix the vocabulary is reached by: `f.` inside , `@f.` in a component that takes it as an argument -const F = process.env.PREFIX ?? 'f.'; -const src = fs.readFileSync(file, 'utf8'); - -/* ---- a tiny tokenizer for the object literal ----------------------- */ -function skipWs(s, i) { - for (;;) { - while (i < s.length && /\s/.test(s[i])) i++; - if (s.startsWith('/*', i)) { i = s.indexOf('*/', i) + 2; continue; } - if (s.startsWith('//', i)) { i = s.indexOf('\n', i); continue; } - return i; - } -} -function readValue(s, i) { - i = skipWs(s, i); - const c = s[i]; - if (c === "'" || c === '"' || c === '`') { - let j = i + 1; - while (s[j] !== c) { if (s[j] === '\\') j++; j++; } - return [s.slice(i, j + 1), j + 1]; - } - if (c === '{' || c === '[' || c === '(') { - const close = { '{': '}', '[': ']', '(': ')' }[c]; - let depth = 0, j = i; - for (; j < s.length; j++) { - const d = s[j]; - if (d === "'" || d === '"' || d === '`') { const [, k] = readValue(s, j); j = k - 1; continue; } - if (s.startsWith('/*', j)) { j = s.indexOf('*/', j) + 1; continue; } - if (d === c) depth++; - else if (d === close) { depth--; if (depth === 0) break; } - } - return [s.slice(i, j + 1), j + 1]; - } - let j = i; - while (j < s.length && !/[,}\]\n]/.test(s[j])) { - if (s[j] === '(') { const [, k] = readValue(s, j); j = k; continue; } - j++; - } - return [s.slice(i, j).trim(), j]; -} -function readObject(s) { - // s is '{ ... }' - const out = []; - let i = skipWs(s, 1); - while (i < s.length - 1) { - i = skipWs(s, i); - if (s[i] === '}') break; - const m = /^([A-Za-z_][A-Za-z0-9_]*)\s*:/.exec(s.slice(i)); - if (!m) { i++; continue; } - i += m[0].length; - const [v, j] = readValue(s, i); - out.push([m[1], v]); - i = skipWs(s, j); - if (s[i] === ',') i++; - } - return out; -} -function readArray(s) { - const out = []; - let i = skipWs(s, 1); - while (i < s.length - 1) { - i = skipWs(s, i); - if (s[i] === ']') break; - const [v, j] = readValue(s, i); - out.push(v); - i = skipWs(s, j); - if (s[i] === ',') i++; - } - return out; -} - -/* ---- TS value → Glimmer expression --------------------------------- */ -function str(v) { - const inner = v.slice(1, -1).replace(/\\'/g, "'").replace(/"/g, '\\"'); - return `"${inner}"`; -} -function expr(v) { - v = v.trim(); - if (/^['"`]/.test(v)) return str(v); - if (v[0] === '{') return `(hash ${readObject(v).map(([k, x]) => `${k}=${expr(x)}`).join(' ')})`; - if (v[0] === '[') return `(array ${readArray(v).map(expr).join(' ')})`; - const call = /^([A-Za-z_][A-Za-z0-9_.]*)\((.*)\)$/s.exec(v); - if (call) { - const args = readArray('[' + call[2] + ']'); - return `(${call[1]} ${args.map(expr).join(' ')})`; - } - if (/^-?\d/.test(v) || v === 'true' || v === 'false' || v === 'null') return v; - if (/^[A-Z][A-Z0-9_]*$/.test(v)) return v; // a constant - if (/^-\s*[A-Z]/.test(v)) return `(neg ${v.slice(1).trim()})`; - if (/[-+*/]/.test(v)) return `(calc "${v}")`; // arithmetic: honest about it - return v; -} -function attr(k, v) { return `@${k}={{${expr(v)}}}`; } - -/* ---- the grouping: which key goes where ----------------------------- */ -// the shot's own facts; the head pose is flattened onto the shot, the -// tail pose is a child, an eye-level walk is a child -const SHOT = ['id', 'ticks', 'cut', 'lead', 'lift', 'bob', 'hold', 'follow', 'to', 'dissolve', 'quality']; -const TYPE = ['mode', 'kicker', 'kanji', 'romaji', 'gloss', 'says', 'bare']; -// ADJUSTMENTS: parameter sets the PICTURE declares, yielded as contextual -// components under f.picture — the package knows none of these names. -// Look is a FILTER (it processes the frame); the rest hold values. -const FILTERS = [ - [F + 'picture.Look', ['grade', 'look', 'lut']], - [F + 'picture.Weather', ['theme', 'hours', 'hoursOver', 'haze', 'rain', 'wx', 'wxCut', 'lightning']], - [F + 'picture.Winter', ['winter']], - [F + 'picture.Sun', ['sun']], - [F + 'picture.Light', ['rim']], - [F + 'picture.Set', ['city', 'grass']], - [F + 'picture.Build', ['build', 'buildBy', 'settle', 'style']], -]; -const RENAME = { kanji: 'word', romaji: 'reading', build: 'clock', buildBy: 'by', style: 'subject', hoursOver: 'over' }; -function pose(tag, v) { - return `${tag} ${readObject(v).map(([k, x]) => attr(k, x)).join(' ')}`; -} - -/* ---- walk the table ------------------------------------------------- */ -const start = src.indexOf('const BEATS: Beat[] = ['); -const body = readValue(src, start + 'const BEATS: Beat[] = '.length)[0]; -// beats plus the banner comments between them -const items = []; -{ - let i = 1; - while (i < body.length - 1) { - const ws = skipWs(body, i); - const gap = body.slice(i, ws); - const banner = /\/\*\s*-+\s*\*\n\s*\*\s*(.+?)\n/.exec(gap); - if (banner) items.push({ banner: banner[1].trim() }); - i = ws; - if (body[i] === ']' || i >= body.length - 1) break; - const [v, j] = readValue(body, i); - items.push({ beat: readObject(v) }); - i = skipWs(body, j); - if (body[i] === ',') i++; - } -} - -const chapters = JSON.parse(process.env.CHAPTERS); -const out = []; -const P = (s) => out.push(s); -let openCh = -1; -const ind = (n) => ' '.repeat(n); -let prevId = null; - -for (const it of items) { - if (it.banner) { P(''); P(`${ind(3)}{{! ${it.banner} }}`); continue; } - const b = Object.fromEntries(it.beat); - const ch = Number(b.ch); - if (ch !== openCh) { - if (openCh >= 0) P(`${ind(3)}`); - const c = chapters[ch]; - const tints = (c.tint ? ` @tint="${c.tint}"` : '') + (c.tintD ? ` @tintD="${c.tintD}"` : ''); - P(`${ind(3)}<${F}Chapter @n="${c.n}" @title="${c.title}"${c.grade ? ` @grade="${c.grade}"` : ''}${c.lut ? ` @lut="${c.lut}"` : ''}${tints}>`); - openCh = ch; - } - const id = b.id.slice(1, -1); - // the seam INTO this shot, as a sibling before it - if (b.join) { - const j = [`@presentation=${str(b.join)}`]; - if (b.dipTo) j.push(`@to=${str(b.dipTo)}`); - P(`${ind(4)}<${F}Join ${j.join(' ')} />`); - } - const shotArgs = SHOT.filter((k) => k in b).map((k) => (k === 'id' ? `@name="${id}"` : attr(k, b[k]))); - const head = b.cam ? readObject(b.cam).map(([k, x]) => attr(k, x)) : []; - P(`${ind(4)}<${F}Shot ${[...shotArgs, ...head].join(' ')}>`); - if (b.toCam) P(`${ind(5)}<${pose(F + 'To', b.toCam)} />`); - if (b.eye) { const e = Object.fromEntries(readObject(b.eye)); P(`${ind(5)}<${F}Eye @from={{${expr(e.at)}}}${e.to ? ` @to={{${expr(e.to)}}}` : ''} @fov={{${e.fov}}} />`); } - const type = TYPE.filter((k) => k in b); - if (type.length) P(`${ind(5)}<${F}Type ${type.map((k) => attr(RENAME[k] ?? k, b[k])).join(' ')} />`); - if (b.vo) P(`${ind(5)}<${F}Voice @line=${str(b.vo)} @read={{get VO_SECS "${id}"}}${b.hush ? ' @hush={{true}}' : ''} />`); - else if (b.hush) P(`${ind(5)}<${F}Voice @hush={{true}} />`); - for (const [tag, keys] of FILTERS) { - const have = keys.filter((k) => k in b); - if (!have.length) continue; - // an object-valued knob (sun: {az, el}; winter: {gust, pack}) is its own - // filter with the object's keys as flat, typed arguments; null clears it - if (have.length === 1 && (b[have[0]].startsWith('{') || b[have[0]] === 'null') && ['sun', 'winter'].includes(have[0])) { - const v = b[have[0]]; - P(`${ind(5)}<${tag} ${v === 'null' ? '@off={{true}}' : readObject(v).map(([k, x]) => attr(k, x)).join(' ')} />`); - continue; - } - P(`${ind(5)}<${tag} ${have.map((k) => attr(RENAME[k] ?? k, b[k])).join(' ')} />`); - } - if (b.mix) P(`${ind(5)}<${F}sound.Mix ${readObject(b.mix).map(([k, v]) => attr(k, v)).join(' ')} />`); - if (b.stamp) { const s = Object.fromEntries(readObject(b.stamp)); P(`${ind(5)}<${F}Stamp @year={{${s.y}}}${s.at ? ` @at={{${s.at}}}` : ''} />`); } - if (b.sky) P(`${ind(5)}<${F}Sky ${readObject(b.sky).map(([k, v]) => attr(k, v)).join(' ')} />`); - if (b.mark) P(`${ind(5)}<${F}Mark ${readObject(b.mark).map(([k, v]) => attr(k, v)).join(' ')} />`); - if (b.trace) for (const t of readArray(b.trace)) P(`${ind(5)}<${F}Trace ${readObject(t).map(([k, v]) => attr(k, v)).join(' ')} />`); - if (b.cycle) P(`${ind(5)}<${F}Lineup @steps={{${expr(b.cycle)}}} />`); - if (b.photo) { - const p = Object.fromEntries(readObject(b.photo)); - P(`${ind(5)}<${F}Attach @lane={{1}}>`); - P(`${ind(6)}<${F}Insert @src=${str(p.src)} @caption=${str(p.caption)} @credit=${str(p.credit)} />`); - P(`${ind(5)}`); - } - if (b.clip) { - const c = Object.fromEntries(readObject(b.clip)); - const win = []; - if (c.at) win.push(`@at={{${c.at}}}`); - if (c.for) win.push(`@for={{${c.for}}}`); - if (c.end) win.push(`@end=${str(c.end)}`); - const kind = c.kind ? c.kind.slice(1, -1) : 'video'; - const comp = { freeze: 'Freeze', image: 'Still', video: 'Video' }[kind]; - const rest = Object.entries(c).filter(([k]) => !['at', 'for', 'end', 'kind'].includes(k)).map(([k, v]) => attr(k, v)); - P(`${ind(5)}<${F}Attach @lane={{2}} ${win.join(' ')}>`); - P(`${ind(6)}<${F}${comp} ${rest.join(' ')} />`); - P(`${ind(5)}`); - } - P(`${ind(4)}`); - prevId = id; -} -if (openCh >= 0) P(`${ind(3)}`); -fs.writeFileSync(`/dev/stdout`, out.join('\n') + '\n'); diff --git a/packages/choreo-gallery/docs/gallery/BOXEL-DEPLOYMENT.md b/packages/choreo-gallery/docs/gallery/BOXEL-DEPLOYMENT.md deleted file mode 100644 index a34661eda61..00000000000 --- a/packages/choreo-gallery/docs/gallery/BOXEL-DEPLOYMENT.md +++ /dev/null @@ -1,80 +0,0 @@ -# Deploy the gallery to Boxel Site - -This workflow uses your own Boxel account and source realm. No account, realm ID, -published demo URL, or credential is embedded in the repository. Obtain the -published site URL assigned to your realm through Boxel's publishing UI; do not -construct a URL from an account name or assume a particular hostname. - -## Build a standalone release - -From the repository root: - -```sh -pnpm install --frozen-lockfile -pnpm build -APP_BASE=./ APP_LOCATION=hash pnpm --dir test-app exec vite build --outDir ../out/widget-boxel-build -node scripts/package-widget-boxel.mjs -``` - -Output is `out/widget-boxel-publish/iframe/gallery/`. `WIDGET_BUILD` selects -another build directory; `WIDGET_PACKAGE` selects another package directory. -The package includes posters, narration, fonts, models, and demo dependencies. -Use a fresh output directory per release to avoid uploading stale chunks. - -The packager preserves raw browser modules as `.mjs` because Boxel processes -`.js` realm modules. It also rewrites Draco loader filenames and embeds a -same-origin document bridge for Sagrada and Towers. Hash routing keeps nested -demos inside the published directory. These steps are required, not optional -minification or performance tweaks. - -## Authenticate, inspect, and upload - -Install the `@cardstack/boxel-cli` version supported by your Boxel deployment, -then authenticate with `boxel profile add` (browser sign-in). Switch to the -intended profile with `boxel profile switch` when using multiple environments. -Credentials remain in the CLI's local profile storage. - -Set `BOXEL_SOURCE_REALM` to your source realm URL using your shell or local -secret configuration; do not commit its value. Inspect the files first: - -```sh -node scripts/publish-widget-boxel.mjs --dry-run -node scripts/publish-widget-boxel.mjs -``` - -`BOXEL_CLI` optionally selects the CLI executable. `WIDGET_UPLOAD_ROOT` selects -the local upload tree (default `out/widget-boxel-publish`). Paths under this tree -become realm-relative paths. The uploader writes only those files, uploads HTML -last, and does not delete other content. A repeated upload replaces files at the -same paths; use a versioned release subdirectory when you need rollback. - -If delivering a rendered MP4, place it under the upload tree's `media/` directory -before upload. Keep the source render outside Git. - -## Publish and verify - -Set `BOXEL_PUBLISHED_REALM` to the published site URL assigned by Boxel, then: - -```sh -boxel realm indexing-errors --realm "$BOXEL_SOURCE_REALM" -boxel realm publish "$BOXEL_SOURCE_REALM" "$BOXEL_PUBLISHED_REALM" -``` - -Use the CLI's readiness/publishability checks. Do not bypass them with `--force`. -For CLI versions without `realm publish`, use Boxel's publishing UI instead. -Publishing a realm publishes its contents; choose a realm intended for sharing. - -The gallery's path relative to the published root is: - -```text -iframe/gallery/widget-room.html?acceptHeader=application%2Fvnd.card%2Bsource&film=1#/_widgets -``` - -A video path uses `?acceptHeader=video%2Fmp4`. The accept-header query ensures -that a browser receives the HTML or video rather than the card viewer. - -Check the gallery anonymously on desktop and phone, confirm all posters load, -play consecutive full-tour clips, inspect Mockup in 3D, and run the freeze and -handoff checks against the published gallery URL. For MP4 delivery, verify -metadata, playback, seeking, and HTTP byte-range responses. The HTML has -`noindex,nofollow,noarchive`; an unlisted URL is still public to anyone who has it. diff --git a/packages/choreo-gallery/docs/source-repo/AGENTS.md b/packages/choreo-gallery/docs/source-repo/AGENTS.md deleted file mode 100644 index 58c19d34070..00000000000 --- a/packages/choreo-gallery/docs/source-repo/AGENTS.md +++ /dev/null @@ -1,90 +0,0 @@ -# Agent guide — Choreo / glimmer-motion - -One package, two layers. **`glimmer-motion`** binds the Motion (motion.dev) -engine — `motion-dom`, untouched — to Glimmer: the `{{motion}}` modifier, -``, ``, ``, reorder, drag, gestures, -scroll, view transitions. **Choreo** sits on top: `` watches a render -pass, hands its **changeset** (inserted/removed/kept, bounds before and -after) to a timeline you declare in the template. The repo is `choreo`; the -npm package is still `glimmer-motion` — every import in docs and code is the -real one. - -## Layout - -- `packages/glimmer-motion/` — the addon. `src/index.ts` is the public - surface; `src/choreo/` is Choreo's model; ~40 lines of Ember-specific glue, - the rest is host-agnostic. -- `test-app/` — the gallery (every demo in - `app/components/examples/*.gts` is real, catalogued in `app/lib/catalog.ts`) - and the test suite: ported upstream Motion tests (fidelity) + Choreo's - contract suite. -- `docs/guide.md` (tutorial), `README.md` (reference), - `docs/choreography.md` + `docs/nested-choreo.md` (Choreo design). - -## Building from the Gallery - -For recreating demos, combining patterns into applications or films, or adding -live DialKit controls, use `.claude/skills/choreo-create/SKILL.md`. Codex also -discovers the same skill through `.agents/skills/choreo-create`. The documentation -website at `/docs` teaches core glimmer-motion, interactive Choreo, spatial/3D -Choreo, and recorded/film Choreo as four learning paths. - -## Picking an animation pattern - -Before writing any animation, invoke the **`motion-pattern`** skill — it -routes to the right pattern skill. Short form: - -| Need | Use | Skill | -| ------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------- | -| One element animates | `{{motion}}` initial/animate/gestures | `motion-element` | -| Animate on removal/insert | `` + `exit` | `motion-presence` | -| Moved because layout changed; same thing in two places | `layout` / `layoutId` / `` | `motion-layout` | -| Ordered multi-element scenes, z-index windows, fly-to-a-place | `` timeline + `{{beacon}}` | `choreo-scene` | -| Multiple regions; cross-region flights | nested `` + far matching | `choreo-regions` | -| Route/page change | `` for live DOM; snapshot APIs when intentional | `magic-move-navigation` / `motion-page-transition` | -| The gallery ⇄ demo Magic Move recipe | `` pairing | `magic-move-navigation` | -| Pointer-driven movement | `drag` / `` | `motion-drag` | -| Scroll-driven / in-view | `scrollProgress` / `InView` | `motion-scroll` | -| Tests touching motion | `glimmer-motion/test-support` | `motion-testing` | - -Escalate element → presence → layout → Choreo; use the smallest pattern that -states the intent. Two standing vetoes: no view transitions over live -content (snapshots freeze it — use layout animation), and no `setTimeout` -sequencing (that is what `c.Sequence` is for). - -## Three rules that silently break motion in Glimmer - -1. CSS for a motion element goes through the modifier - (`{{motion style=…}}`), never a bound `style=` attribute — Glimmer - rewrites the attribute and wipes the engine's transform. -2. Anything the engine must tween/correct/measure must be declared to it — - a stylesheet `border-radius` on a `layout=true` element distorts; - `style=(styles borderRadius='14px')` doesn't. -3. Exit content rides on what `` yields; a leaving block re-runs - from live state, so don't read moved-on state during an exit. - -## Commands - -```bash -pnpm install -pnpm build # builds the addon (required before lint — glint resolves built declarations) -pnpm lint # eslint + glint + prettier -pnpm test # builds, then runs the suite in Chrome -``` - -Dev server for the gallery: `.claude/launch.json` → `glimmer-motion-tests` -(vite on 4202; `/tests` for the QUnit runner). CI runs `pnpm build` before -`pnpm lint` — keep that order. - -## Conventions - -- `.gts` templates; named exports, no `Component` suffix; Glint signatures - on every public component/modifier. -- Transition constants at module scope, not inline hashes; helpers - (`to`, `spring`, `tween`, `styles`, …) over `(hash …)`. -- `data-test-*` for test hooks; never sleep in tests - (`animationsSettled()`). -- `prefers-reduced-motion` is honoured by default (`@reducedMotion="user"`). - Don't opt out to make a demo look right. -- Dark theme is the reference look; light mode rebinds tokens - (`:root[data-theme='light']`) and must not change dark's resolved values. diff --git a/packages/choreo-gallery/docs/source-repo/README.md b/packages/choreo-gallery/docs/source-repo/README.md deleted file mode 100644 index d0d578aee5b..00000000000 --- a/packages/choreo-gallery/docs/source-repo/README.md +++ /dev/null @@ -1,678 +0,0 @@ -
- -

Choreo

- -**Motion, Choreo-graphed.** -The [Motion](https://motion.dev) engine for Ember — and a timeline for the scene. `` watches a render pass, measures its **changeset** — what was inserted, removed, kept, and where everything stood before and after — and plays a score you declare over it. Magic-Move crossings, gates you click through, wires drawn every frame, values derived from other elements' measurements — all interruptible mid-flight, by design. - -**Live gallery:** [**cardstack.github.io/choreo**](https://cardstack.github.io/choreo/)   ·   46 demos with source and guides - -
- -```gts - - {{#each this.cards key='id' as |card|}} -
{{card.title}}
- {{/each}} - - - - - - -
-``` - -
- -**Declared like a template**   ·   measured like layout   ·   sequenced like a score   ·   scrubbed like a video - -**Interrupted like Keynote**   ·   click again mid-flight and everything re-aims from where it is painted - -
- -

- Mockup: a live DOM interface projected inside a 3D device -

- -**Featured demo: Mockup.** The screen is a real Glimmer interface, projected inside -a 3D device. Its controls remain interactive while Choreo coordinates the screen's -layout transitions and the camera. Switch between 2D and 3D to compare the same -application in both spaces. [Source](test-app/app/components/examples/mockup.gts) · -[How DOM projection works](docs/dom-in-3d.md). - ---- - -## What is Choreo? - -Choreo is two layers in one repo, published as two packages. - -**The choreography layer** is the reason the repo exists: a region-scoped model that Motion's per-element API has no equivalent for. A `` region treats each render pass as one event. It snapshots its participants before the DOM changes, measures them after, and hands the difference — the **changeset** — to a timeline declared right in the template. One score describes the whole scene: what leaves, what flies, what arrives, in what order, on which clock. - -**The binding underneath** is **`glimmer-motion`**: `motion-dom` — Motion's framework-free engine, untouched — bound to Glimmer as a modifier and a handful of components. Layout animation, shared-element transitions, presence, variants, drag, reorder, scroll. It is a complete, usable port in its own right (held to Motion's own test suites, pixel for pixel), and it is what the choreography stands on. Both ship in the one `glimmer-motion` package. - -The second package, **`choreo-player`**, is a dependency-free headless transport for clocking Choreo runs from outside — a video renderer, a capture worker, a scrub UI ([packages/choreo-player](packages/choreo-player)). - -> Naming: Motion (motion.dev) is the library formerly called framer-motion; its React package is still published as `framer-motion`, which is why upstream paths in this repo read `packages/framer-motion/…`. The repo is **Choreo**; the published package is still **`glimmer-motion`** — one npm name, unchanged, and every import in these docs is the real one. - -**New here?** [docs/guide.md](docs/guide.md) teaches the binding from a Glimmer card rather than a React translation table. Then: [choreography.md](docs/choreography.md) (the model and its boxel-motion ancestry), [choreo-constructs.md](docs/choreo-constructs.md) (the full construct reference), [step-vocabulary.md](docs/step-vocabulary.md) (the open vocabulary: anchors, composite steps, derived values), [nested-choreo.md](docs/nested-choreo.md) (regions and far matching), [choreo-splices.md](docs/choreo-splices.md) (cuts as a first-class citizen of a score), [film.md](docs/film.md) (the `` construct, as built), [planes-and-cameras.md](docs/planes-and-cameras.md) and [dom-in-3d.md](docs/dom-in-3d.md) (a `` region as a plane in a three.js scene), [demo-recording.md](docs/demo-recording.md) (external clocks), [choreo-player.md](docs/choreo-player.md) (the headless transport), and [postmortem-follow.md](docs/postmortem-follow.md) — the engineering post-mortem that shaped the derived-value API, kept because the mistakes teach more than the result. - -`docs/` is the documentation. The workshop behind it — design records, handoffs, measurements, the films' scripts and wall text — is in [notes/](notes), which is drafts by definition: some of it describes things proposed and never built, or built and since changed. - -- [Why choreography?](#why-choreography) -- [Install](#install) -- [At a glance: the design decisions](#at-a-glance-the-design-decisions) -- [A tour of the vocabulary](#a-tour-of-the-vocabulary) -- [Where it earns its keep](#where-it-earns-its-keep) -- [Interruption, tested](#interruption-tested) -- [External clocks: choreo-player](#external-clocks-choreo-player) -- [DOM in 3D: c.Camera3D](#dom-in-3d-ccamera3d) -- [The binding underneath](#the-binding-underneath) · [Why the engine is untouched](#why-the-engine-is-untouched) · [How it was made](#how-it-was-made) · [Fidelity](#fidelity) -- [API](#api) · [React → Glimmer](#react--glimmer) · [Three rules React does not need](#three-rules-react-does-not-need) -- [Guides and tuning](#guides-and-tuning) · [3D gallery](#3d-gallery) -- [Testing](#testing) · [Architecture](#architecture) · [Development](#development) · [Roadmap](#roadmap) · [Credits](#credits) - -## Why choreography? - -Element-level animation answers "how does this element move?" — and for one element it is the right question. But most of what a product ships is _scenes_: a card opens into a page, a row grows into a detail panel, a slide builds in steps, a dropped card springs into its slot while its neighbours make room. In a scene, elements don't have independent motion — they have **relationships**: this one leaves _before_ that one flies; this badge rides _that_ card; this panel grows out of _that_ row; everything freezes _while_ the hero crosses. - -Per-element APIs make you reconstruct those relationships by hand: measure rects imperatively, spawn clone overlays, chain `setTimeout`s, hide the real element while a copy flies, and hope nobody clicks mid-flight. Every one of those hand-built mechanisms was found, catalogued, and replaced during the port of a real product workspace onto this library — the clones, the `afterRender` measurement hooks, the hand-matched timer pairs, all of it collapsed into declared timelines ([christse/bento-boxel#1](https://github.com/christse/bento-boxel/pull/1) is the receipts). - -The changeset is what makes the declarative form possible. Because the region measures **everything, both sides of every pass**, the timeline never needs a hand-measured rect: a step names _which sprites_ (by id, role, or what happened to them — `inserted`, `removed`, `moved`, `claimed`) and _what to do_, and the geometry is already there. And because every value is a function of the run's clock over measured endpoints, a second click mid-flight simply re-measures and re-aims — interruption is not an edge case to be defended against, it is the normal case the whole model is built around. - -## Install - -The verified setup for this checkout packs the local packages into a generated -app. Start with the -[complete first-app tutorial](test-app/app/content/guides/core-first-app.md): it -generates a clean Ember/Vite consumer and walks through installation, a working -scene, and verification. Current package manifests are version `0.0.0`; this -workflow does not depend on an npm release. [mise](https://mise.jdx.dev/) -provides the pinned Node and pnpm versions, and it ignores a configuration file -until it is trusted: - -```sh -mise trust -mise install -pnpm install -node scripts/create-tutorial-app.mjs /tmp/my-choreo-app -cd /tmp/my-choreo-app -mise trust -pnpm install -pnpm build -pnpm start -``` - -The generator builds `glimmer-motion` and `choreo-player` and packs them into the -app's `vendor/` directory, alongside the declared Motion peers. The app does not -depend on the repository after it is generated; generate a new one to pick up -library changes. The addon is a v2 addon with Glint signatures and TypeScript -declarations; `choreo-player` remains a separate package. - -## At a glance: the design decisions - -Twelve decisions that make Choreo feel the way it does. Each one is enforced by tests, and most were paid for the hard way. - -- **The scene is the unit.** A region owns a subtree; participants opt in with `{{motion id= role=}}`; each render pass is one changeset, one timeline, one run. Two regions are two scenes with separate clocks — and nesting is legal, because an inner region measures its own pass correctly even while an outer one is flying the box it lives in. -- **Leavers belong to the timeline.** A removed participant stays on screen, locked where it stood, exactly as long as the score names it — the region parks it in an orphan layer and releases it when its row ends. No `` inside a scene, no clones, no "keep this in state a bit longer" flags. -- **Identity pairs across renders.** An inserted id that replaces a removed one _claims_ it: one flight, two skins, crossfaded over the moving box. The same bare id in two different regions pairs across them — **far matching** — and the arrival flies from the departure's box as one continuous move. Pairing is also what makes "back" trail-correct for free: a closing page flies to wherever its identity truly renders next. -- **z-index is a window; elevation is a layer.** `c.Hold` sets any property for the span of its block and hands it back — the whole "timed z-index flag" genre, retired. `c.Raise` is for what z-index cannot say: promotion to a real layer above every stacking context and overflow clip in the region, for the window, with the slot held by a placeholder. -- **A gate is a pause; back is a hold.** `c.Gate` parks the run until `advance()` (Keynote's click), `@delay` opens it by itself, and `advance()` mid-segment is the click-through: complete instantly, park at the gate. `run.retreat()` is the Keynote back rule as a verb: land parked at the previous gate, everything ahead re-closed, and **hold** — nothing plays, no `@delay` gate self-opens, until the user advances and the build replays forward. -- **Interruption is the doctrine.** A sprite mid-flight reads as _moved_ on every re-pass — its `initial` is the painted box, by design — so a replacement run starts from what is on screen, with velocities handed over. And a replacement pass may not drop a flight: anything the prior run was driving that the new score doesn't name gets a synthesized continuation to its rest. (Without that rule, any unrelated re-render could snap a flying element home in one frame; the test that caught it watches every frame.) -- **Unnamed means no animation.** A reflow the score doesn't name lands instantly — apps rely on it. The continuity rule above is scoped to what was _flying_; layout stays layout. -- **A beacon is a place, not an identity.** `{{beacon 'trash'}}` publishes a box; `@from`/`@to` borrow it as one end of a flight. The bin never joins a changeset, never deforms, and the pass measures it fresh each time — including _after_ the layout it sits in has re-solved, which kills the "predict where the slot will be" computation entirely. -- **Derived values never read the page.** `c.Follow` computes one sprite's properties from another's _measurements_: each source arrives as `{ from, to, now }` — resting boxes from the pass, plus a current position composed from the values the run is already driving — and the follower's own box arrives as `rest`, which cannot contain what the follower writes. Pure by construction, so a follower cannot feed its own output back or force a style recalculation mid-move. The API looks this way because the obvious API (live boxes) shipped first and failed five ways; [the post-mortem](docs/postmortem-follow.md) is part of the documentation. -- **Some ink stands.** An open-ended `c.Tether` draws its wire every frame and every still, indefinitely — a standing run that reports settled while it ticks. Tether, raise, and scroll cues compare by identity across re-renders, so a busy page cannot tear a standing wire down and refade it sixty times a second. -- **The vocabulary is open.** Blocks answer to the anchor system (`@name` a block, `@at={{at 'carry'}}` a step against it), and a composite step is a published seam: subclass `StepComponent`, return a node tree, and your app speaks its own words — ``, ``, `` — with `generic: true` children that yield to anything more specific in the same score. -- **Time is a value.** `run.time` is settable in both directions; setting it across a gate parks there; a still is a computed frame, not a paused accumulation. That is what makes the Playhead demo scrubbable, the presentation reversible, and the whole thing renderable from an external clock. - -## A tour of the vocabulary - -One scene, ten statements. The fixture is the same shape throughout: participants declared with `{{motion id= role=}}`, a timeline beside them. - -**1 · The changeset, sequenced.** Steps select what _happened_ — `c.inserted` / `c.removed` / `c.kept` / `c.moved` / `c.still` (optionally by role), `c.role`, `c.id`, `c.claimed` / `c.received` for the two halves of a pair, and `c.onstage(query)` to touch only what a viewport can see. - -```gts - - - - - -``` - -A sequence can follow a spring — its length is computed with the engine's own generator, not guessed. - -**2 · z-index as a window.** - -```gts - - - - -``` - -The hold spans the block and releases; `@fill={{true}}` keeps it; `@duration` gives it its own window. Any property, not just z. - -**3 · The crossing — Keynote's Magic Move as one step.** For a view or route swap where the same identities exist on both sides: - -```gts - - {{#if (crossingActive)}} - - {{/if}} - {{outlet}} - -``` - -Leaves dissolve as the paired elements lift off; arrivals land near the settle; only what a viewport can see animates; the crossfade **carries color** (alpha is turned into an actual solid mid-flight so the ground never leaks through the flying box, and handed back to the stylesheet on landing). Shape matching follows the **matching-snapshot rule**: mark the real subject with `data-choreo-substance`, or `pack="content"` on type so the shrink-wrap (the ink) is the subject even when CSS stretched the frame. The entering subject is scaled uniformly _by width_ to fill the exiting subject's width, tops pinned, any vertical overflow cropped at the bottom by a clip window that travels with the box — nothing ever stretches. `c.Crossing @size` defaults to `'crop'`; `'scale'` and `true` are there when you mean them. A sibling step naming a role the crossing would have touched takes that sprite away from it (the yield rule) — a canned move with per-element overrides, no exclusion syntax. - -**4 · Gates, forward and back.** A presentation is gates all the way down: - -```gts - - - {{! opens itself — a preamble beat }} - - {{! waits for the click }} - -``` - -`ctx.run.advance()` opens the gate (or completes the segment and parks — the click-through). `ctx.run.retreat()` steps one build back and _holds_: gates ahead re-close, `@delay` gates do not self-open, and the next advance replays the build. Text can be delivered by word or character (`@by`, `@order`, stagger) as part of any tween. - -**5 · A value derived, not tweened.** The badge rides the card; the shadow spreads by how far the card is from home: - -```ts -const pin = ({ rest, sources }: DeriveContext) => { - const card = sources[0]!.now; - return { x: card.x + card.width - (rest.x + rest.width) }; -}; -``` - -```gts - -``` - -`@read` runs every frame and every scrubbed still, sees only the pass's measurements, and is right on frames nobody planned — including the frame after a mid-flight retarget. It may write transform, opacity, and filter; never layout. - -**6 · Wires.** Geometry drawn continuously between two sprites: - -```gts - -``` - -`@path` receives both boxes in region space each frame and returns SVG path data; the region hosts the layer. No `@duration` means a standing wire — drawn for as long as the score stands, redrawn through anything that moves either end (the Wires demo drags its notes through three drafts of copy). - -**7 · A place, not an identity.** - -```gts -… - -``` - -`@from` works the same way (out of the compose button; from where a drag let go — `c.gesture` borrows the live pointer). The bin never morphs; the pass measures it after layout has settled, so a destination that a re-solve just moved is simply _read_. - -**8 · Elevation and scroll as steps.** - -```gts - - - - -``` - -The scroll animates the sprite's own scroll container and yields to the user's wheel; the flash's _timing_ lives in the timeline while its _fade_ stays in the stylesheet's transition. `c.Raise` promotes a sprite to the region's elevated layer for its window — above every stacking context and clip — and `c.Camera` drives the region's own frame (zoom to a sprite, hold others steady) on the same clock as everything else. - -**9 · Anchors and composite steps.** Name a block, aim at it, and say your app's own words once: - -```gts -class Carry extends StepComponent { - node(): TimelineNode { - const { of, name, spring = carry } = this.args; - return { - at: this.args.at, - children: [ - { generic: true, kind: 'move', of, spring }, - { generic: true, kind: 'hold', of, props: { zIndex: 3 } }, - ], - kind: 'parallel', - name, - }; - } -} -``` - -```gts - - - - -``` - -`at('carry')` / `after('carry')` place steps against the named block; the composite's `generic` children yield their sprites to anything more specific. `c.Crossing` itself is written against this same public seam — the canned step is not privileged. - -**10 · Regions and far matching.** Two ``s are two scenes; give an element the same bare id in both and a pass that removes it from one while inserting it in the other pairs them at a render barrier — measure, match, run, with no frame painted between — and the receiver flies from the sender's box in page space. Scoping the id is precisely how you turn it off. ([docs/nested-choreo.md](docs/nested-choreo.md)) - -Arming — _when_ a timeline should exist — is a first-class helper, because the same ids exist on ordinary passes too: - -```ts -import { createArming } from '@cardstack/choreo'; -const crossing = createArming(); -crossing.begin(region); // watches the run, survives mid-flight replacement, stands down on settle -crossing.active(); // tracked — render the timeline only while true -``` - -## Where it earns its keep - -These are not hypotheticals — each row is a shipped pattern, from the gallery or from the product-workspace port that drove the vocabulary: - -| The scene | The score | -| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | -| Card ⇄ page navigation (gallery ⇄ demo, tile ⇄ document) | `` + `c.Crossing` — real elements, no snapshots, live content never freezes | -| Master–detail: a row grows into a panel | `c.Move @from={{c.beacon row}}` `@size='scale'` — the real panel flies, the solver's re-layout is simply measured | -| Drag-to-shelve: release point → slot | counterpart pairing — the painted box at release _is_ the start | -| A card transmutes into its schema node | beacon flight out of a closing sheet, still measurable mid-exit | -| Slide decks with builds | `c.Gate` / `@delay` / `advance()` / `retreat()` — the Keynote rules | -| Annotations wired to their anchors | standing `c.Tether`s, redrawn through every reflow and every flight | -| "Jump to it and flash it" | `c.Scroll` + `c.Hold` — one clock instead of a scroll racing a classList write racing a timer | -| A badge riding a flying card; a shadow reading lift | `c.Follow` — right on the frames a tween would have had to predict | -| Timed highlight/z states (`justDropped`, pulse flags) | `c.Hold` windows — the timer-and-flag genre, retired | -| Scrubbing, replays, video export | `run.time` + gates + `choreo-player` | -| A live UI inside a device, on camera | `c.Camera3D` — the DOM is a plane in the scene; the shot seeks like any other score | -| A leaderboard that reorders when a result lands | `c.inserted` / `c.moved` / `c.removed` over ONE tracked write — no row is told where to go | -| A simulation running beside a score | the loop owns the physics and the chase camera; the region owns the scene change. Knowing which is which is the skill | - -## Interruption, tested - -The doctrine is that a second click mid-flight is _normal input_. The mechanics that make it true: `initial` is the painted box on every re-pass; velocities hand over to the replacement run; orphans aloft keep their run through unrelated re-renders; unnamed flying sprites get continuations; a fast keep declines pure-noise passes outright (fingerprinting the _composed_ offset chain, so a moved wrapper that is itself no participant still reads as the reflow it is). - -The test method matches: watch the primary element **per frame, in absolute coordinates**, normalize steps by the sample gap (a starved rAF observer merges engine frames; a real teleport pays inside one), and bound against measured travel — because a co-moving pin can stay perfect through a jump a person cannot miss. The suite's interruption soak hammers the real gallery components with clicks at 0 / 60 / 220 ms. - -```ts -import { - setupMotion, - animationsSettled, - bounds, - shape, - orphanCount, - strandedTransforms, - live, - liveAll, - advanceGate, - seekTo, - velocityOf, -} from 'glimmer-motion/test-support'; -``` - -| export | what it is for | -| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| `setupMotion(hooks)` | resets what outlives an owner: beacon registry, far-match barrier, motion speed | -| `animationsSettled(opts?)` | resolves when every motion element, layout animation and `` run has stopped — and names what was still moving when it times out | -| `bounds(el)` / `shape(el)` | rects relative to the test root; the cumulative 2×2 transform | -| `orphanCount()` | leavers currently parked in an orphan layer | -| `strandedTransforms()` | elements wearing a transform nobody is animating | -| `live(sel)` / `liveAll(sel)` | querySelector that declines mid-flight ghosts in the orphan layer — a person never clicks one, a selector must not either | -| `advanceGate()` / `seekTo(s)` | drive a parked run; scrub to a second | -| `velocityOf(el, prop)` | how fast a value is moving right now | -| `isMotionIdle()` / `whatIsBusy()` | the probe underneath, for a custom wait | - -`animationsSettled()` is explicit rather than a blocking test-waiter on purpose: the interruption suite's whole method is to click again while something is still in flight, and a waiter would silently turn every one of those into a wait-for-completion. - -## External clocks: choreo-player - -`choreo-player` is a dependency-free, headless transport for clocking public Choreo runs from outside — a frame-by-frame video renderer, parallel capture workers, a custom scrub surface. It owns runs you hand it (never before an external clock has actually arrived — [docs/demo-recording.md](docs/demo-recording.md) is the field guide for keeping a demo correct both interactively and under capture), and its run contract is structural and five members small: - -```ts -import { createChoreoPlayer } from '@cardstack/choreo-player'; - -const player = createChoreoPlayer({ - duration: 12, - prepare: () => mountScore(), // the first external clock arms the demo - runs: () => (scoreRun ? [scoreRun] : []), // read per operation, never leaked early -}); - -await player.renderAt(4.25); // pause, set speed and absolute time, settle — frame t is a function of t -await player.play(); -``` - -Because a Choreo still is a computed frame, `renderAt(t)` is deterministic across workers on separate pages — no wall clock, no accumulation, no frame counting. - -## DOM in 3D: c.Camera3D - -A `` region can be a plane in a three.js scene — live DOM, still clickable, still animating, with WebGL glass composited over it. Two stages ship on this: **Mockup** (a phone) and **Long Take** (a laptop whose screen is running a second camera of its own). - -```gts - - - - - - -``` - -`c.Camera3D` carries an **orbit pose in the terms a shoot uses** — `dolly` (distance as a multiple of the host's own framing), `pitch` and `yaw` in degrees, `x` / `y` as truck and pedestal in fractions of the framed height, because dolly alone pushes a tall subject's head out of the picture. `@by` adds to the pose in force instead of replacing it: Pan's rule, in 3D. - -**Choreo does not own a renderer and does not pretend to.** The step is a pure function of the run's clock and hands the host a five-number pose through `@onCamera3D`; the host applies them to three.js, a CSS 3D stage, or anything else that can take a pose. So a 3D shot seeks like every other score — `run.time`, gates, `choreo-player` — with no rAF and no accumulation of its own. - -The mapping from three.js to CSS is **85 lines** (`test-app/app/lib/css3d.ts`): three pure functions over a 4×4 matrix, against 454 for three's own `CSS3DRenderer`, because Glimmer already owns the elements and the change tracking and Choreo already owns the clock. The screen is a hole punched in the canvas with `NoBlending` — and because that hole is depth-tested, the laptop's own keyboard occludes the live DOM behind it for free. - -[docs/dom-in-3d.md](docs/dom-in-3d.md) is the write-up: what the two shipped cases cost, what floating cards, anchored pop-ups and effects over DOM would add, and the two things that stay hard regardless of line count. - -## The film: `` - -Two films were cut by hand on the same engine before the construct was written — **Towers** (`/towers`) and **Sagrada Família** (`/sagrada`) — and 69% of the second was the first. `` is that 69%, lifted: a headless cutting room in which a 3D page takes the place of the video track, and everything else is a component reading one clock — the chased lens, the joins (wipe, dip, blend, iris, melt, blur, luma, flash, defocus), the lower third and the plate on Choreo, the timeline, the voice with its measured reads, the transport, the front door and the end card. - -```gts -import { Film, IframePicture } from '@cardstack/choreo/film'; - - - <:picture as |register|> - - - <:default as |f|> - - - - - - - - … - - - - <:gate as |f|>…the front matter, f.begin… - <:end as |f|>…the back matter, f.restart… - -``` - -A film is a GRAPH: a spine of chapters and shots with everything else -attached to a shot, compiled to the shot list the engine runs. Its script -carries the reads measured by ffprobe, the geometry sampled off its -model, and an identity block. The one-task behaviours — walk up the street and only then put your head back, follow the part being built, no line on a thing still going up — are beat fields with their handles exposed. The picture is a port (`Picture`), so a third film can hand in something other than an iframe. - -[docs/film.md](docs/film.md) is the **as-built reference**: every argument and block, the beat table, the clock and its one-tick cue offset, the two seek modes, the `Picture` port — and the section that matters most for the rest of the library, how `` composes with the other constructs rather than replacing them. It is an ordinary consumer of ``: one `c.Camera3D @through` for the entire picture, one `c.Perform` per beat anchored with `at('film')`, the lower third on its own region, and — in exact mode — two Choreo runs the film drives by writing `run.time` instead of playing them. - -[docs/film-graph/](docs/film-graph/) is the design record for the graph — the memo, the constructs, the revisions log and the plan — and [notes/film-construct.md](notes/film-construct.md) is the record behind the construct itself: the two reference films, the duplication measured, the fork between a seekable film and a chased one, and what was built. - ---- - -## The binding underneath - -Everything below this line is **`glimmer-motion`** proper: Motion's own implementation driven through Glimmer, covered by ports of the upstream tests that pin it. - -**Animation** — `initial` / `animate` / `exit` targets, keyframes, per-value transitions, `transitionEnd`; springs, tweens, inertia, `delay`, `repeat`, easings — the engine's full transition surface; **variants** with labels, functions, `custom`, propagation, `staggerChildren` / `delayChildren` / `when`; motion values (`motionValue`, `transformValue`, `animateMotionValue`) bound through `style`; static values and `MotionValue`s side by side, CSS variables, transform shorthands; SVG attribute animation and `pathLength`; `onAnimationStart` / `Complete` / `onUpdate`; unmount cleanup. - -**Layout** — `layout` (`true` / `"position"` / `"size"`): FLIP with the projection tree's scale correction (border radius, box shadow, children don't distort); `layoutId` shared-element transitions, crossfade, lead/follow promotion, the lightbox pattern; `` with `@id` and `@inherit`; `layoutDependency`, `layoutScroll`, `layoutRoot`, `layoutAnchor`, `layoutCrossfade`, `onLayoutMeasure`; nested and relative projection targets; portals, `instantLayoutTransition()`, `layoutChange()` / `snapshotAll()` / `requestSettle()`. - -**Presence** — `` = AnimatePresence: `sync` / `wait` / `popLayout`, `@initial={{false}}`, `@custom`, `@onExitComplete`, nested presence with `@propagate`; `h.isPresent` (tracked); exit-then-enter of the same key; a leaving item keeps its last props; interaction with `layout` / `layoutId`. - -**Drag** (Motion's own pan/drag session, inlined from framer-motion's build) — `drag` / axis locks / `dragPropagation` / `dragListener`; constraints as object, element or ref, `dragElastic`, `dragMomentum`, `dragTransition`, `dragSnapToOrigin`, re-measure on resize; `createDragControls()` with `snapToCursor`; `whileDrag` and the full handler set; pan handlers; drag inside `layout` / `layoutId`, nested draggables, drag under scroll; `transformPagePoint` with `correctParentTransform()` and `transformViewBoxPoint()`. - -**Gestures** — `whileHover` / `whileTap` / `whileFocus` / `whileInView` with their handlers, keyboard tap activation, `globalTapTarget`, viewport options, gesture priority; `` — and `@reducedMotion` defaults to **`"user"`** here, not React's `"never"`: honouring a platform setting is not a feature. `scrollProgress()` and `InView` over Motion's `scroll()` / `inView()`. - -**Reorder** — `` / `` on real `ul` / `li`; axis detection; auto-scroll near edges; works inside ``. - -**Glimmer** — `.gts` with Glint signatures throughout; plain-function template helpers (`to`, `spring`, `tween`, `inertia`, `stagger`, `ease`) so a template is not written in `(hash)`; a v2 addon, ESM, tree-shakeable; host hooks isolated in ~40 lines so the engine glue can be re-hosted. - -## Why the engine is untouched - -Motion's React library is two things. An animation engine — `motion-dom`: motion values, springs and keyframes, the _projection tree_ that does layout animation and scale correction, the pan/drag session — and a thin layer of React that feeds that engine props at the right moments. The engine is framework-free and published on its own. Only the glue is React. - -So this package does not re-implement animation, and it does not fork Motion. It binds the engine to Glimmer's rendering lifecycle and reproduces, exactly, the _ordering_ React's glue gives the engine: - -- visual elements are constructed **parents-first during render** and mounted **children-first in effects**; -- every element that re-renders **snapshots its layout before the DOM changes**, the root **measures after**; -- presence is announced **after the commit**, and a leaving element is rendered **from its last element** — its props frozen; -- the engine's own update runs in a microtask **after all of that**, never between a render and its mounts. - -Getting those right is the whole job. When they are right, the engine's behaviour matches Motion to the pixel — which is not an aspiration but the thing we measure. - -## How it was made - -Choreo started as the motion layer of a port: a React app built on Motion (the "bentobox" reference, frozen as ground truth) moving to a modern Vite/Embroider Ember app. The first question was how much work Motion needed to run under Glimmer. The answer turned out to be "none, if you don't touch it." - -**Strategy.** Cardstack had done this once before with [boxel-motion](https://github.com/cardstack/boxel), a Glimmer animation library with its own engine. Comparing the two made the call obvious: Motion's value is in the engine (the projection tree especially), and that engine is already framework-free. The binding would use `motion-dom` as-is and replicate only the React glue — and it would be held to the standard of Motion's own tests rather than our eyes. - -**Method: port the test suite, in order, and let it drive the binding.** Each step ported one of Motion's Jest suites or Cypress fixture specs line by line — the fixture components rebuilt as Glimmer components, the literal expected pixel values kept — and whatever failed was a place where the binding's ordering differed from React's. In sequence: the animate prop, variants, AnimatePresence, LayoutGroup, keyframes and delay, the style prop and unmount, then the eighteen `layout-*` Cypress specs, then the nineteen `drag-*` specs, then Reorder. Nothing was declared done on a behaviour until the upstream test for it was green in a real Chrome. - -**What that surfaced.** Almost everything a port like this gets wrong is a timing rule, and the tests found each one: - -- Glimmer installs modifiers children-first, so a child can't find its parent's visual element when its modifier first runs. First runs queue; the queue constructs in document order and mounts post-order after the pass — and sibling order matters, because the engine's stagger index is built from it. -- There is no `getSnapshotBeforeUpdate`. boxel-motion's render detector — a getter consuming `@glimmer/validator`'s `VOLATILE_TAG` re-evaluates on every render pass — lets `` snapshot every projection node before the DOM changes, the way React re-rendering each motion component would. -- The engine's update microtask must not run between Glimmer's render and the mounts, or it clears snapshots and layout animations become identity transitions. One settle per pass, after the mounts. -- An element created already-absent must still mount _present_ and then leave, or its exit is swallowed. -- AnimatePresence renders a leaving child from its last present element; a live Glimmer block doesn't. The modifier freezes an exiting element's props — React's behaviour, arrived at from a failing Cypress fixture. -- `initial` values must be in the DOM before the projection first measures; React has them in the style attribute synchronously, so the binding renders them synchronously at mount. -- A Cypress page is fresh per test; a test runner isn't. The document projection node caches scroll per `animationId` and leaked a stale offset across tests until the harness reset it the way a page visit does. - -**And then the choreography earned its own list.** The same discipline — ship nothing a test didn't force — produced the region model's rules: the fast keep that declines noise passes without touching the world; page-space deltas so a region can move in the pass that moves its children; the release-measure-reassert cycle that makes every measurement a _resting_ measurement; screen-CTM mapping for every place a client rect becomes local pixels (orphan locks, raises, scroll targets, tether ink — each was found double-scaling under an external transform, each is now pinned by a fixture inside a scaled wrapper); and the derived-value API redesign that [postmortem-follow.md](docs/postmortem-follow.md) documents in full. - -**Packaging.** With the suite green the binding was split along the line the imports already drew: `MotionNode` (one element's lifecycle, no host framework), the layout pipeline, the features and a `postRender` scheduler on one side; the `ember-modifier` shell, the scheduler's runloop adapter and the Glimmer components on the other. That second side is about forty lines plus the components — the surface another Glimmer host (a SES-sandboxed renderer, say) re-implements to reuse everything else. - -## Fidelity - -The suite has two halves, and they answer different questions. - -**Is it Motion?** `test-app` carries the upstream ports: the Jest suites (animate prop, variants, AnimatePresence, LayoutGroup, keyframes/delay, style prop, unmount) and the Cypress fixtures (all `layout-*`, all `drag-*`, `drag-to-reorder`, `drag-tabs`) — the fixture components rebuilt as Glimmer components with the literal expected pixel values kept. Where a literal was changed the reason is inline; each is a Cypress-runner artefact. Environment deltas are documented the same way. - -**Does the choreography model hold?** `` is not Motion's, so it has no upstream test to inherit. It has a contract suite that states its rules on small fixtures — nesting, beacon lifetime, undo, far matching, orphan ownership, anchors, composite steps, derived-value purity (a test literally vandalises the page mid-window and asserts the frame at _t_ does not change), gate retreat, continuity from both sides, and the external-scale rules — plus per-frame interruption tests and a soak that hammers the real gallery. - -**589 cases — 587 pass, 2 are upstream's own `it.skip`.** - -``` -pnpm install && pnpm test # builds the addon, runs the suite (a development-mode build, as boxel does) in Chrome -``` - -## API - -Named exports from `glimmer-motion`; deep imports (`glimmer-motion/presence`, …) are the same modules. - -### `{{motion}}` — `motion.div`, `motion.circle`, … as a modifier - -Any element. Named arguments are Motion's props — same names, same types (`MotionNodeOptions` from `motion-dom`): - -| group | props | -| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| animation | `initial` `animate` `exit` `variants` `transition` `custom` `inherit` `onAnimationStart` `onAnimationComplete` `onUpdate` | -| layout | `layout` `layoutId` `layoutDependency` `layoutScroll` `layoutRoot` `layoutCrossfade` `layoutAnchor` `onLayoutMeasure` | -| drag | `drag` `dragConstraints` `dragElastic` `dragMomentum` `dragSnapToOrigin` `dragDirectionLock` `dragPropagation` `dragTransition` `dragControls` `dragListener` `onDragStart` `onDrag` `onDragEnd` `onDirectionLock` `onMeasureDragConstraints` `whileDrag` | -| pan | `onPanStart` `onPan` `onPanEnd` `onPanSessionStart` | -| style | `style` — static values land on the element as React's style attribute would; `MotionValue`s are bound | -| choreo | `id` `role` — participation in the nearest `` region | -| glimmer-specific | `presence` — the handle a `` block yields; `transformPagePoint` — per element, or inherited from `` | - -### `` — the region - -`@id` (nesting and far matching), `@route` (a route swap inside is one pass), `@scroll` (`'top'` or a thunk — applied inside the pass, before final bounds), `@quiet` (pause the rest of the page for the run's span), `@debug` (outlines + `console.table` per run). Yields the context: the step components (`Sequence` `Parallel` `Gate` `Tween` `Spring` `Move` `Hold` `Wait` `Raise` `Scroll` `Camera` `Tether` `Follow` `Crossing`), the queries, `c.beacon` / `c.gesture`, and `c.run` — `advance()` `retreat()` `time` `speed` `parked` `standing` `segment` `finished`. - -Timing: `@duration` / `@delay` in seconds, `@spring` for spring specs, `@stagger` across a query's sprites; `@name` / `@at={{at 'name'}}` / `after('name')` on steps _and blocks_. Any property may be a function `(sprite, changeset) => value`; sprites carry `initial` / `final` bounds in `context`, `parent` and `page` space, a `delta`, and their `counterpart`. Open vocabulary: `StepComponent` + `toMs` + node literals ([docs/step-vocabulary.md](docs/step-vocabulary.md)). - -`c.Tween` also accepts normalized `@times` for keyframe arrays, so a loop can -hold, scatter, and regroup on an explicit rhythm without remounting its elements. -The [Circle continuity example](test-app/app/components/examples/circle-loop.gts) -and [guide](test-app/app/content/guides/interactive-circle-loop.md) show the pattern. - -### `` / `` / `` + `` / `` - -As in Motion — see [React → Glimmer](#react--glimmer). `` keeps leaving items rendered until their exit finishes (`@mode`, `@initial`, `@custom`, `@onExitComplete`, `@propagate`); `` namespaces `layoutId`s and hosts the render detector; outside one, wrap the state change in `layoutChange(() => …)`. - -### Template helpers - -Plain functions, no registration: `(to opacity=0 y=20)`, `(spring stiffness=300 damping=30)`, `(tween …)`, `(inertia …)`, `(stagger 0.05)`, `(ease 0.4 0 0.1 1)`. Typed, which is the difference: `(hash opacty=1)` is a perfectly good hash, and `(to opacty=1)` is a Glint error. `(hash …)` remains valid everywhere. - -### Drag helpers and motion values - -`createDragControls()`, `correctParentTransform(elOrRef)`, `transformViewBoxPoint(svgOrRef)`. For motion values use `motion-dom` directly — `motionValue()`, `transformValue()`, `animateMotionValue()` — and hand them in through `style`. - -## React → Glimmer - -| React | here | -| ---------------------------------------------- | ---------------------------------------------------------------------------- | -| `` | `
` — any tag, including SVG | -| `ref` | the element itself, or a `{current}` object filled by a modifier | -| `useMotionValue`, `useTransform` | `motionValue`, `transformValue` from `motion-dom` | -| `` with keyed children | `` yielding a handle | -| `useIsPresent()` | `h.isPresent` | -| `` | `` (also the render detector) | -| re-render of a motion component → snapshot | any render pass inside a `LayoutGroup` / `ReorderGroup`, or `layoutChange()` | -| `` | `` — same args, found by DOM proximity | -| `useDragControls()` | `createDragControls()` | -| `useScroll()` / `useInView()` | `scrollProgress()` / `new InView(…)` — no hooks, so no `use` prefix | -| `Reorder.Group` / `Reorder.Item` | `` / `` | -| — | ``: region-scoped timelines over a render pass's changeset | - -## Three rules React does not need - -React's model hides three things that Glimmer's does not, and each surfaced while porting a real app onto this binding. They are the only places where the port is not a mechanical translation. - -**1. Motion owns a motion element's inline style — pass CSS through the modifier, not a `style` attribute.** In React the `style` prop belongs to Motion: it merges what you write with the transforms it renders. In Glimmer a bound `style="…"` attribute is yours, and Glimmer rewrites the whole declaration whenever the bound value changes — wiping out the transform Motion just wrote. The symptom is brutal and quiet: a card that stays put while the drag logic runs perfectly around it. - -```gts -{{! ✗ the next re-render erases the drag transform }} -
- -{{! ✓ Motion applies these itself, custom properties included }} -
-``` - -**2. A leaving child stays live — read exiting content from the yielded item.** React keeps the _element tree_ it captured before the diff, so a leaving child cannot re-render. A Glimmer block re-runs from live tracked state for as long as the leaver is on screen. So anything the exit needs — the symbol the panel was showing, the rect the modal flew from — has to ride on the item `` yields, not be read back out of state that has already moved on. - -**3. Declare what you want tweened — a value in the stylesheet is invisible to the engine.** A layout animation moves and resizes by transform, and the engine corrects `borderRadius` and `boxShadow` against the scale in force — but only values it holds. A radius that lives in CSS is not one, and a square tile growing into a wide hero comes out with oval corners. Hand the value to Motion and every frame is corrected. - -```gts -{{! ✗ the engine cannot correct what it does not know about }} -
/* .card { border-radius: 14px } */ - -{{! ✓ corrected per frame, against whatever scale the layout animation is applying }} -
-``` - -## Testing - -See [Interruption, tested](#interruption-tested) for the test-support surface. The suite is in halves: `tests/integration/choreo/contract-test.gts` states the choreography rules on fixtures small enough that a failure names the defect; sibling suites cover the crossing, gates and retreat, continuity, wires, raise/scroll, far matching, and derived values; `interruption-test.gts` is the soak. The binding's half is the upstream ports ([Fidelity](#fidelity)). - -## Architecture - -``` -motion-dom (engine, unchanged) - ▲ 30 public exports -node.ts MotionNode — one element's lifecycle, no host framework: construct in document order, - mount post-order after the pass, update, freeze props while exiting, tear down -layout.ts snapshot → mount → measure → settle (React's getSnapshotBeforeUpdate / componentDidUpdate timing) -features.ts animation / exit / layout / drag / pan registrations, and the scale correctors -scheduler.ts postRender(fn): the single host hook — "after this render pass has committed" -activity.ts who is still moving, and why — what animationsSettled() waits on -helpers.ts to / styles / from / spring / tween / inertia / perValue / stagger / ease -framer-motion-internals.ts - Motion's pan/drag session, gesture and animation features, and Reorder's - checkReorder / detectAxis / auto-scroll, inlined from framer-motion's build (VENDORED.md) -gestures/ drag-gesture.ts (glimmer-motion's two drag deviations), drag controls, transformPagePoint helpers -choreo/ the choreography layer: changeset, compile, run, anchors, beacons, arming, - far-match barrier, measure — the region model in ~a dozen modules -test-support/ animationsSettled, bounds, live, orphanCount, … — a published entrypoint - ▲ Ember host adapter -motion.ts {{motion}} — the ember-modifier shell; installs the runloop as postRender -presence.gts, layout-group.gts, motion-config.gts, choreo.gts, reorder/{group,item}.gts -``` - -Only the last two lines know about Ember. Re-hosting means re-doing the modifier shell, the `postRender` adapter and the components; everything above the adapter line is untouched. `choreo-player` sits beside all of it, dependency-free, speaking only the run contract. - -## Not ported (yet) - -`m` / `LazyMotion` (a React bundle-splitting device; the addon is tree-shaken per module already), Reorder's `as` prop (the group is a `ul`, items are `li`), server rendering. - -## Examples - -`test-app` serves a gallery of **46 stages** at `/` — filter by **Animate**, **Layout**, **Drag**, **Scroll**, **Choreo**, **3D**, **Timeline**, or **Deep Dive**, and open any one for its annotated source. Most stages carry a speed control (**Full · ÷2 · ÷5 · ÷10**); a transition you cannot see is a transition you cannot judge, and the divisor scales the transition on its way to the engine rather than slowing a running animation, so what you watch at ÷10 is the same motion, born slower. The gallery ⇄ demo navigation is itself the crossing, eating its own cooking on every click. - -Every demo is also a test fixture: the interruption soak hammers them, which is why they are the first place a regression shows up. - -``` -pnpm --filter test-app start -``` - -### Guides and tuning - -Three complete tutorials provide runnable starting points: -[first application](test-app/app/content/guides/core-first-app.md), -[first spatial scene](test-app/app/content/guides/spatial-first-scene.md), and -[first MP4](test-app/app/content/guides/film-first-export.md). The -[troubleshooting guide](test-app/app/content/guides/core-troubleshooting.md) -connects common failures to concrete checks. - -The documentation website at `/docs` teaches four paths: **core glimmer-motion**, -**interactive Choreo**, **spatial & 3D Choreo**, and **recorded & film Choreo**. -Each section starts with motivation and goals; concept pages pair explanations -with code, live examples, experiments, and pitfalls. - -Every catalog demo links to its detailed guide and a DialKit workspace at -`/playground/:demo_id`. Controls edit named variables consumed by that demo, -with units, authored presets, and restoration of source defaults. Timing edits -update running motion without remounting the example. - -The [API inventory](docs/api-inventory.json) and -[teaching map](test-app/app/content/demo-lessons.json) cover 343 API/vocabulary -entries and all 46 demos across 88 guides. `pnpm docs:check` checks those mappings, -guide depth, section goals, and internal links. Focused docs-only spatial and film -studies remain [planned](docs/docs-studies-plan.md), separate from shipped examples. -See [documentation authoring](docs/documentation-site.md) for the content workflow. - -### 3D gallery - -`/_widgets` arranges the catalog in a dark exhibition space. Mockup-style -thumbsticks provide pan, zoom, and rotation around the viewer's fixed position; -arrow keys work when a control is focused. Inactive tiles use prerendered previews, -with live interaction activated on approach to limit rendering work. - -The full audio tour connects the demos through narrated explanations and guided -clicks. Its resume point survives pauses, exploration, and refreshes. A separate -highlight tour covers selected capabilities in about 59 seconds, with narration, -camera, and clicks following one media clock. Manual navigation pauses the guide. -The existing recorded highlight remains a separate export. - -Additional [screenshots and motion previews](docs/media/README.md) are linked -separately to keep this reference focused on the library. - -## Development - -``` -pnpm install -pnpm build # packages/glimmer-motion → dist/ + declarations/ -pnpm test # build, then test-app: vite build --mode=development --out-dir dist-tests && ember test --path dist-tests -pnpm lint:types # glint, workspace packages -pnpm docs:check # API and demo teaching coverage -pnpm build:boxel # gallery adaptation and realm artifacts -pnpm --filter test-app start # examples at / ; tests at /tests -``` - -Cards in a [Boxel](https://github.com/cardstack/boxel) realm import -`glimmer-motion` and `@cardstack/choreo` by their package names. The Boxel host -shims both packages, so a realm needs no bundled copy of either. - -The complete gallery, guides, tuning workspaces, and 3D room can also be deployed -through the [Boxel gallery build](packages/choreo-gallery/README.md). Use your own -realm and host configuration; account-specific addresses do not belong in the -source documentation. GitHub Pages continues to build the canonical `test-app`. - -Agents can use the shared [choreo-create skill](.claude/skills/choreo-create/SKILL.md) -from Claude Code or [Codex](.agents/skills/choreo-create/SKILL.md). It routes to -real demo sources, motion patterns, composition recipes, tuning, and verification. - -`packages/glimmer-motion/VENDORED.md` lists the Motion modules glimmer-motion inlines from framer-motion's build, the two deliberate deviations, and the one adapted file; when bumping Motion, re-check the declarations in `src/framer-motion-internals.ts` against upstream. The Cypress-port harness lives in `test-app/tests/helpers/layout-fixture.ts`. - -## Roadmap - -- **`0.1.0` on npm.** Consumers today use `workspace:*` against a checkout. A version number is what turns "copy this style" into a dependency, and it is the gate for everything below. -- **`ember-try` against LTS** (5.12 / 6.4 / release). The peer range already says `>= 5.4`; nothing proves it. -- **`ember-a11y-testing` over the gallery.** `reducedMotion` already defaults to `"user"`, which was the substantive half; a smoke pass is the other half. -- **Focused spatial and film studies** — smaller, tunable teaching examples alongside the existing composite demos; see the [study plan](docs/docs-studies-plan.md). -- **boxel-motion** — a re-host inside [cardstack/boxel](https://github.com/cardstack/boxel) for its constraints (SES-sandboxed card code, cross-realm orchestration, fitted vs embedded intrinsic sizing). It replaces the modifier shell, the scheduler adapter and the components and keeps everything above the adapter line. Three host transitions are the proving ground: a stack open/close (far match), a panel that is its own scene over a moving shell (nested ``), and compose/trash (`{{beacon}}`, not `layoutId`). - -## License - -MIT. © 2026 Cardstack Foundation. See [LICENSE](LICENSE). - -## Credits - -The engine, the algorithms and the test suites are [Motion](https://motion.dev) ([motiondivision/motion](https://github.com/motiondivision/motion), MIT). The render-detector idea and the choreography model's ambitions are from [boxel-motion](https://github.com/cardstack/boxel). This package is the Glimmer binding and the choreography layer, by Cardstack. diff --git a/packages/choreo-gallery/eslint.config.mjs b/packages/choreo-gallery/eslint.config.mjs index b0502a68fab..5cb26e454a5 100644 --- a/packages/choreo-gallery/eslint.config.mjs +++ b/packages/choreo-gallery/eslint.config.mjs @@ -8,10 +8,6 @@ import globals from 'globals'; export default defineConfig([ globalIgnores([ - // the film-graph sketches are proposals written in a syntax that does - // not exist yet (f.Spine, f.Attach, f.picture.*): read them, don't lint them - 'docs/film-graph/**/*.gts', - 'docs/film-graph/**/*.mjs', 'out/**', // the film app's build (choreo-film-app) and the films' vendored scripts 'realm/film-app/**', diff --git a/packages/choreo-gallery/package.json b/packages/choreo-gallery/package.json index 759e5569cc8..7eb68956724 100644 --- a/packages/choreo-gallery/package.json +++ b/packages/choreo-gallery/package.json @@ -43,6 +43,8 @@ "eslint": "^9.39.4", "glimmer-motion": "workspace:*", "globals": "^15.15.0", + "marked": "catalog:", + "marked-gfm-heading-id": "catalog:", "playwright": "1.61.0", "prettier": "catalog:", "prettier-plugin-ember-template-tag": "catalog:", diff --git a/packages/choreo-gallery/realm/asset/towers-model.html b/packages/choreo-gallery/realm/asset/towers-model.html index 3ec8cdce11c..415c3955cbf 100644 --- a/packages/choreo-gallery/realm/asset/towers-model.html +++ b/packages/choreo-gallery/realm/asset/towers-model.html @@ -18,7 +18,7 @@ master/voice bus mixer with narration on the graph, winter (settled snow and a pinned blizzard on the shot's clock), lightning on cue, the page's own UI hidden from the first paint under html[data-hosted], and the - lightning flash plane hidden when unlit. docs/towers-porting-delta.md + lightning flash plane hidden when unlit. design/towers-porting-delta.md keeps the list of what upstream can do that the film does not use yet. --> diff --git a/packages/choreo-gallery/notes/README.md b/packages/choreo-gallery/realm/design/README.md similarity index 62% rename from packages/choreo-gallery/notes/README.md rename to packages/choreo-gallery/realm/design/README.md index d4e02850d8f..7734e64f364 100644 --- a/packages/choreo-gallery/notes/README.md +++ b/packages/choreo-gallery/realm/design/README.md @@ -1,16 +1,22 @@ -# Working notes +# Design notes This is the workshop, not the documentation. Everything here is a draft, a design record, a handoff, a measurement or a production script — kept because it says _why_ something is the way it is, and cited from the source where the reasoning matters. -**The documentation is in [`docs/`](../docs).** If you are trying to use -Choreo, start at [docs/guide.md](../docs/guide.md) and -[docs/choreo-constructs.md](../docs/choreo-constructs.md). Nothing in +**The documentation is in [`docs/`](../docs/README.md).** If you are trying to use +Choreo, start at [the guide](../docs/guide.md) and +[the construct reference](../docs/choreo-constructs.md). Nothing in this folder is a reference, and some of it describes things that were proposed and never built, or built and then changed. +The notes were written in Choreo's own repository, before it moved into the +Boxel monorepo, so the source paths they name (`test-app/…`, +`packages/glimmer-motion/src/choreo/…`) are that repository's layout at the +time. The demos are now cards in this realm, Choreo and Film are +`packages/choreo`, and the films' app is `packages/choreo-film-app`. + ## What is here **Design records** — the argument behind a construct, written before or @@ -19,7 +25,7 @@ during the work. - [film-construct.md](film-construct.md) — toward ``: the two reference films, the duplication measured, the fork between a seekable film and a chased one. The as-built reference is - [docs/film.md](../docs/film.md). + [film.md](../docs/film.md). - [choreo-composition.md](choreo-composition.md) — the compositor. - [drift.md](drift.md) — a driving model written by hand, and why a score was the wrong shape for it. @@ -32,6 +38,13 @@ during the work. proposals. - [camera-api-wishlist.md](camera-api-wishlist.md) — what `` was missing, written from the outside. +- [film-graph/](film-graph/README.md) — the films as a graph: the memo's + sketches, the constructs, the revisions log and the plan. +- [docs-studies-plan.md](docs-studies-plan.md) — focused, tunable teaching + studies, planned rather than shipped. +- [documentation-site.md](documentation-site.md) — the guides site: its four + learning paths, how a guide is written and registered, live tuning, and + teaching coverage. **Handoffs and measurements** — written for whoever picks the work up. @@ -39,6 +52,12 @@ during the work. - [subdivision-browser-timing-test.md](subdivision-browser-timing-test.md) - [towers-porting-delta.md](towers-porting-delta.md), [towers-quality.md](towers-quality.md) +- [sagrada-web-model.md](sagrada-web-model.md) — the Sagrada picture's + procedural model pass +- [choreo-player-reference-proof.md](choreo-player-reference-proof.md) — the + Playhead render with one and ten workers, and the 1.0 release gate +- [gallery/](gallery/README.md) — the 3D gallery room and its narrated + tours: room design, tile capacity, tour script and pacing, performance **Film production** — the scripts, the wall text, the narration. diff --git a/packages/choreo-gallery/notes/camera-api-wishlist.md b/packages/choreo-gallery/realm/design/camera-api-wishlist.md similarity index 100% rename from packages/choreo-gallery/notes/camera-api-wishlist.md rename to packages/choreo-gallery/realm/design/camera-api-wishlist.md diff --git a/packages/choreo-gallery/notes/choreo-composition.md b/packages/choreo-gallery/realm/design/choreo-composition.md similarity index 99% rename from packages/choreo-gallery/notes/choreo-composition.md rename to packages/choreo-gallery/realm/design/choreo-composition.md index 01d470a1626..ab47b2e4211 100644 --- a/packages/choreo-gallery/notes/choreo-composition.md +++ b/packages/choreo-gallery/realm/design/choreo-composition.md @@ -615,7 +615,7 @@ piece is at gallery quality. the existing seekable step — after Clip, not before. The plane-and-camera pairing has its own design: -[planes-and-cameras.md](planes-and-cameras.md) — independent pan and zoom +[planes-and-cameras.md](../docs/planes-and-cameras.md) — independent pan and zoom per plane (shipped in the reel's clock plane), the coordinate contract, and sprites moving between planes without relayout. diff --git a/packages/choreo-gallery/realm/design/choreo-player-reference-proof.md b/packages/choreo-gallery/realm/design/choreo-player-reference-proof.md new file mode 100644 index 00000000000..31d4074215b --- /dev/null +++ b/packages/choreo-gallery/realm/design/choreo-player-reference-proof.md @@ -0,0 +1,35 @@ +# The choreo-player reference proof + +> From `docs/choreo-player.md` in Choreo's own repository, where the fixture +> lives. It records a measurement made once, and the gate that was planned +> to repeat it for a 1.0 release. Nothing in the Boxel monorepo runs either. + +## Reference proof + +`videos/playhead-recording` is the installation and render fixture. It: + +- builds the real Glimmer gallery application; +- copies the published-shape `choreo-player/hyperframes` module into the + composition; +- mounts the real Playhead Glimmer component with no added border; +- binds native `hf-seek` to its awaited frame transaction; +- pins HyperFrames 0.8.14 locally; and +- renders 1920×1080, 30fps, with ten workers. + +The proof was compared with a same-build one-worker render. FFmpeg measured +54.15 dB average PSNR and 0.999583 SSIM, and found no post-opening frame that +matched the opening frame at any worker boundary. + +## 1.0 release gate + +Before publishing: + +1. Build and run the package contract tests. +2. Run `pnpm pack` and inspect the tarball contents. +3. Install the tarball in the reference fixture. +4. Run the complete interactive Ember suite. +5. Run HyperFrames check and render with one and ten workers. +6. Scan every worker boundary for an opening-frame insertion. + +The npm artifact should contain only `dist`, `README.md`, `LICENSE`, and +`package.json`. diff --git a/packages/choreo-gallery/notes/demos-as-applications.md b/packages/choreo-gallery/realm/design/demos-as-applications.md similarity index 100% rename from packages/choreo-gallery/notes/demos-as-applications.md rename to packages/choreo-gallery/realm/design/demos-as-applications.md diff --git a/packages/choreo-gallery/notes/dialkit.md b/packages/choreo-gallery/realm/design/dialkit.md similarity index 99% rename from packages/choreo-gallery/notes/dialkit.md rename to packages/choreo-gallery/realm/design/dialkit.md index bd0c6af5452..58c7c83fb11 100644 --- a/packages/choreo-gallery/notes/dialkit.md +++ b/packages/choreo-gallery/realm/design/dialkit.md @@ -240,7 +240,7 @@ localStorage with no way home. They sit in one row because to a player they are one question — what am I driving — but only one of them can be deleted. **`type: 'spring'` is NOT cheap, and the plan that said it was got the name -wrong.** `notes/drift.md` proposed one spring control per pair instead of two +wrong.** `drift.md` proposed one spring control per pair instead of two sliders, "cheap once folders exist". The store does not emit `'spring'` for a `SpringConfig` at all: it emits `type: 'transition'` (store `index.js:552`), whose value is the whole spring object plus a companion `path.__mode` that diff --git a/packages/choreo-gallery/docs/docs-studies-plan.md b/packages/choreo-gallery/realm/design/docs-studies-plan.md similarity index 100% rename from packages/choreo-gallery/docs/docs-studies-plan.md rename to packages/choreo-gallery/realm/design/docs-studies-plan.md diff --git a/packages/choreo-gallery/docs/documentation-site.md b/packages/choreo-gallery/realm/design/documentation-site.md similarity index 83% rename from packages/choreo-gallery/docs/documentation-site.md rename to packages/choreo-gallery/realm/design/documentation-site.md index e73ac38b22e..1dae165a550 100644 --- a/packages/choreo-gallery/docs/documentation-site.md +++ b/packages/choreo-gallery/realm/design/documentation-site.md @@ -1,5 +1,10 @@ # Choreo Documentation Website +> The design of the test app's documentation site, which the gallery realm's +> guides take over. Paths into the test app name it as it was. The tools and +> checks are named where they live now, in `packages/choreo-gallery`; the realm +> check (`pnpm lint:realm`) replaced `docs:check`. + The guides are part of `test-app`, sharing its brand mark, theme, navigation, and deployment. Open `/docs`; `/docs/:topic_id` is a guide, `/playground/:demo_id` is a live tuning workspace, and `/_widgets` opens the 3D gallery. ## Information Architecture @@ -19,7 +24,7 @@ The writing reference is [ember-learn/guides-source](https://github.com/ember-le Use a friendly, direct explanation of an observable behavior. Start with the common case, show a small example, then explain the relevant lines. Introduce technical terms when the reader needs them. Open each page with a preamble that explains its purpose before the live example. Each main section starts with Motivation and Learning Goals. Keep a guide focused enough to read in a few screens, and split advanced topics into linked pages. Code blocks show a filename or explicitly identify an excerpt. Use this repository's named exports and `.gts` conventions instead of copying Ember's `.gjs` naming literally. -Source Markdown lives in `test-app/app/content/guides/`. Register introductory pages in `test-app/app/lib/guides.ts` and deeper concept pages in `test-app/app/lib/guide-reference.ts`. The reviewed metadata is recorded in `docs/guide-topics.json`. The renderer escapes raw HTML, highlights code with the gallery's existing highlighter, and rewrites internal URLs through Ember's router so hash and subdirectory deployments work. +Source Markdown lives in `test-app/app/content/guides/`. Register introductory pages in `test-app/app/lib/guides.ts` and deeper concept pages in `test-app/app/lib/guide-reference.ts`. The reviewed metadata is recorded in `test-app/app/content/guide-topics.json`. The renderer escapes raw HTML, highlights code with the gallery's existing highlighter, and rewrites internal URLs through Ember's router so hash and subdirectory deployments work. Use `/docs/` for guide links, the public catalog ID for demos, and `/_widgets` for the 3D room. Do not assume an example filename is its public route ID. Label proposal-only material clearly; the guides describe implemented behavior. @@ -52,7 +57,7 @@ Publish the complete build and required static media using the chosen host. Do n ## API Coverage and Depth -`docs/api-inventory.json` maps 343 exported names and yielded vocabulary members to their concept guides. Each registered guide has at least 300 explanatory prose words, excluding code and API tables. Related aliases and supporting types share their behavior chapter. `scripts/check-guide-coverage.mjs` compares the inventory with the TypeScript declarations and checks depth, preambles, section goals, and internal guide links. Run `pnpm docs:check` after an API or documentation change. +`packages/choreo-gallery/tools/api-inventory.json` maps the exported names and yielded vocabulary members to their concept guides. Each registered guide has at least 300 explanatory prose words, excluding code and API tables. Related aliases and supporting types share their behavior chapter. `packages/choreo-gallery/tools/check-realm.mjs` compares the inventory with the packages' exports and checks depth, preambles, section goals, and internal guide links. Run `pnpm lint:realm` in `packages/choreo-gallery` after an API or documentation change. The inventory appears at `/docs/core-api-inventory`. Keep it synchronized when adding a public API. Do not automatically map a new export to a broad introduction simply to satisfy the check: review whether its concept needs a dedicated treatment. @@ -73,27 +78,27 @@ Use the same binding name wherever the same source variable is shared. Use disti Timing-only edits use the workbench’s `live-demo-motion.ts` adapter after Glimmer’s `postRender` hook. It retimes the existing Motion visual elements and preserves repeating animations’ phase; it does not remount the demo. -Run `node scripts/check-demo-live-controls.mjs` against the dev server to +Run `node packages/choreo-gallery/tools/check-demo-live-controls.mjs` against the dev server to verify live duration and target edits, DOM identity, and compact control rows. Each catalog demo includes two authored looks in DialKit’s Versions menu. `demo-presets.ts` maps each look to explicit demo variable names and values. Version 1 restores the source defaults; choosing a look keeps the demo mounted. Add presets only after their variables are registered, and keep all numeric -values inside their declared ranges. Run `node scripts/check-demo-presets.mjs` +values inside their declared ranges. Run `node packages/choreo-gallery/tools/check-demo-presets.mjs` against the development server to exercise both looks and restoration. Teaching coverage is tracked in `test-app/app/content/demo-lessons.json`. Each catalog example has a concept explanation, an experiment, a pitfall, a composition idea and a concept-guide link. The shared GuideDemo renders these beside the example code. Seven focused walkthroughs are registered in `demo-guides.ts`. -`docs:check` verifies that all catalog examples remain embedded and have complete +`pnpm lint:realm` verifies that all catalog examples remain embedded and have complete teaching records; prose correctness still requires source review. -`docs/docs-studies-plan.md` specifies the planned docs-only spatial and film +[docs-studies-plan.md](docs-studies-plan.md) specifies the planned docs-only spatial and film studies. Keep these separate from shipped demo coverage until implemented. The 3D room and its tile frames force a dark palette without overwriting the -visitor’s saved theme. `scripts/check-guide-lessons.mjs` checks real embedded +visitor’s saved theme. `packages/choreo-gallery/tools/check-guide-lessons.mjs` checks real embedded controls and this gallery theme behaviour against the development server. ## Complete tutorials diff --git a/packages/choreo-gallery/notes/drift.md b/packages/choreo-gallery/realm/design/drift.md similarity index 99% rename from packages/choreo-gallery/notes/drift.md rename to packages/choreo-gallery/realm/design/drift.md index 8b1292374a2..d4e0534ce41 100644 --- a/packages/choreo-gallery/notes/drift.md +++ b/packages/choreo-gallery/realm/design/drift.md @@ -170,7 +170,7 @@ emits `type: 'transition'` for a `SpringConfig` (store `index.js:552`), whose value is the whole spring object plus a companion `path.__mode` switching between an easing curve, a two-number "simple" form and a five-number "advanced" one. Folders of ordinary sliders get the same nesting for none of -that. See `notes/dialkit.md` for the full account of the second pass. +that. See `dialkit.md` for the full account of the second pass. **The skid marks are a canvas.** The note wanted them as `c.inserted` / `c.removed` over a list nobody clicked to build. A mark is laid every few world diff --git a/packages/choreo-gallery/notes/external-clock-camera-seek-handoff.md b/packages/choreo-gallery/realm/design/external-clock-camera-seek-handoff.md similarity index 100% rename from packages/choreo-gallery/notes/external-clock-camera-seek-handoff.md rename to packages/choreo-gallery/realm/design/external-clock-camera-seek-handoff.md diff --git a/packages/choreo-gallery/notes/film-construct.md b/packages/choreo-gallery/realm/design/film-construct.md similarity index 99% rename from packages/choreo-gallery/notes/film-construct.md rename to packages/choreo-gallery/realm/design/film-construct.md index 8e9eb1d8118..c01d8ec47c4 100644 --- a/packages/choreo-gallery/notes/film-construct.md +++ b/packages/choreo-gallery/realm/design/film-construct.md @@ -660,7 +660,7 @@ derives everything else from it: `Beat.clip` exists (2026-09-02, after seek). A beat is a shot of the picture; a clip is something laid over it for a while — a video, a still, or a freeze of the picture itself — and it is the compositor's `ClipSpec` -arithmetic (`notes/choreo-composition.md`, Phase C3) lifted whole: +arithmetic (`choreo-composition.md`, Phase C3) lifted whole: ``` source = in + (film − start) × rate diff --git a/packages/choreo-gallery/docs/film-graph/CONSTRUCTS.md b/packages/choreo-gallery/realm/design/film-graph/CONSTRUCTS.md similarity index 100% rename from packages/choreo-gallery/docs/film-graph/CONSTRUCTS.md rename to packages/choreo-gallery/realm/design/film-graph/CONSTRUCTS.md diff --git a/packages/choreo-gallery/docs/film-graph/GAPS.md b/packages/choreo-gallery/realm/design/film-graph/GAPS.md similarity index 100% rename from packages/choreo-gallery/docs/film-graph/GAPS.md rename to packages/choreo-gallery/realm/design/film-graph/GAPS.md diff --git a/packages/choreo-gallery/docs/film-graph/PLAN.md b/packages/choreo-gallery/realm/design/film-graph/PLAN.md similarity index 98% rename from packages/choreo-gallery/docs/film-graph/PLAN.md rename to packages/choreo-gallery/realm/design/film-graph/PLAN.md index 33f464b1eef..fe12244fe37 100644 --- a/packages/choreo-gallery/docs/film-graph/PLAN.md +++ b/packages/choreo-gallery/realm/design/film-graph/PLAN.md @@ -73,7 +73,7 @@ Three things have to be true at the end, and each is testable: Mockup, the Long Take, Sylva, Replate — is written the way its sketch in this folder is written, and the old `FilmSignature` with its thirty-five arguments is gone. -- **Documentation:** `docs/film.md` describes the graph; `docs/choreo-constructs.md` +- **Documentation:** `../../docs/film.md` describes the graph; `../../docs/choreo-constructs.md` §3 lists Attach and Value with the other constructs; this folder's memo, sketches and revisions log become the design record. @@ -321,12 +321,12 @@ are useful without it. ### Documentation, alongside each phase -- `docs/film.md` rewritten as the as-built reference for the graph +- `../../docs/film.md` rewritten as the as-built reference for the graph (Phase 2), keeping its §4 clock section verbatim — the load-bearing TICK offset does not change. -- `docs/choreo-constructs.md` §3 gains Attach and Value; §4 gains the +- `../../docs/choreo-constructs.md` §3 gains Attach and Value; §4 gains the driven-run and value contracts (Phases 1 and 4). -- `docs/step-vocabulary.md` gains the `Shot` base class beside +- `../../docs/step-vocabulary.md` gains the `Shot` base class beside `StepComponent` (Phase 2). - This folder moves to `docs/film-graph/` as the design record: the memo, `CONSTRUCTS.md`, `REVISIONS.md`, the six sketches (Phase 2). diff --git a/packages/choreo-gallery/docs/film-graph/README.md b/packages/choreo-gallery/realm/design/film-graph/README.md similarity index 78% rename from packages/choreo-gallery/docs/film-graph/README.md rename to packages/choreo-gallery/realm/design/film-graph/README.md index a035d6fb6ed..58d2aa9fb80 100644 --- a/packages/choreo-gallery/docs/film-graph/README.md +++ b/packages/choreo-gallery/realm/design/film-graph/README.md @@ -6,26 +6,26 @@ > real films before any of it is built. The design they follow is the > 2026-09-03 memo _Film as a Graph_ (an artifact; to be filed here). -| file | what it is | -| --------------- | ------------------------------------------------------------------------------------------------------- | -| `sagrada.gts` | Sagrada Família, 29 shots in 5 chapters, in the graph syntax | -| `towers.gts` | Towers, 26 shots in 5 chapters, in the graph syntax | -| `mockup.gts` | the Mockup demo as a film: six scenes, jump cuts and joins it could not say | -| `long-take.gts` | the Long Take as a film: one shot, two cameras, the inner one a lane of each shot | -| `sylva.gts` | Sylva as a film: a custom shot kind whose pose is a query on the picture; planes that replace | -| `replate.gts` | Replate as a film: a reel attached to a moving anchor through a time map; one filter, not an adjustment | -| `tograph.mjs` | the converter: reads a film's `BEATS` table and emits its spine, verbatim | -| `REVISIONS.md` | the running log: what each demo changed in syntax, library, parameters | -| `CONSTRUCTS.md` | the rationalisation: two engine constructs, and what everything else is | -| `PLAN.md` | what it takes to make this the baseline: comfort, spikes, phases, sizes | -| `SPIKES.md` | the two spikes' written results: the lane door passes; the fold's order is pinned | +| file | what it is | +| ----------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| [`sagrada.gts`](sagrada.gts.md) | Sagrada Família, 29 shots in 5 chapters, in the graph syntax | +| [`towers.gts`](towers.gts.md) | Towers, 26 shots in 5 chapters, in the graph syntax | +| [`mockup.gts`](mockup.gts.md) | the Mockup demo as a film: six scenes, jump cuts and joins it could not say | +| [`long-take.gts`](long-take.gts.md) | the Long Take as a film: one shot, two cameras, the inner one a lane of each shot | +| [`sylva.gts`](sylva.gts.md) | Sylva as a film: a custom shot kind whose pose is a query on the picture; planes that replace | +| [`replate.gts`](replate.gts.md) | Replate as a film: a reel attached to a moving anchor through a time map; one filter, not an adjustment | +| `tograph.mjs` | the converter, in `packages/choreo-film-app/tools/`: reads a film's `BEATS` table and emits its spine, verbatim | +| [`REVISIONS.md`](REVISIONS.md) | the running log: what each demo changed in syntax, library, parameters | +| [`CONSTRUCTS.md`](CONSTRUCTS.md) | the rationalisation: two engine constructs, and what everything else is | +| [`PLAN.md`](PLAN.md) | what it takes to make this the baseline: comfort, spikes, phases, sizes | +| [`SPIKES.md`](SPIKES.md) | the two spikes' written results: the lane door passes; the fold's order is pinned | Both spines were **generated** from the beat tables on `origin/main` (`2547ded`), not written by hand, so every pose, phrase, stamp and trace is the film's own and the translation can be re-run when the tables move: ```bash -CHAPTERS='[…]' node docs/film-graph/tograph.mjs test-app/app/components/sagrada-film.gts +CHAPTERS='[…]' node packages/choreo-film-app/tools/tograph.mjs packages/choreo-film-app/app/lib/films/sagrada.ts ``` ## The legend diff --git a/packages/choreo-gallery/docs/film-graph/REVISIONS.md b/packages/choreo-gallery/realm/design/film-graph/REVISIONS.md similarity index 99% rename from packages/choreo-gallery/docs/film-graph/REVISIONS.md rename to packages/choreo-gallery/realm/design/film-graph/REVISIONS.md index ae9f63c6f73..508babb09ab 100644 --- a/packages/choreo-gallery/docs/film-graph/REVISIONS.md +++ b/packages/choreo-gallery/realm/design/film-graph/REVISIONS.md @@ -464,7 +464,7 @@ length, not a regression to hunt. Measured: `fade('chapter', 0.9201)` one frame after the cut on main, `fade('chapter', 0)` after the fix. - _"the dip isn't super smooth. are there better curves?"_ Yes, and the - reason is compositing space, not easing. See docs/film.md §5.5. + reason is compositing space, not easing. See ../../docs/film.md §5.5. - And the tier's own regression, found while measuring the dip: the decode gate defers the CUT but not the BEAT, so the hour, the build clock and the model landed two or three frames before the seam had diff --git a/packages/choreo-gallery/docs/film-graph/SPIKES.md b/packages/choreo-gallery/realm/design/film-graph/SPIKES.md similarity index 100% rename from packages/choreo-gallery/docs/film-graph/SPIKES.md rename to packages/choreo-gallery/realm/design/film-graph/SPIKES.md diff --git a/packages/choreo-gallery/docs/film-graph/TRANSITIONS.md b/packages/choreo-gallery/realm/design/film-graph/TRANSITIONS.md similarity index 100% rename from packages/choreo-gallery/docs/film-graph/TRANSITIONS.md rename to packages/choreo-gallery/realm/design/film-graph/TRANSITIONS.md diff --git a/packages/choreo-gallery/docs/film-graph/long-take.gts b/packages/choreo-gallery/realm/design/film-graph/long-take.gts.md similarity index 98% rename from packages/choreo-gallery/docs/film-graph/long-take.gts rename to packages/choreo-gallery/realm/design/film-graph/long-take.gts.md index be1284a8cd8..db7a8094b9d 100644 --- a/packages/choreo-gallery/docs/film-graph/long-take.gts +++ b/packages/choreo-gallery/realm/design/film-graph/long-take.gts.md @@ -1,3 +1,8 @@ +# `long-take.gts`, in the graph syntax + +A sketch: it does not compile. See [the film graph](README.md). + +```gts /** * THE LONG TAKE, AS A FILM — a sketch, not a build. * @@ -176,3 +181,4 @@ export class LongTake extends Component {
} +``` diff --git a/packages/choreo-gallery/docs/film-graph/mockup.gts b/packages/choreo-gallery/realm/design/film-graph/mockup.gts.md similarity index 98% rename from packages/choreo-gallery/docs/film-graph/mockup.gts rename to packages/choreo-gallery/realm/design/film-graph/mockup.gts.md index af1c65041eb..ed4342a3d1a 100644 --- a/packages/choreo-gallery/docs/film-graph/mockup.gts +++ b/packages/choreo-gallery/realm/design/film-graph/mockup.gts.md @@ -1,3 +1,8 @@ +# `mockup.gts`, in the graph syntax + +A sketch: it does not compile. See [the film graph](README.md). + +```gts /** * THE MOCKUP, AS A FILM — a sketch, not a build. * @@ -183,3 +188,4 @@ export class Mockup extends Component {
} +``` diff --git a/packages/choreo-gallery/docs/film-graph/replate.gts b/packages/choreo-gallery/realm/design/film-graph/replate.gts.md similarity index 98% rename from packages/choreo-gallery/docs/film-graph/replate.gts rename to packages/choreo-gallery/realm/design/film-graph/replate.gts.md index 38469fb3234..4c7876cb133 100644 --- a/packages/choreo-gallery/docs/film-graph/replate.gts +++ b/packages/choreo-gallery/realm/design/film-graph/replate.gts.md @@ -1,3 +1,8 @@ +# `replate.gts`, in the graph syntax + +A sketch: it does not compile. See [the film graph](README.md). + +```gts /** * REPLATE, AS A FILM — a sketch, not a build. * @@ -121,3 +126,4 @@ export class Replate extends Component { f.send('picture', which); }; } +``` diff --git a/packages/choreo-gallery/docs/film-graph/sagrada.gts b/packages/choreo-gallery/realm/design/film-graph/sagrada.gts.md similarity index 99% rename from packages/choreo-gallery/docs/film-graph/sagrada.gts rename to packages/choreo-gallery/realm/design/film-graph/sagrada.gts.md index 1c9603681e3..05f69bed224 100644 --- a/packages/choreo-gallery/docs/film-graph/sagrada.gts +++ b/packages/choreo-gallery/realm/design/film-graph/sagrada.gts.md @@ -1,3 +1,8 @@ +# `sagrada.gts`, in the graph syntax + +A sketch: it does not compile. See [the film graph](README.md). + +```gts /** * SAGRADA FAMÍLIA, AS A GRAPH — a sketch, not a build. * @@ -1223,3 +1228,4 @@ export default class SagradaFilm extends Component<{ Args: { embed?: boolean } } } +``` diff --git a/packages/choreo-gallery/docs/film-graph/sylva.gts b/packages/choreo-gallery/realm/design/film-graph/sylva.gts.md similarity index 98% rename from packages/choreo-gallery/docs/film-graph/sylva.gts rename to packages/choreo-gallery/realm/design/film-graph/sylva.gts.md index e67febcd596..a5772bbe383 100644 --- a/packages/choreo-gallery/docs/film-graph/sylva.gts +++ b/packages/choreo-gallery/realm/design/film-graph/sylva.gts.md @@ -1,3 +1,8 @@ +# `sylva.gts`, in the graph syntax + +A sketch: it does not compile. See [the film graph](README.md). + +```gts /** * SYLVA, AS A FILM — a sketch, not a build. * @@ -157,3 +162,4 @@ export class SylvaStage extends Component<{ Args: { theater?: boolean } }> { f.send('picture', 'fly', spot.id); }; } +``` diff --git a/packages/choreo-gallery/docs/film-graph/towers.gts b/packages/choreo-gallery/realm/design/film-graph/towers.gts.md similarity index 99% rename from packages/choreo-gallery/docs/film-graph/towers.gts rename to packages/choreo-gallery/realm/design/film-graph/towers.gts.md index 2f25b9520ad..64db916e2dd 100644 --- a/packages/choreo-gallery/docs/film-graph/towers.gts +++ b/packages/choreo-gallery/realm/design/film-graph/towers.gts.md @@ -1,3 +1,8 @@ +# `towers.gts`, in the graph syntax + +A sketch: it does not compile. See [the film graph](README.md). + +```gts /** * TOWERS, AS A GRAPH — a sketch, not a build. * @@ -885,3 +890,4 @@ export default class TowerFilm extends Component<{ Args: { embed?: boolean } }> {{! THE IDENTITY: the ~140 lines of serif, hanko and cue overrides, unchanged }} } +``` diff --git a/packages/choreo-gallery/docs/gallery/PERFORMANCE.md b/packages/choreo-gallery/realm/design/gallery/PERFORMANCE.md similarity index 100% rename from packages/choreo-gallery/docs/gallery/PERFORMANCE.md rename to packages/choreo-gallery/realm/design/gallery/PERFORMANCE.md diff --git a/packages/choreo-gallery/docs/gallery/QUICK-TOUR.md b/packages/choreo-gallery/realm/design/gallery/QUICK-TOUR.md similarity index 100% rename from packages/choreo-gallery/docs/gallery/QUICK-TOUR.md rename to packages/choreo-gallery/realm/design/gallery/QUICK-TOUR.md diff --git a/packages/choreo-gallery/docs/gallery/README.md b/packages/choreo-gallery/realm/design/gallery/README.md similarity index 98% rename from packages/choreo-gallery/docs/gallery/README.md rename to packages/choreo-gallery/realm/design/gallery/README.md index 9d926250106..a662f23c2dd 100644 --- a/packages/choreo-gallery/docs/gallery/README.md +++ b/packages/choreo-gallery/realm/design/gallery/README.md @@ -67,7 +67,7 @@ assets, and repeatable capture tooling, not a hosted video link. - `scripts/capture-widget-previews-now.mjs`: recapture posters from actual cards. - `scripts/package-widget-boxel.mjs`: standalone Boxel host packaging. -See [Boxel deployment](BOXEL-DEPLOYMENT.md), [room design](ROOM-DESIGN.md), +See [room design](ROOM-DESIGN.md), [highlight score](QUICK-TOUR.md), and [performance notes](PERFORMANCE.md). The accompanying Sagrada changes refine model geometry, shader stability, material grading, and typography; its procedural model is an interpretation, diff --git a/packages/choreo-gallery/docs/gallery/ROOM-DESIGN.md b/packages/choreo-gallery/realm/design/gallery/ROOM-DESIGN.md similarity index 100% rename from packages/choreo-gallery/docs/gallery/ROOM-DESIGN.md rename to packages/choreo-gallery/realm/design/gallery/ROOM-DESIGN.md diff --git a/packages/choreo-gallery/docs/gallery/TILE-CAPACITY.md b/packages/choreo-gallery/realm/design/gallery/TILE-CAPACITY.md similarity index 100% rename from packages/choreo-gallery/docs/gallery/TILE-CAPACITY.md rename to packages/choreo-gallery/realm/design/gallery/TILE-CAPACITY.md diff --git a/packages/choreo-gallery/docs/gallery/TOUR-PACING.md b/packages/choreo-gallery/realm/design/gallery/TOUR-PACING.md similarity index 100% rename from packages/choreo-gallery/docs/gallery/TOUR-PACING.md rename to packages/choreo-gallery/realm/design/gallery/TOUR-PACING.md diff --git a/packages/choreo-gallery/docs/gallery/TOUR-SCRIPT.md b/packages/choreo-gallery/realm/design/gallery/TOUR-SCRIPT.md similarity index 100% rename from packages/choreo-gallery/docs/gallery/TOUR-SCRIPT.md rename to packages/choreo-gallery/realm/design/gallery/TOUR-SCRIPT.md diff --git a/packages/choreo-gallery/notes/hyperframes-choreo-foundation.md b/packages/choreo-gallery/realm/design/hyperframes-choreo-foundation.md similarity index 100% rename from packages/choreo-gallery/notes/hyperframes-choreo-foundation.md rename to packages/choreo-gallery/realm/design/hyperframes-choreo-foundation.md diff --git a/packages/choreo-gallery/notes/sagrada-film.md b/packages/choreo-gallery/realm/design/sagrada-film.md similarity index 98% rename from packages/choreo-gallery/notes/sagrada-film.md rename to packages/choreo-gallery/realm/design/sagrada-film.md index a368670d68f..8cf2e043571 100644 --- a/packages/choreo-gallery/notes/sagrada-film.md +++ b/packages/choreo-gallery/realm/design/sagrada-film.md @@ -1,6 +1,6 @@ # The Sagrada film -`/_sagrada` is a second film on the Towers engine (`docs/choreo-splices.md` +`/_sagrada` is a second film on the Towers engine (`../docs/choreo-splices.md` for the machinery). The scene is the construction study in `~/Projects/sagrada-familia`, vendored to `test-app/public/sagrada.html` by that repo's own build (`node build/build.mjs --lean …`), and the film is @@ -217,7 +217,7 @@ completion date, and the credits no longer claim someone else's work. ## Status: the second reference film This film is committed as the second creative reference for the Choreo -film construct — see `notes/film-construct.md` for what the two films have +film construct — see `film-construct.md` for what the two films have in common, what a `` would absorb, and the traps found building them. `/_sagrada` serves the picture alone: no wall plate, no deep dive. The notes component is still in the repo for whoever writes the construct; diff --git a/packages/choreo-gallery/notes/sagrada-vo.md b/packages/choreo-gallery/realm/design/sagrada-vo.md similarity index 100% rename from packages/choreo-gallery/notes/sagrada-vo.md rename to packages/choreo-gallery/realm/design/sagrada-vo.md diff --git a/packages/choreo-gallery/docs/sagrada-web-model.md b/packages/choreo-gallery/realm/design/sagrada-web-model.md similarity index 100% rename from packages/choreo-gallery/docs/sagrada-web-model.md rename to packages/choreo-gallery/realm/design/sagrada-web-model.md diff --git a/packages/choreo-gallery/notes/subdivision-browser-timing-test.md b/packages/choreo-gallery/realm/design/subdivision-browser-timing-test.md similarity index 100% rename from packages/choreo-gallery/notes/subdivision-browser-timing-test.md rename to packages/choreo-gallery/realm/design/subdivision-browser-timing-test.md diff --git a/packages/choreo-gallery/notes/sylva-one-world.md b/packages/choreo-gallery/realm/design/sylva-one-world.md similarity index 100% rename from packages/choreo-gallery/notes/sylva-one-world.md rename to packages/choreo-gallery/realm/design/sylva-one-world.md diff --git a/packages/choreo-gallery/notes/towers-about.md b/packages/choreo-gallery/realm/design/towers-about.md similarity index 95% rename from packages/choreo-gallery/notes/towers-about.md rename to packages/choreo-gallery/realm/design/towers-about.md index 39be0485600..4a1dcec24af 100644 --- a/packages/choreo-gallery/notes/towers-about.md +++ b/packages/choreo-gallery/realm/design/towers-about.md @@ -40,5 +40,5 @@ file. - The film: `/_towers` (front door, sound choice, player, cutting room) - The gallery face: `/towers` (the film in a frame, the deep dive below) -- The mechanics: `docs/choreo-splices.md`, `notes/towers-quality.md`, - `notes/towers-porting-delta.md` +- The mechanics: `../docs/choreo-splices.md`, `towers-quality.md`, + `towers-porting-delta.md` diff --git a/packages/choreo-gallery/notes/towers-porting-delta.md b/packages/choreo-gallery/realm/design/towers-porting-delta.md similarity index 99% rename from packages/choreo-gallery/notes/towers-porting-delta.md rename to packages/choreo-gallery/realm/design/towers-porting-delta.md index 84d872e376c..66c237f1e1b 100644 --- a/packages/choreo-gallery/notes/towers-porting-delta.md +++ b/packages/choreo-gallery/realm/design/towers-porting-delta.md @@ -183,7 +183,7 @@ ducks under the voice; the demo page's well takes the film's own aspect. Later the same cut: the narrative goes on every face — the gallery card, the door, the tile, the deep dive's opening and a director's statement -(`notes/towers-about.md`), the end card — _not a video: a live 3D composite +(`towers-about.md`), the end card — _not a video: a live 3D composite with motion graphics, interactive chapters, its own mixer, rendered in the browser at any size; entirely generated with AI, directed by a human_. The bar's chapters glyph is gone (the chapter name beside the clock is the way diff --git a/packages/choreo-gallery/notes/towers-quality.md b/packages/choreo-gallery/realm/design/towers-quality.md similarity index 99% rename from packages/choreo-gallery/notes/towers-quality.md rename to packages/choreo-gallery/realm/design/towers-quality.md index 82c5482aff7..1f0f9e2430a 100644 --- a/packages/choreo-gallery/notes/towers-quality.md +++ b/packages/choreo-gallery/realm/design/towers-quality.md @@ -120,7 +120,7 @@ scratchpad (`towers-profile.js`) and runs from the console. These are the improvements the film argues for in `packages/glimmer-motion` and its neighbours. None are built yet. -1. **A freeze-blend is a library step.** `docs/choreo-splices.md` names the +1. **A freeze-blend is a library step.** `../docs/choreo-splices.md` names the WebGL problem: a crossfade needs both frames, and a canvas has one. The answer that works is _the source keeps the freeze_ — the host asks for a dissolve by kind and duration and the glass does it. Choreo's junction diff --git a/packages/choreo-gallery/notes/towers-vo-files.md b/packages/choreo-gallery/realm/design/towers-vo-files.md similarity index 97% rename from packages/choreo-gallery/notes/towers-vo-files.md rename to packages/choreo-gallery/realm/design/towers-vo-files.md index d7998795772..6a3e9407393 100644 --- a/packages/choreo-gallery/notes/towers-vo-files.md +++ b/packages/choreo-gallery/realm/design/towers-vo-files.md @@ -7,8 +7,8 @@ does not load. Voice: **Calvin — Asian, Calm, British, Professional** - **In app:** `/asset/towers/vo/.mp3` - **On disk:** `test-app/public/asset/towers/vo/.mp3` -The lines are in `notes/towers-vo.md`, the timings in the beats themselves, and -the pronunciations in `notes/towers-vo-pronunciation.md`. This file is about +The lines are in `towers-vo.md`, the timings in the beats themselves, and +the pronunciations in `towers-vo-pronunciation.md`. This file is about the audio as audio. ## Level diff --git a/packages/choreo-gallery/notes/towers-vo-pronunciation.md b/packages/choreo-gallery/realm/design/towers-vo-pronunciation.md similarity index 97% rename from packages/choreo-gallery/notes/towers-vo-pronunciation.md rename to packages/choreo-gallery/realm/design/towers-vo-pronunciation.md index 5c0ee7b30a4..f104b9cd29b 100644 --- a/packages/choreo-gallery/notes/towers-vo-pronunciation.md +++ b/packages/choreo-gallery/realm/design/towers-vo-pronunciation.md @@ -1,6 +1,6 @@ # TOWERS — pronunciation layer for the voiceover -Companion to `notes/towers-vo.md`, which is generated from the beats and is the +Companion to `towers-vo.md`, which is generated from the beats and is the authority on WHAT is said and WHEN. This file is hand-authored and is the authority on HOW the foreign words are said. Nothing here changes a line's words or its window. @@ -54,7 +54,7 @@ before the release. Final `-e` is always "eh", never silent. ## Canonical read -The lines in `notes/towers-vo.md` are recorded verbatim. Only two carry a +The lines in `towers-vo.md` are recorded verbatim. Only two carry a foreign word, and both are respelled for the take: - **`azuchi`** — "Fifteen seventy-six. Seven gilded storeys over Lake bee-wah. @@ -79,7 +79,7 @@ air in it. **ADOPTED, 2026-09-01.** All ten alternates were recorded and are now the canonical `.mp3`; the takes that do not name the term are kept beside them as `-plain.mp3`. Each beat's `vo` in the component was updated to -match what is spoken, and `notes/towers-vo.md` regenerated from it. +match what is spoken, and `towers-vo.md` regenerated from it. One correction to the estimate above: the named takes did NOT all fit their existing windows. Four ran over — ishigaki by 1.5s, timber by 0.8s, shachi diff --git a/packages/choreo-gallery/notes/towers-vo.md b/packages/choreo-gallery/realm/design/towers-vo.md similarity index 99% rename from packages/choreo-gallery/notes/towers-vo.md rename to packages/choreo-gallery/realm/design/towers-vo.md index 1118ff26e8a..7406165cf61 100644 --- a/packages/choreo-gallery/notes/towers-vo.md +++ b/packages/choreo-gallery/realm/design/towers-vo.md @@ -19,7 +19,7 @@ in the read — the dry ones land harder delivered straight. Ten beats NAME their term aloud, once, as the first word, and never again in that beat: the screen is holding the kanji, and a narrator who repeats a word the audience is currently reading sounds like he does not trust -them. `notes/towers-vo-pronunciation.md` is the authority on how each is +them. `towers-vo-pronunciation.md` is the authority on how each is said, and the takes were prompted with its respellings. Do not chase the pictures. The camera is always moving. The voice should diff --git a/packages/choreo-gallery/notes/towers-wall-text.md b/packages/choreo-gallery/realm/design/towers-wall-text.md similarity index 100% rename from packages/choreo-gallery/notes/towers-wall-text.md rename to packages/choreo-gallery/realm/design/towers-wall-text.md diff --git a/packages/choreo-gallery/realm/docs/README.md b/packages/choreo-gallery/realm/docs/README.md new file mode 100644 index 00000000000..d6f99f4b542 --- /dev/null +++ b/packages/choreo-gallery/realm/docs/README.md @@ -0,0 +1,53 @@ +# Choreo documentation + +The long-form documentation for glimmer-motion, Choreo and choreo-player. Each +package's README is its reference: what it exports and how to install it. + +- [glimmer-motion](https://github.com/cardstack/boxel/tree/main/packages/glimmer-motion#readme): + the Motion engine bound to Glimmer, with `{{motion}}`, ``, + ``, drag and reorder +- [@cardstack/choreo](https://github.com/cardstack/boxel/tree/main/packages/choreo#readme): + ``, its vocabulary and design decisions, and `` +- [@cardstack/choreo-player](https://github.com/cardstack/boxel/tree/main/packages/choreo-player#readme): + the headless transport for clocking Choreo runs from outside + +## Start here + +- [guide.md](guide.md): Choreo from a Glimmer card. How you actually write + motion in a `.gts` file, in the order you meet the problems. + +## The choreography model + +- [choreography.md](choreography.md): the model. A region, its render pass's + changeset, and the timeline declared over it, plus the boxel-motion lineage + they descend from. +- [choreo-constructs.md](choreo-constructs.md): the full construct reference. +- [step-vocabulary.md](step-vocabulary.md): the open vocabulary. Anchors, + composite steps and derived values. +- [nested-choreo.md](nested-choreo.md): regions inside regions, and far + matching between them. +- [choreo-splices.md](choreo-splices.md): cuts as a first-class part of a score. + +## Space and film + +- [planes-and-cameras.md](planes-and-cameras.md) and [dom-in-3d.md](dom-in-3d.md): + a `` region as a plane in a three.js scene. +- [film.md](film.md): the `` construct, as built. + +## External clocks + +- [demo-recording.md](demo-recording.md): keeping a demo correct both when + someone pokes at it and when a recorder clocks it frame by frame. + +## Post-mortem + +- [postmortem-follow.md](postmortem-follow.md): the engineering post-mortem that + shaped the derived-value API. It is kept because the mistakes teach more than + the result. + +## Design history + +[The design notes](../design/README.md) are the workshop behind these documents: +design records, handoffs, measurements, and the films' scripts. They are drafts +by definition. Some describe things that were proposed and never built, or +built and then changed. diff --git a/packages/choreo-gallery/docs/choreo-constructs.md b/packages/choreo-gallery/realm/docs/choreo-constructs.md similarity index 99% rename from packages/choreo-gallery/docs/choreo-constructs.md rename to packages/choreo-gallery/realm/docs/choreo-constructs.md index 39fa81bc411..5826a59bff5 100644 --- a/packages/choreo-gallery/docs/choreo-constructs.md +++ b/packages/choreo-gallery/realm/docs/choreo-constructs.md @@ -4,7 +4,7 @@ | -------------- | ------------------------------------------------------------------------- | | **Status** | Implemented — `choreo/constructs`; demo coverage follows (§7) | | **Start date** | 2026-08-25 | -| **Package** | `glimmer-motion` (the Choreo layer) | +| **Package** | `@cardstack/choreo` | | **Drivers** | frameloop ✅ · native WAAPI/CSS realm target ☐ (§6.2) | | **Related** | [choreography.md](choreography.md) · [nested-choreo.md](nested-choreo.md) | @@ -53,7 +53,7 @@ inspector — `with`/`after` relations, delays, durations, by-word and by-character delivery, a scrubbable playhead — and the language now carries every one of those semantics natively. The demo pass (§7) finishes the thought: the inspector becomes a thin surface over a real -timeline, and its private sampled score in `test-app/app/lib/builds.ts` +timeline, and its private sampled score in the gallery's `lib/builds.ts` is deleted. ### 2.1 The Keynote map @@ -166,7 +166,7 @@ holds, else the earliest future one. Under `@exact`, a child whose score integrates (a spring, a follow) is refused once, loudly, and never driven: an integrator's state is its history, and a driven clock has none. This is the first of the two constructs the film graph needs -(docs/film-graph/CONSTRUCTS.md); `` uses it for the type, the +([film-graph/CONSTRUCTS.md](../design/film-graph/CONSTRUCTS.md)); `` uses it for the type, the insert and the clip, and it is what makes a whole film a pure function of one number. @@ -1038,7 +1038,7 @@ fits first — and the pan that solves `x + P = centre`, all from the changeset's rest-layout boxes divided back by `measureZoom`. That is the measurement space FLIP already uses, so a dive begun mid-flight on a different tile is correct by construction -(`notes/camera-api-wishlist.md` is the case study). And a cue with no +([camera-api-wishlist.md](../design/camera-api-wishlist.md) is the case study). And a cue with no origin of its own holds the aim point in force rather than recentring — backing out of a dive backs straight out of its tile, instead of sliding across the neighbouring one mid-flight. diff --git a/packages/choreo-gallery/docs/choreo-splices.md b/packages/choreo-gallery/realm/docs/choreo-splices.md similarity index 99% rename from packages/choreo-gallery/docs/choreo-splices.md rename to packages/choreo-gallery/realm/docs/choreo-splices.md index 4caead5936b..73152bd3f2e 100644 --- a/packages/choreo-gallery/docs/choreo-splices.md +++ b/packages/choreo-gallery/realm/docs/choreo-splices.md @@ -196,7 +196,7 @@ they cost one idle element for a few hundred milliseconds. ## What a splice means to the smoother (2026-09-04) `c.Camera3D @settle` filters the path by averaging it — see §4.2a of -`docs/film.md`. An average is exactly the machinery this note exists to +[film.md](film.md). An average is exactly the machinery this note exists to keep away from a seam, so the window is tapered to the shot the clock is in: it shrinks to the room it has and reaches zero at the cut itself. A seam therefore stays a step function no matter how much hand is asked diff --git a/packages/choreo-gallery/docs/choreography.md b/packages/choreo-gallery/realm/docs/choreography.md similarity index 97% rename from packages/choreo-gallery/docs/choreography.md rename to packages/choreo-gallery/realm/docs/choreography.md index 88e34d37d1a..05d1a3ea580 100644 --- a/packages/choreo-gallery/docs/choreography.md +++ b/packages/choreo-gallery/realm/docs/choreography.md @@ -215,7 +215,7 @@ timeline the legacy could only print as a matrix. completes when its row ends - **interruption** — a second dirty pass cancels timers, releases holds, and does not strand orphans (a soak over the gallery demos, so it lives in - `packages/choreo-test-app/tests/integration/choreo/`) + the host suite, `packages/host/tests/integration/choreo-gallery-interruption-test.gts`) - **counterpart** — an inserted id matching a removed one carries the old element as `counterpart` @@ -242,6 +242,6 @@ sequence repeat or extend a run's finite transport duration. Add a finite timing window when the run must be seekable. Playback, text delivery and numeric still sampling honor these offsets. -The [Circle continuity guide](../test-app/app/content/guides/interactive-circle-loop.md) -combines a parent zoom with stable circles that rearrange locally. Its playground -is `/playground/circle-loop`; the same live example also appears in the 3D gallery and full tour. +The [Circle continuity demo](../demos/circle-loop) combines a parent zoom with +stable circles that rearrange locally, and its lesson links the guide that +walks through it. diff --git a/packages/choreo-gallery/docs/demo-recording.md b/packages/choreo-gallery/realm/docs/demo-recording.md similarity index 99% rename from packages/choreo-gallery/docs/demo-recording.md rename to packages/choreo-gallery/realm/docs/demo-recording.md index 3c6d763c3b0..8deda42fbdc 100644 --- a/packages/choreo-gallery/docs/demo-recording.md +++ b/packages/choreo-gallery/realm/docs/demo-recording.md @@ -119,7 +119,7 @@ and nothing else. - **Derived values are fine, and are the good answer.** A `c.Follow` is reproducible precisely because it is a pure function of the pass's measurements and the run's clock — it never reads the live page at all - (docs/postmortem-follow.md). + ([postmortem-follow.md](postmortem-follow.md)). ## Things that pause themselves diff --git a/packages/choreo-gallery/docs/dom-in-3d.md b/packages/choreo-gallery/realm/docs/dom-in-3d.md similarity index 99% rename from packages/choreo-gallery/docs/dom-in-3d.md rename to packages/choreo-gallery/realm/docs/dom-in-3d.md index 520e6049d32..7ccaf97c7cb 100644 --- a/packages/choreo-gallery/docs/dom-in-3d.md +++ b/packages/choreo-gallery/realm/docs/dom-in-3d.md @@ -8,7 +8,7 @@ remain design. ## What exists -`test-app/app/lib/css3d.ts` is **85 lines**: three pure functions over a +`packages/choreo-film-app/app/lib/css3d.ts` is **85 lines**: three pure functions over a 4×4 matrix. ```ts @@ -254,7 +254,7 @@ explicitly. `c.Camera3D` is a step, not a loop — the pose is a pure function of the run's time, the host merely applies what it is handed. So a 3D shot seeks like any other score: `run.time`, a gate, or `choreo-player` clocking it from outside -([docs/demo-recording.md](docs/demo-recording.md)). Nothing in the 85 +([demo-recording.md](demo-recording.md)). Nothing in the 85 lines runs a rAF of its own, and nothing accumulates, so frame _t_ is a function of _t_ on a worker page too. diff --git a/packages/choreo-gallery/docs/film.md b/packages/choreo-gallery/realm/docs/film.md similarity index 99% rename from packages/choreo-gallery/docs/film.md rename to packages/choreo-gallery/realm/docs/film.md index 111e2ac0a67..3ab2bda02be 100644 --- a/packages/choreo-gallery/docs/film.md +++ b/packages/choreo-gallery/realm/docs/film.md @@ -6,7 +6,7 @@ > than replacing them. For the design record (why it exists, the two > reference films, the duplication that was measured, the fork between a > seekable film and a chased one) see -> [film-construct.md](film-construct.md). +> [film-construct.md](../design/film-construct.md). ```ts import { Film } from '@cardstack/choreo/film'; @@ -269,7 +269,7 @@ actually is; they were one thing for a while and should not have been. One limit worth knowing before you plan around it: **a beat carries one clip**. Two `f.Inset` children on the same shot compile to one row and the second wins. Lifting that is the insert-track question in -`docs/film-graph/GAPS.md`. +[film-graph/GAPS.md](../design/film-graph/GAPS.md). --- @@ -388,14 +388,14 @@ That is what makes the film **measurable**. `schedule(beats, chapters, defaultJoin)` returns the whole thing as one object — every beat's start, the cue table, the waypoints, the chapters — and two things pin it: -- `scripts/film-fixtures.mjs` writes it for each reference film to - `test-app/tests/fixtures/film/{sagrada,towers}.json`, headless, on +- `scripts/film-fixtures.mjs` in `packages/choreo-film-app` writes it for each reference film to + `tests/fixtures/film/{sagrada,towers}.json`, headless, on Node's own type stripping (`node scripts/film-fixtures.mjs`; `--check` fails when a fixture would change). It can do that because the films' data is plain TypeScript with no Ember in it: - `test-app/app/lib/films/{sagrada,towers}.ts` — the shot list, the + `app/lib/films/{sagrada,towers}.ts` — the shot list, the chapters, the measured reads, the geometry sampled off the model. -- `test-app/tests/unit/film-schedule-test.ts` recomputes the schedule in +- `tests/unit/film-schedule-test.ts` recomputes the schedule in the browser and asserts it equals the fixture to the digit. The rule that follows: **the cue table and the camera path are the diff --git a/packages/choreo-gallery/docs/guide.md b/packages/choreo-gallery/realm/docs/guide.md similarity index 93% rename from packages/choreo-gallery/docs/guide.md rename to packages/choreo-gallery/realm/docs/guide.md index c256b2e32c9..25d3c4bda00 100644 --- a/packages/choreo-gallery/docs/guide.md +++ b/packages/choreo-gallery/realm/docs/guide.md @@ -1,6 +1,6 @@ # Choreo, from a Glimmer card -The [README](../README.md) is a fidelity document: it says what Motion does and where each piece +The [glimmer-motion README](https://github.com/cardstack/boxel/tree/main/packages/glimmer-motion#readme) is a fidelity document: it says what Motion does and where each piece went. This is the other thing — how you actually write motion in a `.gts` file, in the order you meet the problems. @@ -174,8 +174,8 @@ A removed element is kept alive, locked where it stood, for exactly as long as i timeline lasts — then dropped. You do not manage that. A `` inside another `` is a separate scene: the outer one does not see the inner -one's participants, and does not collect its steps. [docs/choreography.md](choreography.md) is the -full design; [docs/nested-choreo.md](nested-choreo.md) is the nesting model. +one's participants, and does not collect its steps. [choreography.md](choreography.md) is the +full design; [nested-choreo.md](nested-choreo.md) is the nesting model. --- @@ -335,7 +335,8 @@ own settings UI. ## Where to go next -- [README](../README.md) — the whole API surface, and the React translation table -- [docs/choreography.md](choreography.md) — ``'s design, and the boxel-motion lineage -- [docs/nested-choreo.md](nested-choreo.md) — regions, nesting, and far matching -- `test-app` — every example in the gallery is a real component you can read +- [glimmer-motion's README](https://github.com/cardstack/boxel/tree/main/packages/glimmer-motion#readme) — the whole API surface, and the React translation table +- [Choreo's README](https://github.com/cardstack/boxel/tree/main/packages/choreo#readme) — the region, its vocabulary and its design decisions +- [choreography.md](choreography.md) — ``'s design, and the boxel-motion lineage +- [nested-choreo.md](nested-choreo.md) — regions, nesting, and far matching +- [the gallery](../index) — every demo is a real component you can read diff --git a/packages/choreo-gallery/docs/nested-choreo.md b/packages/choreo-gallery/realm/docs/nested-choreo.md similarity index 93% rename from packages/choreo-gallery/docs/nested-choreo.md rename to packages/choreo-gallery/realm/docs/nested-choreo.md index b0b52c1dcac..6b4aae1cbbe 100644 --- a/packages/choreo-gallery/docs/nested-choreo.md +++ b/packages/choreo-gallery/realm/docs/nested-choreo.md @@ -79,7 +79,7 @@ same render, they play side by side. There is no shared clock. ## What nesting does not do - **No shared timeline across the boundary.** Far matching (below in - spirit, `src/choreo/far.ts` in fact) pairs an inserted id in one region + spirit, `packages/choreo/src/far.ts` in fact) pairs an inserted id in one region with a removed id in another so the receiver flies from the sender's box — but each region still compiles and plays its own timeline. - **No shared clock.** The shell's `Sequence` does not wait for the @@ -232,12 +232,12 @@ the high-risk path and is out of scope. ## Status -| Piece | State | -| -------------------------------------------------- | ----------------------------------------------- | -| Nearest-ancestor host (`closestChoreo`) | done | -| `collect()` skips nested `[data-choreo]` | done | -| Isolated snapshot / changeset / orphans / timeline | done | -| Far-matching across regions | done — `src/choreo/far.ts`, the Far match demo | -| `{{beacon}}`, `c.beacon`, `changeset.beacon` | done — `src/beacon.ts`, `src/choreo/beacons.ts` | -| `Move` `@from` / `@to` rewriting initial / final | done — `compile.ts` `resolveMove` | -| App-global beacon registry | done — the version the Beacons demo needs | +| Piece | State | +| -------------------------------------------------- | ------------------------------------------------------------------------ | +| Nearest-ancestor host (`closestChoreo`) | done | +| `collect()` skips nested `[data-choreo]` | done | +| Isolated snapshot / changeset / orphans / timeline | done | +| Far-matching across regions | done — `packages/choreo/src/far.ts`, the Far match demo | +| `{{beacon}}`, `c.beacon`, `changeset.beacon` | done — `packages/choreo/src/beacon.ts`, `packages/choreo/src/beacons.ts` | +| `Move` `@from` / `@to` rewriting initial / final | done — `compile.ts` `resolveMove` | +| App-global beacon registry | done — the version the Beacons demo needs | diff --git a/packages/choreo-gallery/docs/planes-and-cameras.md b/packages/choreo-gallery/realm/docs/planes-and-cameras.md similarity index 98% rename from packages/choreo-gallery/docs/planes-and-cameras.md rename to packages/choreo-gallery/realm/docs/planes-and-cameras.md index 7438a1e7665..0901e582a2c 100644 --- a/packages/choreo-gallery/docs/planes-and-cameras.md +++ b/packages/choreo-gallery/realm/docs/planes-and-cameras.md @@ -1,7 +1,7 @@ # Planes and cameras, hand in hand **Status:** design — nothing here is implemented beyond what "Standing -today" lists. Parent documents: [choreo-composition.md](choreo-composition.md) +today" lists. Parent documents: [choreo-composition.md](../design/choreo-composition.md) (the plane concept and its review gates), [nested-choreo.md](nested-choreo.md) (regions and far matching), [demo-recording.md](demo-recording.md) (what any of this owes an external clock). @@ -26,7 +26,7 @@ BETWEEN planes without relayout. resolved against the pose in force at cue start — which is precisely what makes multi-plane direction composable: a plane's pulses stack on its own state with no absolute coupling to any other plane's score. -- Far matching (`src/choreo/far.ts`) pairs one bare id across regions at +- Far matching (`packages/choreo/src/far.ts`) pairs one bare id across regions at a render-pass barrier: MEASURE → MATCH → RUN with no frame painted between phases. The receiver flies from the sender's PAGE box as a `kept` sprite with a counterpart; the sender is dropped, not orphaned. diff --git a/packages/choreo-gallery/docs/postmortem-follow.md b/packages/choreo-gallery/realm/docs/postmortem-follow.md similarity index 100% rename from packages/choreo-gallery/docs/postmortem-follow.md rename to packages/choreo-gallery/realm/docs/postmortem-follow.md diff --git a/packages/choreo-gallery/docs/step-vocabulary.md b/packages/choreo-gallery/realm/docs/step-vocabulary.md similarity index 98% rename from packages/choreo-gallery/docs/step-vocabulary.md rename to packages/choreo-gallery/realm/docs/step-vocabulary.md index e573b43859f..8a2e15f3213 100644 --- a/packages/choreo-gallery/docs/step-vocabulary.md +++ b/packages/choreo-gallery/realm/docs/step-vocabulary.md @@ -177,7 +177,7 @@ part is not done. ## Part 3 — `c.Follow` and the derived cue > **Landed — twice.** The first shipped design handed `@read` live boxes -> and paid for it five separate ways (docs/postmortem-follow.md); what +> and paid for it five separate ways ([postmortem-follow.md](postmortem-follow.md)); what > stands now is the postmortem's replacement: **the changeset is the > context**. `@read` receives only what the pass measured — each source's > resting boxes and its composed position this frame, plus the follower's @@ -273,7 +273,7 @@ drawn, not the frame last painted: a rect read from the page mid-FLIP is a whole bay stale on frame one, because the layout has already jumped to the destination while the transform still has to carry it back. The first design corrected for that staleness per frame and found four distinct -ways for the correction to be wrong (docs/postmortem-follow.md); this one +ways for the correction to be wrong ([postmortem-follow.md](postmortem-follow.md)); this one never measures, so there is nothing to correct. And the write is _rendered_, not scheduled: a scheduled render lands a hop later than the synchronous pin the move it follows just did, which is a visible diff --git a/packages/choreo-gallery/docs/licenses/lucide-icons.txt b/packages/choreo-gallery/realm/licenses/lucide-icons.txt similarity index 100% rename from packages/choreo-gallery/docs/licenses/lucide-icons.txt rename to packages/choreo-gallery/realm/licenses/lucide-icons.txt diff --git a/packages/choreo-gallery/realm/notes/towers.gts b/packages/choreo-gallery/realm/notes/towers.gts index 4876a3505f6..c4affe7f7f3 100644 --- a/packages/choreo-gallery/realm/notes/towers.gts +++ b/packages/choreo-gallery/realm/notes/towers.gts @@ -268,7 +268,7 @@ const TowersNotes: TOC =