diff --git a/AGENTS.md b/AGENTS.md index f13f2ed..9e890e8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,8 +11,8 @@ deterministic mp4 render. Human edits survive AI regeneration of the base. ## Commands -- `pnpm reframe render [--overlay f] [-o out]` — mp4 into `out/` -- `pnpm reframe batch ` — one mp4 per row (row keys are overlay addresses like `nodes..`) +- `pnpm reframe render [--overlay f] [--theme brand.json] [-o out]` — mp4 into `out/`; `--theme` re-skins `token()` colors (a brand kit is a nested partial theme; also on `frame`/`player`) +- `pnpm reframe batch ` — one mp4 per row (row keys are overlay addresses like `nodes..` or `design.` for a per-brand re-skin) - `pnpm reframe logo [--motion ] [--energy n] [--seed n]` — animate a logo into a sting (published CLI command; `packages/render-cli/src/logoSting.ts`) - `pnpm reframe labels ` — print the compiled event clock (every timeline label → exact seconds; the timing source for `audio.cues` and beat debugging) - `pnpm reframe compile [-o out.json] [--stdin] [--code ""] [--json]` — bundle + validate eDSL source into SceneIR JSON, NO render (no ffmpeg/chromium; fast). On failure: a concise classified error (`bundle`/`eval`/`validation`), never the base64 bundle; `--json` makes it `{ok:false,error,kind,issues?}` where `issues` is the structured validation problems (each `{code,path,message}` — e.g. `code:"unknown-blend", path:"nodes.box"`). The in-process equivalent is exported as `reframe-video/compile` (`loadScene`/`loadSceneFromCode`/`checkDeterminism`, server-only); a thrown `SceneValidationError` carries `.issues` (and `.problems` for back-compat), and `SceneLoadError.issues` propagates them across the scene bundle. Entry `packages/render-cli/src/compile.ts`; loader `loadScene.ts`. @@ -158,6 +158,18 @@ no `Math.random()`/`Date` (use `wiggle` with a seed, or pass a `seed` knob). spread into node `x`/`y` (a row of cards, a grid of tiles) — pure math, no nodes. Both added after a fresh-user reproducibility test flagged hand-positioned affixes + absolute-only layout as the main friction. +- Design tokens / brand (`packages/core/src/theme.ts`, `DESIGN.md`) — the house brand as code: + `brand` (the DESIGN.md values) + `theme(overrides)` (deep-merge a reusable brand kit). `brand.color.accent` + bakes the literal at author time; `token("color.accent")` is a DEFERRED ref the compiler resolves + against the scene's `design?: DeepPartial` field (falling back to `brand`), scoped to color + props (`fill`/`stroke`/`shadowColor`). Golden-safe via `CompiledScene.hasDesign` (no `design`/`$ref` + → byte-identical; resolution only touches `$`-strings on color props, so a text `content:"$5M"` is + untouched). Overlay-addressable as `design.` (`compose.ts`, validated against the brand + shape, regen-stable by name, orphan-clean); `sceneManifest().design` surfaces them. Re-skin via + `render/frame/player --theme brand.json` (a nested partial theme, flattened in `render-cli/overlay.ts` + `loadThemeDoc`) or a `batch` `design.` column (one mp4 per brand). Demo + `examples/scenes/themed-card.ts` + `examples/data/themed-card-brands.json`. Numeric/type/gradient-stop + tokens + scene-`background` tokens are a later phase. - Photo/video montage (`packages/core/src/montage.ts`) — `photoMontage(shots, opts)` / `videoMontage` (same generator) turn a list of shots — images AND video clips, mixed (video detected by src extension, plays as a clip for its `hold`, audio muted by default diff --git a/DESIGN.md b/DESIGN.md index 01227c0..54555ac 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -75,8 +75,22 @@ const myBrand = theme({ color: { accent: "#1E90FF" } }); `packages/core/src/theme.ts` is the source of truth for the values; this document mirrors and explains them. (`brand` is pure data, so referencing a token renders byte-identical to writing -the literal. An engine-level `theme` field that the compiler resolves and an overlay can re-skin -is a later phase.) +the literal.) + +For a scene you want to RE-SKIN later, use `token("color.accent")` on a color prop instead of a +literal. It is a deferred reference the compiler resolves against the scene's `design` (or the +house brand), so the same scene renders in any brand: + +```ts +import { rect, token } from "reframe-video"; +rect({ id: "bar", fill: token("color.accent") }); +``` + +Then re-skin with no edit to the scene: `reframe frame scene.ts --theme brand.json` (a brand kit +is a nested partial theme), or a `batch` data file with a `design.` column (one mp4 +per brand). An overlay can patch `design.color.accent` directly, and the re-skin survives an AI +regen of the base (the address is the token name). Color props resolve tokens today; numeric and +type tokens are a later phase. ## Brand diff --git a/README.md b/README.md index f944aec..cf4e538 100644 --- a/README.md +++ b/README.md @@ -287,8 +287,8 @@ and `liquid-glass-nav.ts` (nav bar over a photo). | command | what it does | |---|---| -| `pnpm reframe render [--overlay f]... [-o out]` | deterministic mp4 (mode inferred from extension; output defaults to `out/`) | -| `pnpm reframe batch [-o dir] [--overlay f]...` | one mp4 per data row (rows = overlays), parallel, with a per-row report | +| `pnpm reframe render [--overlay f]... [--theme brand.json] [-o out]` | deterministic mp4 (mode inferred from extension; output defaults to `out/`); `--theme` re-skins `token()` colors | +| `pnpm reframe batch [-o dir] [--overlay f]...` | one mp4 per data row (rows = overlays; a `design.` column = one mp4 per brand), parallel, with a per-row report | | `pnpm reframe compile [-o out.json] [--json]` | bundle + validate a scene to SceneIR JSON, no render (fast; no ffmpeg/chromium); `--json` returns `{ok, kind, issues}` | | `pnpm reframe frame [--t ] [--overlay f]... [-o out.png]` | render one frame at time `t` to a PNG (chromium only, no mux); `--overlay` previews edits | | `pnpm reframe compose --overlay f... [-o out.json] [--json]` | compose overlay(s) onto a scene and emit composed SceneIR, no render (feed to `player`/`frame` for live preview) | diff --git a/docs/guides/edsl-guide.md b/docs/guides/edsl-guide.md index 5f1eda4..fa563ce 100644 --- a/docs/guides/edsl-guide.md +++ b/docs/guides/edsl-guide.md @@ -61,6 +61,30 @@ const myBrand = theme({ color: { accent: "#1E90FF" } }); `brand.motion.*`, and `brand.layout.*` cover the tokens above. Referencing a token is byte-identical to writing the literal. +### Re-skinnable scenes (`token()`) + +`brand.color.accent` bakes the literal at author time. For a scene you want to RE-SKIN later, +use `token("color.accent")` on a color prop (`fill` / `stroke` / `shadowColor`) instead. It is a +deferred reference the compiler resolves against the scene's `design` (or the house brand), so +the same scene renders in any brand: + +```ts +import { rect, token } from "@reframe/core"; +rect({ id: "bar", fill: token("color.accent") }); // resolves to the active theme +``` + +Re-skin without touching the scene: + +- one frame/render: `reframe frame scene.ts --theme brand.json` (a brand kit is a nested partial + theme, e.g. `{ "color": { "accent": "#1E90FF" } }`). +- many brands at once: a `batch` data file with a `design.` column (plus `_name`) + renders one mp4 per brand: + `[{ "_name": "ocean", "design.color.accent": "#1E90FF" }, …]` → + `reframe batch scene.ts brands.json`. + +An overlay can also patch `design.color.accent` directly, and that re-skin survives an AI regen +of the base (the address is the token name). Only color props resolve tokens today. + ## Nodes Factories return plain data. Every node needs a unique `id`. diff --git a/examples/data/themed-card-brands.json b/examples/data/themed-card-brands.json new file mode 100644 index 0000000..ef5d509 --- /dev/null +++ b/examples/data/themed-card-brands.json @@ -0,0 +1,5 @@ +[ + { "_name": "vivid", "nodes.title.content": "Vivid", "design.color.accent": "#FF3D6E", "design.color.bg": "#140A1E", "design.color.surface": "#241433" }, + { "_name": "mono", "nodes.title.content": "Mono", "design.color.accent": "#E6E6EA", "design.color.bg": "#0C0C0E", "design.color.surface": "#1A1A1E" }, + { "_name": "ocean", "nodes.title.content": "Ocean", "design.color.accent": "#1E90FF", "design.color.bg": "#0B1020", "design.color.surface": "#141B2E" } +] diff --git a/examples/themes/ocean.json b/examples/themes/ocean.json new file mode 100644 index 0000000..3fa49ca --- /dev/null +++ b/examples/themes/ocean.json @@ -0,0 +1,7 @@ +{ + "color": { + "accent": "#1E90FF", + "bg": "#0B1020", + "surface": "#141B2E" + } +} diff --git a/packages/reframe-video/README.md b/packages/reframe-video/README.md index 189a7e4..b2edd65 100644 --- a/packages/reframe-video/README.md +++ b/packages/reframe-video/README.md @@ -20,8 +20,8 @@ npx reframe-video render hello.ts # → out/hello.mp4 | command | what it does | |---|---| -| `reframe render [--overlay edits.json] [-o out.mp4]` | deterministic mp4 | -| `reframe batch ` | one mp4 per data row (row keys are overlay addresses) | +| `reframe render [--overlay edits.json] [--theme brand.json] [-o out.mp4]` | deterministic mp4; `--theme` re-skins `token()` colors | +| `reframe batch ` | one mp4 per data row (row keys are overlay addresses; a `design.` column = one mp4 per brand) | | `reframe compile [-o out.json] [--json]` | bundle + validate a scene to SceneIR JSON, no render (fast; no ffmpeg/chromium) | | `reframe frame [--t ] [-o out.png]` | render one frame at time `t` to a PNG (chromium only, no mux) | | `reframe compose --overlay f... [-o out.json]` | compose overlay(s) onto a scene and emit composed SceneIR, no render | diff --git a/packages/render-cli/src/batch.ts b/packages/render-cli/src/batch.ts index 016cab0..c1ffd6d 100644 --- a/packages/render-cli/src/batch.ts +++ b/packages/render-cli/src/batch.ts @@ -56,9 +56,12 @@ export function overlayFromFlat(row: FlatRow, name: string): OverlayDoc { ); } (doc.scene ??= {})[sceneKey as "background" | "duration" | "fps"] = value as never; + } else if (head === "design" && parts.length >= 2) { + // a design-token re-skin column, e.g. "design.color.accent" -> one mp4 per brand + (doc.design ??= {})[parts.slice(1).join(".")] = value; } else { throw new Error( - `row key "${key}" is not a valid overlay address — expected nodes.., states..., timeline.