diff --git a/.claude/skills/open-edit/SKILL.md b/.claude/skills/open-edit/SKILL.md index a7c0a42..52ecfc9 100644 --- a/.claude/skills/open-edit/SKILL.md +++ b/.claude/skills/open-edit/SKILL.md @@ -850,6 +850,10 @@ the defects `--verify` can't see). Exits: on request); on a probe FAIL offer a `--seed`/`--style` re-run; never redesign or hand-edit the .wv document · **3** = the sampled ref has no compiled recipe (stale `style.json`; rerun SAMPLE ONE STYLE). +The chain's `--verify` runs the bounds family only. After exit 0, run the SAFE-ZONE CHECK (the VERIFY invocation +in the DESIGN + RENDER step's RENDER + VERIFY block) on `runs//final` — seconds, no re-record on a clean +result — and triage per its CHROME/TRANSIENT/MINOR/MAJOR rule: chrome, transient and minor → deliver and mention; +major → fix on a `--module` copy and re-run the chain. CUSTOMISING (only when the user explicitly asks for a tweak to a recipe run): **NEVER edit a library recipe (`refs/html//recipe.ts`) in place** — it is validated, shared by every run. COPY it to your @@ -969,16 +973,89 @@ RENDER + VERIFY (OUTSIDE any sandbox — needs a real desktop session; binary = — engine-limit anti-patterns (animated blur, the stacking trap, missing cue ids, per-corner radius). Exit 1 → fix the flagged rule, re-lint before verifying. VERIFY (analytic, fast, no video, reads manifest render block): - {repo}/.veed-engine/veed-engine-cli {repo}/runs/{key}/final --verify + {repo}/.veed-engine/veed-engine-cli {repo}/runs/{key}/final --verify=bounds,safezones --verify-report {repo}/runs/{key}/final/verify.json It replays the whole timeline offscreen and checks the REAL draw list. Exit 0 = clean. Exit 1 = it prints ONE stdout line per problem, naming the element id, e.g.: frame 3 t=0.400s FAIL[bounds] #cap3 glyph 14 right 3.1px outside (8.42% of glyph box) viewport 736x1312 FAIL[never-visible] #cap5 glyph 2 ink in 300 frames, never fully visible (best 0.00% at frame 0 ...) frame 2 t=0.200s FAIL[occluded] #cap2 glyph 5 fully covered by later opaque rect + FAIL[safezone] #b4w31 zone generic-9x16 53.5% of ink outside keep-inside, worst frame 314 t=10.5s, window 10.5..11.3s, 24 offending frames (longest run 24) Built-in rules = the exact defects this pipeline hits: bounds (type off the viewport), never-visible (type clipped away in EVERY frame — e.g. stuck behind a mask/box), occluded (type fully hidden under a later opaque - layer — the z-order/opacity trap). Fix ONLY the flagged element (nudge inside the safe zone / fix z-order or the - mask) and re-run --verify until exit 0. (exit 2 = engine render failure = a real authoring error, not a nit.) + layer — the z-order/opacity trap), safezone (type inside the viewport but outside the platform-safe area — + the SAFE-ZONE CHECK below owns its triage and fix). Fix ONLY the flagged element (nudge inside the safe zone / + fix z-order or the mask) and re-run --verify until exit 0. (exit 2 = engine render failure = a real authoring + error, not a nit.) If the engine prints its usage instead of a report, it predates `--verify=` + (`--help` lists `--verify[=]` on an engine that has it): run bare `--verify` for the first three rules + and say plainly that the safe-zone check did not run — never fake it from a screenshot. + SAFE-ZONE CHECK — the `safezones` family of the VERIFY invocation above. Same walk, no extra cost; it judges + GLYPH INK only (plates, boxes, images, video and semi-transparent fills are not observed) against the + platform-safe area, and both the check and every fix below are the DOCUMENT's — the mp4 is never inspected. + Zones: bare `safezones` picks the generic preset by canvas aspect — 9:16 → x 6..89% / y 11..83% (top 11%, + bottom 17%, left 6%, right 11%: the brief's own safe margins, the band feed UI covers), 16:9 → 6% inset, + 1:1 → 5% inset. Custom zones when the user's platform or brand says so: + `--verify=bounds,safezones:{path}.json` with + `{"zones":[{"name":"...","rect":{"x":%,"y":%,"w":%,"h":%},"mode":"keep-inside|keep-out","severity":"error|warn"}],"exempt":["id"]}` + — or the same object as `"verify":{"safezones":{...}}` in manifest.json, which wins over the flag. `keep-out` + names a rect ink must stay OUT of (a logo corner, a sticker band); `warn` reports and scores but exits 0; + `exempt` lists ids whose bleed is the DESIGN (a full-bleed title, a ticker) — never an id you want to stop + failing. + READ THE REPORT: one FAIL[safezone] line per (element, zone), aggregated over the whole timeline — `% of ink` + is the element's intruding ink-area over its visible ink-area across every frame, `worst frame` is where to + look, `window` is first..last offending time, and `N offending frames (longest run M)` is how long the ink was + actually out (the window alone cannot tell one flash from two far apart). The element named is the INNERMOST id'd element carrying the + ink — in this pipeline's documents that is a WORD span (`#b4w31`); the thing you move is the LINE or BLOCK + that positions it (its `.ln` / page container), and the words of one line move together. `verify.json` + carries what stdout does not: `max_intrusion_px` per violation (the depth in canvas px — the number a fix + is sized from), `alpha_at_max_intrusion` (the run's effective opacity in that deepest frame — a slide-in still + fading up posts a deep number nobody sees), `offending_frames` / `longest_run_frames`, per-element scores and + an `overall_score` (100 = clean; ink-area-time weighted, so a brief + flash barely moves it and a line HELD outside sinks it — use it to describe, never to triage). To SEE a + worst frame: `{repo}/.veed-engine/veed-engine-cli {repo}/runs/{key}/final --headless --frame-num-until-exit {frame} --exit-screenshot {path}.png`. + CHROME FIRST: an element whose id ends in `-chrome` is DRESSING (kickers, credits, film-strip labels and + marks, stickers) — not the spoken line. The convention: a recipe or an authored document puts the suffix on + the element that DIRECTLY wraps the text (the engine labels a run by its direct parent's id; an id one level + up leaves the run reported by its text, pooled with every other run of the same text). A `FAIL[safezone]` on + a `-chrome` id is not triaged and is never fixed: name it in ONE clause at delivery ("the credits strip runs + under the feed UI") and deliver. Only content violations enter the triage below. + TRIAGE — TRANSIENT vs MINOR vs MAJOR, per positioned line, from the numbers (never by eye). Let `edge` = 2% + of the canvas's shorter side (22px on a 1080-wide canvas) and `hold` = 250ms in frames (8 at 30fps, 6 at + 25). A report without `longest_run_frames` (an engine before that field) reads the `window` length as the + hold instead — it overstates two flashes far apart, never understates a held line. + TRANSIENT: every violation on the line has `longest_run_frames` < hold AND `ink_intruding_fraction` <= 0.05. + A line's slide-in or settle crossing the margin for a few frames, usually while still fading up; the + composition is right at rest, and `max_intrusion_px` here is the animation's travel, not a placement. + NOT a fix: deliver, and mention it in ONE clause ("two lines cross the safe margin for a few frames on + entry"). Name it as a visible flick only when `alpha_at_max_intrusion` is above 0.5. + MINOR: not transient, and every violation on the line has `max_intrusion_px` <= edge AND + `ink_intruding_fraction` <= 0.25. + A descender, an overshoot, a line box set ON the margin. AUTOCORRECT without asking: shift that line's + container inward by ceil(max_intrusion_px) + 4px (the side is the one the line sits nearest — the + document tells you, the report does not), re-run VERIFY, and mention it in ONE clause at delivery + ("nudged two lines inside the safe area"). + MAJOR: anything else — deeper than `edge` and not transient, more than a quarter of the ink out, or `100%` + (the whole element outside). A band placed under the feed UI, a title wider than the zone, a wrong-canvas + assumption. + AUTOCORRECT too, but as a RE-PLACEMENT, not a nudge: put the block at the nearest legal position on its + own side (bottom band → top = zone bottom px − block height − 8px; a side → left/right = zone edge + 8px); + if the block is WIDER than the zone (left AND right both fail, or its width exceeds the zone's) step it + ONE rung down the size ladder first, then place; if `analysis.json` exists and the new position lands on + `faceBbox`, take the other band. Re-run VERIFY. A major fix changes the composition, so tell the user what + moved and by how much in plain terms — not the rule name. Never resize, recolour or re-time anything + else. + keep-out zones triage on the same numbers (depth = how far INTO the rect, fraction = how much of the ink + is in it, hold = how long); the fix moves the ink OUT the shortest way. + `WARN[safezone]` lines never block; treat them as minor for the message and fix them only if the fix is a + nudge. + At most TWO correction cycles, then stop and report honestly — a third means the placement rule is wrong, + not the number. On a COMPILED-RECIPE run the document is script-owned: a safezone FAIL is a recipe geometry + bug — a CHROME one is dressing and is delivered and mentioned; a TRANSIENT one is delivered and mentioned (the + recipe's entry animation, not its placement); a MINOR + one is delivered and reported (the recipe's own margins are the fix, offline); a MAJOR one is fixed on a + `--module` COPY (the CUSTOMISING route), never in `final/template.wv` by hand. + A delivered result the user asks you to CHECK ("is this inside the safe zones?") is the same command on its + run dir's `final/` — the check needs the document; an mp4 with no run dir has nothing to replay, so say that + rather than judging frames by eye. EXPECT WINDOWS (optional) — word-reveal TIMING: to assert a caption is shown/hidden in a time window, add a "verify" block to manifest.json alongside "render": {"verify":{"expect":[{"element":"cap3","visible":true,"from":2.1,"to":3.4}]}}. --verify then FAILs[expect-visible]/[expect-hidden] if a word isn't on-screen when it should be. Use when a @@ -1075,8 +1152,10 @@ naming the donors in `donors`), then author whole chain with one command (OUTSIDE any sandbox — verify and record need a real desktop session): `npx @veedstudio/openedit-cli gates runs/-remix` It runs design → lint → `--verify` → WCAG → `--record` → probe-qa → mux, stops at the first failure and names -the gate. A `--verify` failure: fix ONLY the flagged element and re-run, at most twice, then stop and -report honestly. A probe-qa failure: report it in plain terms and pick the fix WITH the user — never +the gate. Its `--verify` is the bounds family only, so run the SAFE-ZONE CHECK (the VERIFY invocation in the +RENDER + VERIFY block) on `runs/-remix/final` BEFORE the chain — you authored the document, the fix is +yours, and a clean document costs no re-record. A `--verify` failure: fix ONLY the flagged element and re-run, +at most twice, then stop and report honestly. A probe-qa failure: report it in plain terms and pick the fix WITH the user — never redesign. The deliverable lands next to the original, and the user compares. The WCAG AA pass runs INSIDE that chain, before the record — do not run it again afterwards. It DETECTS and REPORTS only; the chain does not pause and does not apply anything. On `status: attention` the diff --git a/README.md b/README.md index 1acf07e..bf590a9 100644 --- a/README.md +++ b/README.md @@ -24,19 +24,26 @@ OpenEdit is an open-source, agent-driven editing pipeline that ships with VEED's source, but free to use. There is no GUI and no timeline. The pipeline is driven entirely through your coding agent — edit, -cut, and reframe footage; layer motion graphics and visual elements; turn -slides or websites into video; capture web pages; and pull in any video/image generating service +cut, and reframe footage; layer motion graphics and visual elements; turn slides or websites into +video; capture web pages; and pull in any [AI video/image generator](https://www.veed.io/tools/ai-video) or MCP server when it helps. Source video is optional: stills, slides, generated media, or pure motion graphics are enough when the brief calls for it. Be creative and fluid — try new ideas rather than collapsing every ask onto captions. OpenEdit edits footage. Supply your own source files, or ask it to generate a talking-head clip with -VEED Fabric when you have none — that spends credits, and never without your approval. +[VEED Fabric](https://www.veed.io/ai/fabric-1-0) when you have none — that spends credits, and never +without your approval. OpenEdit launch video *This launch video was made in OpenEdit — click it to watch with sound.* +GPT-6 Astra title sequence made in OpenEdit + +*Also made in OpenEdit, with GPT-6 Astra driving the pipeline — pure motion graphics, no source +footage. Its prompt was written against the [OpenAI Brand Film](https://vimeo.com/1122006941) as a +visual reference.* + ## Requirements | | | @@ -98,7 +105,8 @@ Real outputs, each with the prompt that produced it. Click any example to watch create viral subtitles with /open-edit and translate my video to 5 languages using VEED Lipsync 2.0 on Fal ``` -One source clip, three languages, three caption styles: +One source clip, three languages, three caption styles — translated and re-lipsynced through +[VEED's Lip Sync API](https://www.veed.io/tools/lip-sync-api): | Spanish | French | German | | --- | --- | --- | @@ -108,7 +116,8 @@ One source clip, three languages, three caption styles: generate 3 viral hooks in Seedance 2.0 on Fal and create dynamic motion graphics using /open-edit ``` -Three AI-generated hooks, three motion-graphic treatments: +Three hooks generated with [Seedance](https://www.veed.io/tools/ai-video/seedance), three +motion-graphic treatments: | | | | | --- | --- | --- | diff --git a/docs/examples/openedit-astra.webp b/docs/examples/openedit-astra.webp new file mode 100644 index 0000000..42aa35c Binary files /dev/null and b/docs/examples/openedit-astra.webp differ diff --git a/docs/recipe-format.md b/docs/recipe-format.md index 47a0482..12a144c 100644 --- a/docs/recipe-format.md +++ b/docs/recipe-format.md @@ -66,7 +66,11 @@ fix loop · DO NOT → simply how the code behaves. 6. **VERIFY LOOP** — verbatim commands: `{repo}/.veed-engine/veed-engine-cli {repo}/runs//final --verify` → per failure class a MECHANICAL fix (bounds → which value to change by how much; never-visible → what to check; occluded → z rule), re-run - to exit 0, ≤2 fix cycles → `--record`. Manifest line included verbatim. + to exit 0, ≤2 fix cycles → `--record`. Manifest line included verbatim. Ids: the engine labels a text run by + its DIRECT parent's id, so the id goes on the element that wraps the text. Dressing text (kickers, credits, + labels, marks, stickers) ends its id in `-chrome`; the safe-zone check reports those but never fixes them, so + only the spoken line is ever moved. The bounds ladder's demotion key stops at the hyphen (`b1p2mk` for + `#b1p2mk-chrome`), so a recipe keys its demotion on the bare id. 7. **DO NOT** — no fonts/colors/keyframes beyond the sheet; no invented timing; no reading the frames; no redesign. diff --git a/pipeline/director-brief.md b/pipeline/director-brief.md index 0befe8f..9f91694 100644 --- a/pipeline/director-brief.md +++ b/pipeline/director-brief.md @@ -311,16 +311,26 @@ The binary is `{run dir}/../../.veed-engine/veed-engine-cli` (the preflight-mana — catches the engine-limit anti-patterns above (animated blur, the stacking trap, a multi-value radius, missing cue ids) before the slower verify. Exit 1 → fix the flagged rule, re-lint. 1. VERIFY (analytic, fast, no video — the ONLY self-check you perform by default; reads the manifest render block): - `.veed-engine/veed-engine-cli {run}/final --verify`. It replays the whole timeline offscreen and checks the REAL - draw list. Exit 0 = clean. Exit 1 = one stdout line per problem, naming the element id, e.g.: + `.veed-engine/veed-engine-cli {run}/final --verify=bounds,safezones --verify-report {run}/final/verify.json`. It + replays the whole timeline offscreen and checks the REAL draw list. Exit 0 = clean. Exit 1 = one stdout line per + problem, naming the element id, e.g.: `frame 3 t=0.400s FAIL[bounds] #cap3 glyph 14 right 3.1px outside (8.42% of glyph box) viewport 736x1312` `FAIL[never-visible] #cap5 glyph 2 ink in 300 frames, never fully visible (best 0.00% at frame 0 ...)` `frame 2 t=0.200s FAIL[occluded] #cap2 glyph 5 fully covered by later opaque rect` + `FAIL[safezone] #b4w31 zone generic-9x16 53.5% of ink outside keep-inside, worst frame 314 t=10.5s, window 10.5..11.3s, 24 offending frames (longest run 24)` The rules map to the real defects: bounds (type off the viewport), never-visible (type clipped away EVERY frame, - e.g. stuck behind a mask/box), occluded (type fully hidden under a later opaque layer — the z-order/opacity trap). - Fix ONLY the flagged element (nudge inside the safe zone / fix z-order or the mask) and re-run --verify until exit - 0. Do NOT change the aesthetic, colours, fonts, device, animation, or timing. (exit 2 = engine render failure = a - real authoring error.) Every text layer needs a unique `id` so the failure lines name it. OPTIONAL word-reveal + e.g. stuck behind a mask/box), occluded (type fully hidden under a later opaque layer — the z-order/opacity trap), + safezone (glyph ink outside the safe margins above — the `safezones` family picks the preset by canvas aspect, + and the 9:16 preset IS those margins). Fix ONLY the flagged element (nudge inside the safe zone / fix z-order or + the mask) and re-run --verify until exit 0. A safezone line names the WORD span; move the line that positions it, + sized from `max_intrusion_px` in verify.json — the skill's SAFE-ZONE CHECK holds the transient/minor/major rule + (a slide-in crossing the margin for a few frames is not a placement defect). An engine + that prints its usage on `--verify=` predates the rule list: run bare `--verify` and report that safe zones went + unchecked. Do NOT change the aesthetic, colours, fonts, device, animation, or timing. (exit 2 = engine render failure = a + real authoring error.) Every text layer needs a unique `id` so the failure lines name it — on the element that + DIRECTLY wraps the text, since the engine labels a run by its direct parent. Dressing text (kickers, credits, + labels, stickers — anything that is not the spoken line) ends its id in `-chrome`, which the safe-zone check + reports but never fixes. OPTIONAL word-reveal timing: add `"verify":{"expect":[{"element":"cap3","visible":true,"from":2.1,"to":3.4}]}` to manifest.json to make --verify FAIL[expect-visible]/[expect-hidden] when a caption is on/off screen at the wrong time. 2. RECORD the deliverable — ONLY after --verify is clean. --verify and --record are mutually exclusive, so this is a diff --git a/pipeline/recipes/template-lib.ts b/pipeline/recipes/template-lib.ts new file mode 100644 index 0000000..5a220fd --- /dev/null +++ b/pipeline/recipes/template-lib.ts @@ -0,0 +1,224 @@ +/** + * Shared assembly for the authored-template ports (refs/html/template-0NN/recipe.ts). Each source + * template was designed on a 480x864 canvas; a recipe authors in those px and this module scales them + * to the run's canvas, gates cues/pages, wraps lines and emits the document shell. + */ +import { + type RecipeGenerator, + type RecipeOptions, + type RecipeOutput, + type RunMeta, + type Unit, + escapeHtml, + manifestFor, + paginate, + splitLines, + winMsFor, +} from './lib.ts'; +import type { BeatTiming, WordTiming, WordTimings } from '../scripts/synth-word-timings.ts'; + +export { escapeHtml, paginate, splitLines, winMsFor, manifestFor }; +export type { RecipeGenerator, RecipeOptions, RecipeOutput, RunMeta, Unit, WordTimings, BeatTiming, WordTiming }; + +export const REF_W = 480; +export const REF_H = 864; +export const DEMOTE_STEP = 0.92; + +export type Casing = 'none' | 'uppercase' | 'lowercase'; + +export interface Canvas { + W: number; + H: number; + portrait: boolean; + /** size scale: fonts, paddings, horizontal offsets */ + s: number; + /** vertical-anchor scale (bottom/top offsets) */ + sy: number; + px: (n: number) => number; + py: (n: number) => number; + /** usable width in reference px for a block inset `margin` each side */ + refWidth: number; +} + +// Portrait scales with width (the design's own axis). Landscape keeps type at a portrait-like share of +// the frame height and lets the wider measure absorb the difference. +export function canvasFor(meta: RunMeta): Canvas { + const W = meta.width; + const H = meta.height; + const portrait = H >= W; + const s = portrait ? W / REF_W : (H / REF_H) * 1.6; + const sy = H / REF_H; + return { + W, H, portrait, s, sy, + px: (n) => Math.round(n * s), + py: (n) => Math.round(n * sy), + refWidth: W / s, + }; +} + +function applyCasing(w: string, casing: Casing): string { + if (casing === 'uppercase') return w.toUpperCase(); + if (casing === 'lowercase') return w.toLowerCase(); + return w; +} + +/** lib.toUnits with the VEED leading-"-" glue but caller-controlled casing. */ +export function unitsOf(words: WordTiming[], casing: Casing): Unit[] { + const units: Unit[] = []; + for (const wt of words) { + const text = applyCasing(wt.w, casing); + const prev = units[units.length - 1]; + if (text.startsWith('-') && prev) { + prev.spans.push({ text, delayMs: wt.delayMs }); + prev.chars += text.length; + } else { + units.push({ spans: [{ text, delayMs: wt.delayMs }], chars: text.length }); + } + } + return units; +} + +export const unitText = (u: Unit): string => u.spans.map((s) => s.text).join(''); +export const unitDelay = (u: Unit): number => u.spans[0].delayMs; + +/** Greedy wrap by a per-unit width estimate; a two-line result is rebalanced when both halves fit. */ +export function wrapByWidth(units: Unit[], maxW: number, widthOf: (u: Unit) => number, gap: number): Unit[][] { + const lines: Unit[][] = []; + let line: Unit[] = []; + let load = 0; + for (const u of units) { + const add = widthOf(u) + (line.length ? gap : 0); + if (line.length && load + add > maxW) { + lines.push(line); + line = [u]; + load = widthOf(u); + } else { + line.push(u); + load += add; + } + } + if (line.length) lines.push(line); + if (lines.length === 2) { + const [a, b] = splitLines(units); + const lw = (l: Unit[]) => l.reduce((acc, u) => acc + widthOf(u), 0) + gap * (l.length - 1); + if (b && lw(a) <= maxW && lw(b) <= maxW) return [a, b]; + } + return lines; +} + +/** Char-budget wrap: width = chars * advance, gap = one space. */ +export function wrapByChars(units: Unit[], maxChars: number): Unit[][] { + return wrapByWidth(units, maxChars, (u) => u.chars, 1); +} + +// The runner demotes by the exact element id --verify names (b…); a recipe that sizes per beat +// reads the max across every id of that beat. +// Clamped: the ladder bottoms out at 0.92^6 ≈ 0.6, so a runner that counts one demotion per failing +// frame cannot shrink a beat to nothing. +export const MAX_DEMOTE_ROWS = 6; +export function demoteRowsFor(demote: Record, n: number): number { + return Math.min(MAX_DEMOTE_ROWS, Math.max(0, ...Object.keys(demote).filter((k) => new RegExp(`^b${n}([a-z]|$)`).test(k)).map((k) => demote[k]))); +} + +/** Reveal window of unit `idx` on a flat page: until the next later delay, else the cue end. */ +export function unitWinMs(flat: Unit[], idx: number, cueEndMs: number): number { + const d = unitDelay(flat[idx]); + for (let k = idx + 1; k < flat.length; k++) { + const nd = unitDelay(flat[k]); + if (nd > d) return Math.max(80, nd - d); + } + return Math.max(80, cueEndMs - d); +} + +/** Per-glyph spans at a fixed stagger from the unit's own delay (the typewriter/rise devices). */ +export function glyphSpans(text: string, delayMs: number, stepMs: number, cls: string, styleExtra = ''): string { + return [...text] + .map((ch, k) => `${escapeHtml(ch)}`) + .join(''); +} + +/** Page gate style: mid-beat pages switch on at their first word and off at the successor's. A middle + * page runs ONE keyframe animation for its whole window, because two opacity owners on one element + * (an on ramp plus an off ramp) is a contradiction the engine resolves by dropping one. */ +export function pageGateStyle(pi: number, firstDelayMs: number, nextStartMs: number | null): string { + if (pi > 0 && nextStartMs !== null) return `animation:pgMid ${Math.max(80, nextStartMs - firstDelayMs)}ms linear ${firstDelayMs}ms forwards;`; + if (pi > 0) return `animation:pgOn 40ms linear ${firstDelayMs}ms forwards;`; + if (nextStartMs !== null) return `opacity:1;animation:pgOff 40ms linear ${nextStartMs - 40}ms forwards;`; + return 'opacity:1;'; +} + +export function cueDiv(n: number, cueDelayMs: number, winMs: number, inner: string): string { + return `
\n${inner}\n
`; +} + +export interface BeatCtx { + beat: BeatTiming; + n: number; + cueDelayMs: number; + winMs: number; + cueEndMs: number; + rows: number; + units: Unit[]; + isLast: boolean; +} + +/** Walks the beats with the shared bookkeeping (window, demotion rows, units) so recipes only lay out. */ +export function eachBeat(meta: RunMeta, timings: WordTimings, opts: RecipeOptions, casing: Casing, fn: (b: BeatCtx) => string): string[] { + const demote = opts.demote ?? {}; + const out: string[] = []; + for (let bi = 0; bi < timings.beats.length; bi++) { + const beat = timings.beats[bi]; + if (beat.words.length === 0) continue; + const winMs = winMsFor(timings.beats, bi, meta.durationSec); + out.push(fn({ + beat, + n: beat.i, + cueDelayMs: beat.cueDelayMs, + winMs, + cueEndMs: beat.cueDelayMs + winMs, + rows: demoteRowsFor(demote, beat.i), + units: unitsOf(beat.words, casing), + isLast: bi === timings.beats.length - 1, + })); + } + return out; +} + +export interface DocSpec { + canvas: Canvas; + videoPath: string; + /** css2 family params, e.g. "Baloo+2:wght@800" */ + fonts: string[]; + css: string; + body: string; +} + +export function docShell(d: DocSpec): string { + const { W, H } = d.canvas; + const link = d.fonts.length + ? `\n\n\n` + : ''; + return ` +${link} + +${d.body} +`; +} + +export function templateRecipe(id: string, gen: (meta: RunMeta, timings: WordTimings, opts: RecipeOptions) => string): RecipeGenerator { + return { + refId: id, + generate: (meta, timings, opts) => ({ wv: gen(meta, timings, opts ?? {}), manifest: manifestFor(meta) }), + }; +} diff --git a/refs/html/hook-015 (0-00-14-06)/recipe.ts b/refs/html/hook-015 (0-00-14-06)/recipe.ts index 5441b9b..e52aa62 100644 --- a/refs/html/hook-015 (0-00-14-06)/recipe.ts +++ b/refs/html/hook-015 (0-00-14-06)/recipe.ts @@ -126,8 +126,8 @@ export function heroIndex(words: WordTiming[], isLastBeat: boolean): number { // beat and points toward the second one (the old fixed positions pointed at nothing). Slot tops and // sides mirror the CSS slot table below (kept in one place via these consts). export const SLOT_TOPS_015: Record<'A' | 'B', number[]> = { - A: [145, 300, 665, 820, 1005], - B: [160, 315, 680, 835, 1020], + A: [145, 300, 665, 820, 955], + B: [160, 315, 680, 835, 970], }; export const SLOT_SIDE_015: Record<'A' | 'B', Array<'l' | 'r'>> = { A: ['l', 'r', 'l', 'r', 'l'], diff --git a/refs/html/hook-031 (0-00-00-00)/recipe.ts b/refs/html/hook-031 (0-00-00-00)/recipe.ts index b34c364..937c106 100644 --- a/refs/html/hook-031 (0-00-00-00)/recipe.ts +++ b/refs/html/hook-031 (0-00-00-00)/recipe.ts @@ -33,6 +33,8 @@ const LADDER: SizeRow[] = [ { cls: '', fs: 48, maxC: 25.8 }, { cls: '', fs: 42, maxC: 29.4 }, ]; +// safe-zone pass: anchors 62/92 leave 582px, not the 612 the table was measured for +const BUDGET_SCALE = 582 / 612; const MAX_ROWS = 5; const CANVAS_H = 1312; const INK_EM = 0.72; // Anton caps ink height (em) @@ -71,7 +73,7 @@ export function rowLoad(row: Row): number { export function beatFs(rows: Row[], demoteRows = 0): number { const L = Math.max(...rows.map(rowLoad)); - return (pickRow(LADDER, L, demoteRows) as SizeRow).fs; + return (pickRow(LADDER, L / BUDGET_SCALE, demoteRows) as SizeRow).fs; } // section 4: entrances compress when the gate closes before a full 350ms reveal (250ms floor) @@ -136,9 +138,10 @@ ${body} gl = flush left, gr = flush right (the split counterweight). */ .grp { position:absolute; z-index:2; display:inline-block; white-space:nowrap; line-height:1; letter-spacing:${p(-1)}px; color:#fede0c; font-weight:400; } - /* v3: symmetric margins — the 44/81 pair read as a leftward drift of the whole block */ + /* v3 made the margins symmetric (62/62); the safe-zone pass pulls the right anchor in to 92 — + the 11% right margin is 81px and right-flushed rows sat 19px past it */ .gl { left:${p(62)}px; } - .gr { right:${p(62)}px; } + .gr { right:${p(92)}px; } /* word reveal — the prefab's plain eased alpha fade IN, then HOLD; the cue gate is the only exit. The dark text-shadow grounds the yellow ink over light footage. */ diff --git a/refs/html/hook-045 (0-00-06-22)/recipe.ts b/refs/html/hook-045 (0-00-06-22)/recipe.ts index cd3f9f5..701ce15 100644 --- a/refs/html/hook-045 (0-00-06-22)/recipe.ts +++ b/refs/html/hook-045 (0-00-06-22)/recipe.ts @@ -52,6 +52,11 @@ export const CAP_LADDER: LadderRow[] = [ { cls: 's22', maxC: Infinity }, ]; +// safe-zone pass 2026-09-03: the ladder's fs values are rendered ×BIG_SCALE. The ladder was measured +// against a 704px centred ink budget, but the 9:16 safe zone is 611px wide (6..89%), so a full-width +// word at any rung overshot both sides; 0.84 puts the budget inside the zone with ~8px to spare. +// The rows and their maxC stay as measured. +const BIG_SCALE = 0.84; const MAX_CHARS = 26; const MAX_UNITS = 6; const FADE_MS = 250; @@ -174,7 +179,7 @@ ${pgs} `; }); - const bigRows = BIG_LADDER.map((r) => ` .${r.cls} { font-size: ${p(fsOf(r))}px; }`).join('\n'); + const bigRows = BIG_LADDER.map((r) => ` .${r.cls} { font-size: ${p(fsOf(r) * BIG_SCALE)}px; }`).join('\n'); const capRows = CAP_LADDER.map((r) => ` .${r.cls} { font-size: ${p(fsOf(r))}px; }`).join('\n'); const wv = ` @@ -200,7 +205,9 @@ ${pgs} centered by text-align (never shrink-to-fit flex); NO scaleX squeeze (--verify measures glyph ink pre-transform — parent-scaled monumental text false-fails and shears). The dark shadow is grounding: pure off-white glyphs vanish over light footage. */ - .big { position: absolute; left: 0; top: ${p(95)}px; width: ${p(736)}px; text-align: center; /* v3: same headline height as hook-210 */ + /* safe-zone pass: top 95 → 150; left -18 centres the word on the ZONE (6% left / 11% right margins put + its centre 18px left of the canvas's), so a full-budget word clears both sides */ + .big { position: absolute; left: ${p(-18)}px; top: ${p(150)}px; width: ${p(736)}px; text-align: center; font-family: 'League Gothic', sans-serif; font-weight: 400; line-height: 0.72; letter-spacing: 0.004em; color: #f6f3f7; white-space: nowrap; text-shadow: 0 ${p(4)}px ${p(28)}px rgba(0,0,0,0.40); } diff --git a/refs/html/hook-054-peak/recipe.ts b/refs/html/hook-054-peak/recipe.ts index a615afe..0ff695c 100644 --- a/refs/html/hook-054-peak/recipe.ts +++ b/refs/html/hook-054-peak/recipe.ts @@ -1,6 +1,6 @@ // Compiled recipe — hook-054-peak (9:16, authored at 736×1312 @25fps). Source sheet: ./recipe.md. // Two-register caption: per beat the first 2–3 words ride a gold italic Cormorant accent arc (per-glyph -// rotation about the fixed crown centre 368/520, radius 325, dark 8-direction stroke), the remaining +// rotation about the fixed crown centre 350/520, radius 280, dark 8-direction stroke), the remaining // words land as a white Archivo payoff block in the lower third; both reveal within ONE cue and hold // together until the gate cuts at the next beat. One hero word per beat: hot-amber on the arc (+`big` // 1.32em when a digit token — DEVICE INTENSITY axis: standard), white 800 in the body. @@ -14,34 +14,28 @@ import { } from '../../../pipeline/recipes/lib.ts'; import type { WordTiming, WordTimings } from '../../../pipeline/scripts/synth-word-timings.ts'; -const R = 325; // crown radius (px, scales) +// Safe-zone pass 2026-09-03: the crown is centred on the 9:16 safe zone (x 44..655, so 350 — not the +// canvas's 368) and the radius tightened 325 → 280 so the fan stays narrow; the apex (520 − 280 = 240) +// keeps a 1.32em hero digit above the 144px top margin. +const R = 280; // crown radius (px, scales) +const ARC_CX = 350; +const ARC_CY = 520; +// The fan must fit the zone: an end glyph's rotated box reaches ~0.5em past its centre, so the crown's +// width is 2R·sin(half) + FONT; 595 leaves 8px each side of the 611px zone. The half-angle cap keeps end +// glyphs from turning sideways (the sheet's table topped out at 59°). +const ARC_WIDTH_MAX = 595; +const ARC_HALF_MAX_DEG = 64; const BASE_IN_MS = 420; // wordIn/payIn entrance const DUR_FLOOR_MS = 250; const DEVICE_INTENSITY: 'standard' | 'calm' = 'standard'; -// Section-3 arc table, verbatim: FONT per C (1..15) + the angle rows (degrees — NEVER scaled). -const ARC_ROW_FONT = [125, 125, 125, 125, 118, 118, 106, 106, 99, 99, 93, 90, 86, 83, 80]; -const ARC_ANGLE_TABLE: string[][] = [ - ['0.00'], - ['-6.61', '6.61'], - ['-13.22', '0.00', '13.22'], - ['-19.83', '-6.61', '6.61', '19.83'], - ['-24.96', '-12.48', '0.00', '12.48', '24.96'], - ['-31.20', '-18.72', '-6.24', '6.24', '18.72', '31.20'], - ['-33.64', '-22.42', '-11.21', '0.00', '11.21', '22.42', '33.64'], - ['-39.24', '-28.03', '-16.82', '-5.61', '5.61', '16.82', '28.03', '39.24'], - ['-41.89', '-31.42', '-20.94', '-10.47', '0.00', '10.47', '20.94', '31.42', '41.89'], - ['-47.12', '-36.65', '-26.18', '-15.71', '-5.24', '5.24', '15.71', '26.18', '36.65', '47.12'], - ['-49.19', '-39.35', '-29.51', '-19.67', '-9.84', '0.00', '9.84', '19.67', '29.51', '39.35', '49.19'], - ['-52.36', '-42.84', '-33.32', '-23.80', '-14.28', '-4.76', '4.76', '14.28', '23.80', '33.32', '42.84', '52.36'], - ['-54.58', '-45.48', '-36.39', '-27.29', '-18.19', '-9.10', '0.00', '9.10', '18.19', '27.29', '36.39', '45.48', '54.58'], - ['-57.07', '-48.29', '-39.51', '-30.73', '-21.95', '-13.17', '-4.39', '4.39', '13.17', '21.95', '30.73', '39.51', '48.29', '57.07'], - ['-59.24', '-50.77', '-42.31', '-33.85', '-25.39', '-16.92', '-8.46', '0.00', '8.46', '16.92', '25.39', '33.85', '42.31', '50.77', '59.24'], -]; -// Demotion ladder: the distinct FONTs, largest→smallest ("next smaller FONT + its ANGLE row"). -export const ARC_FONT_LADDER = [125, 118, 106, 99, 93, 90, 86, 83, 80]; +// The sheet's section-3 table (FONT per C, angle rows) is replaced by its own closed form solved +// against the zone: the fan takes the LARGEST font whose half-angle and width both fit, so a long +// accent (C>15 used to keep FONT 80 and fan past ±80°) steps down instead of leaving the zone. +// Demotion ladder: the distinct FONTs, largest→smallest; the tail is the long-accent range. +export const ARC_FONT_LADDER = [125, 118, 106, 99, 93, 90, 86, 83, 80, 76, 72, 68, 64, 60, 57, 54, 51, 48, 45, 42]; -// Sheet closed form: angle(k) = (k − (C−1)/2) × step, step(deg) = 0.60·FONT·(180/π) ÷ 325. +// Sheet closed form: angle(k) = (k − (C−1)/2) × step, step(deg) = 0.60·FONT·(180/π) ÷ R. export function arcStepDeg(font: number): number { return (0.6 * font * (180 / Math.PI)) / R; } @@ -49,11 +43,20 @@ export function arcAngles(C: number, font: number): string[] { const step = arcStepDeg(font); return Array.from({ length: C }, (_, k) => ((k - (C - 1) / 2) * step).toFixed(2)); } -// Base row = the table verbatim (C>15 keeps FONT=80 with the C=15 step); demoted rows re-solve angles. +export function arcHalfDeg(C: number, font: number): number { + return ((C - 1) / 2) * arcStepDeg(font); +} +export function arcWidth(C: number, font: number): number { + return 2 * R * Math.sin((arcHalfDeg(C, font) * Math.PI) / 180) + font; +} +export function arcFits(C: number, font: number): boolean { + return arcHalfDeg(C, font) <= ARC_HALF_MAX_DEG && arcWidth(C, font) <= ARC_WIDTH_MAX; +} +// Base row = the largest fitting FONT; demoted rows step down the ladder (angles re-solved). export function arcRow(C: number, demote = 0): { font: number; angles: string[] } { - const base = ARC_FONT_LADDER.indexOf(ARC_ROW_FONT[Math.min(C, 15) - 1]); + const found = ARC_FONT_LADDER.findIndex((f) => arcFits(C, f)); + const base = found < 0 ? ARC_FONT_LADDER.length - 1 : found; const font = ARC_FONT_LADDER[Math.min(ARC_FONT_LADDER.length - 1, base + demote)]; - if (!demote && C <= 15) return { font, angles: ARC_ANGLE_TABLE[C - 1] }; return { font, angles: arcAngles(C, font) }; } @@ -225,7 +228,7 @@ function generate(meta: RunMeta, timings: WordTimings, opts: RecipeOptions = {}) /* ARC: gold Cormorant italic accent crown; font-size inline on .arc, inherited by the arc-chars */ .arc { position: absolute; inset: 0; } - .arc-char { position: absolute; left: ${p(368)}px; top: ${p(520)}px; + .arc-char { position: absolute; left: ${p(ARC_CX)}px; top: ${p(ARC_CY)}px; font-style: italic; font-weight: 700; line-height: 1; color: #e9bd2b; white-space: pre; text-shadow: -${p(1)}px -${p(1)}px 0 #15100a, ${p(1)}px -${p(1)}px 0 #15100a, -${p(1)}px ${p(1)}px 0 #15100a, ${p(1)}px ${p(1)}px 0 #15100a, @@ -239,7 +242,7 @@ function generate(meta: RunMeta, timings: WordTimings, opts: RecipeOptions = {}) @keyframes wordIn { 0% { opacity: 0; transform: translateY(0.28em); } 100% { opacity: 1; transform: translateY(0); } } /* BODY: clean white Archivo payoff (lower third) */ - .pay { position: absolute; left: ${p(68)}px; top: ${p(852)}px; width: ${p(600)}px; } + .pay { position: absolute; left: ${p(68)}px; top: ${p(960)}px; width: ${p(600)}px; } /* 852 → 960: payoff sits on the bottom safe margin, off the face */ .payline { display: block; width: ${p(600)}px; text-align: center; white-space: nowrap; line-height: 1.0; font-family: 'Archivo', system-ui, sans-serif; font-weight: 700; letter-spacing: ${f(-0.5)}px; color: #f7f5f3; text-shadow: 0 ${p(2)}px ${p(7)}px rgba(0,0,0,.6), 0 0 ${p(3)}px rgba(0,0,0,.55); } diff --git a/refs/html/hook-059-peak/recipe.ts b/refs/html/hook-059-peak/recipe.ts index 5ecad50..15d1260 100644 --- a/refs/html/hook-059-peak/recipe.ts +++ b/refs/html/hook-059-peak/recipe.ts @@ -44,7 +44,9 @@ const BALOO_MID = 0.5925; const DESC_RE = /[QJ,;]/; export const W_REF = 1088; // displayed row width (the prefab fills ~85% of its canvas) -const CY1 = 181, CY2 = 537, CYH = 298, CYC = 81; // ink-band centers measured off the prefab render +// ink-band centers measured off the prefab render; safe-zone pass 2026-09-03 pulled the two-row pair +// in by 20 each (181/537 → 201/517): the stretched caps overshot the 6% inset by ~17px top and bottom +const CY1 = 201, CY2 = 517, CYH = 298, CYC = 81; const CREDITS_TOP = 506; // scaleY stretches; layout-font caps give the prefab's visual cap sizes (366 / 356 / 321). const K_TOP = 2.27, K_BOT = 2.17, K_HEAD = 1.56; @@ -59,16 +61,17 @@ const GAP_MIN = 0.18, GAP_MAX = 2.6, LS_MAX = 2.6; // Ink bands by ROW COUNT. The beat splits at its accent — words before it, the accent, words after — // so the rows always run in the sentence's own order top to bottom. A 3-row beat needs tighter bands, // so its rows start further down the ladder (LADDER_START) to keep the stretched ink from colliding. -const BANDS: Record = { 1: [360], 2: [CY1, CY2], 3: [140, 360, 580] }; +// (3-row bands 140/360/580 → 158/360/560, safe-zone pass: the bottom row overshot the 6% inset by ~15px) +const BANDS: Record = { 1: [360], 2: [CY1, CY2], 3: [158, 360, 560] }; const LADDER_START: Record = { 1: 0, 2: 0, 3: 7 }; // Ordered layout-font ladders (×0.95 steps off each cap). The rung a row may take is filtered by its -// BAND at fit time (see fitAnton) — a descender tail must stay inside the 720 canvas. +// BAND at fit time (see fitAnton) — a descender tail must stay inside the 6% bottom inset. export const TOP_FS = [161, 153, 145, 138, 131, 125, 118, 112, 107, 101, 96, 92, 87, 83, 79, 75, 71, 67, 64, 61, 58, 55, 52, 49, 47, 45, 42, 40, 38, 36]; export const BOT_FS = [164, 156, 148, 141, 134, 127, 121, 115, 109, 103, 98, 93, 89, 84, 80, 76, 72, 69, 65, 62, 59, 56, 53, 50, 48, 45, 43, 41, 39, 37]; export const HEAD_FS = [206, 196, 186, 177, 168, 159, 151, 144, 137, 130, 123, 117, 111, 106, 100, 95, 91, 86, 82, 78, 74, 70, 67, 63, 60, 57, 54, 52, 49, 47]; export const CFS = [58, 52, 47, 42, 38, 34, 31, 28, 25, 23, 21]; // Baloo counter-line ladder -const CANVAS_H = 720; +const SAFE_BOT = 677; // the 6% bottom inset of the 720 canvas — the tail guard's floor (was the canvas edge) type RowKind = 'top' | 'bottom' | 'head'; const KIND = { @@ -106,10 +109,10 @@ export function fitAnton(units: Unit[], kind: RowKind, demote = 0, cy?: number, // The tail budget comes from the BAND, never from the row's ROLE. The reading-order split can put // any role in any band — an accent spoken first leaves a 'top' row sitting in the low band — and a // role-keyed guard let that row take the full ladder with the guard permanently off, hanging its - // Q/J/comma tails past the canvas (verify cannot see it: the stretch lives in scaleY and glyph ink - // is measured PRE-transform). Ink below the band centre is 0.545·f·k with a tail, 0.435 without. + // Q/J/comma tails past the safe bottom (bounds cannot see it: the stretch lives in scaleY and glyph + // ink is measured PRE-transform). Ink below the band centre is 0.545·f·k with a tail, 0.435 without. const reach = units.some((u) => u.spans.some((s) => DESC_RE.test(s.text))) ? 0.545 : 0.435; - const sizes = cfg.fs.slice(start).filter((f) => band + reach * f * cfg.k <= CANVAS_H); + const sizes = cfg.fs.slice(start).filter((f) => band + reach * f * cfg.k <= SAFE_BOT); const ladder = ladderFor(sizes.length ? sizes : [cfg.fs[cfg.fs.length - 1]], cfg.avg); const budget = W_REF * 0.96 ** demote; // layout px — the row's displayed width const sumAdv = units.reduce((s, u) => s + unitAdv(u), 0); diff --git a/refs/html/hook-071 (0-00-02-21)/recipe.ts b/refs/html/hook-071 (0-00-02-21)/recipe.ts index c32c4af..455f331 100644 --- a/refs/html/hook-071 (0-00-02-21)/recipe.ts +++ b/refs/html/hook-071 (0-00-02-21)/recipe.ts @@ -36,7 +36,7 @@ const SPLIT_C = 34; // widest 24px Inter BOLD line inside the 536px measure colu const HERO_BASE = 560; // the script BASELINE — the composition's one fixed anchor const H_MAX = 260; // display size every accent word reaches unless the frame budget bites const H_MIN = 88; -const HERO_W = 1140; // hero ink budget, frame-centered (70px inset each side of 1280) +const HERO_W = 1120; // hero ink budget, frame-centered (80px inset each side of 1280 — the 6% safe inset is 77) const GAP = 12; // ONE rhythm unit (half the body size) — the same clearance above and below the hero // A LETTER whose ink drops more than this below the baseline is carrying a TAIL — a script descender // or a cap swash. Tails leave the optical band and overhang the gap. Pinyon's letters split cleanly: @@ -48,7 +48,7 @@ const GAP = 12; // ONE rhythm unit (half the body size) — the same clearance a const TAIL_EM = 0.08; const LETTER = /\p{L}/u; // an unknown letter takes the fallback's deep descent → tail, like a real one const SAFE_TOP = 44; -const SAFE_BOT = 700; +const SAFE_BOT = 677; // the 6% bottom inset of the 720 canvas (was 700, which is under the margin) const PUNCH = 1.12; // punch-close cap bump const DEMOTE_STEP = 0.9; // one verify ladder step // Baseline offset from a .hero row box top at line-height 1.2: half-leading + ascent, with Pinyon's diff --git a/refs/html/hook-072 (0-00-04-10)/recipe.ts b/refs/html/hook-072 (0-00-04-10)/recipe.ts index 9bcf84c..201c0d6 100644 --- a/refs/html/hook-072 (0-00-04-10)/recipe.ts +++ b/refs/html/hook-072 (0-00-04-10)/recipe.ts @@ -24,7 +24,7 @@ export const LIMITS: LadderRow[] = [ const POP_MS = 400; const POP_MIN_MS = 250; const POP_AVAIL_MS = 500; -const STACK_TOP = 563; // 640 − 77 +const STACK_TOP = 593; // 670 − 77 (was 640 − 77; safe-zone pass: the stack's foot sits 7px above the 6% bottom inset) const PITCH = 87; // 77 + 10 gap const ANCHOR = 395; diff --git a/refs/html/hook-078 (0-00-04-15)/recipe.ts b/refs/html/hook-078 (0-00-04-15)/recipe.ts index 0316ed6..0b7cfb7 100644 --- a/refs/html/hook-078 (0-00-04-15)/recipe.ts +++ b/refs/html/hook-078 (0-00-04-15)/recipe.ts @@ -53,7 +53,10 @@ export const ROLE_FADE_MS_078: Record = { }; // Role assignment is total and deterministic by node id — spoken content only ever lands in b{N}l{k}. -export function roleFor078(nodeId: string): Role078 { +// Chrome ids end in `-chrome` (on the element that DIRECTLY wraps the text, since the engine labels a run +// by its direct parent) so the safe-zone report can set dressing aside; the role reads the bare id. +export function roleFor078(rawId: string): Role078 { + const nodeId = rawId.replace(/(-[ab])?-chrome$/, ''); if (nodeId === 'tagl') return 'kicker-left'; if (nodeId === 'tagr') return 'kicker-right'; if (/^lp\d$/.test(nodeId)) return 'laurel-panel'; @@ -144,7 +147,7 @@ function lineHtml(units: Unit[], id: string, sizeCls: string, tight: boolean, ga // v3: the title block is anchored by its BOTTOM edge — a fixed SUB_GAP to the subtitle chrome at // SUB_TOP, whatever the line count (fewer lines → more actor visible; 3 lines just start higher). -const SUB_TOP = 989; +const SUB_TOP = 1049; // 989 → 1049 (safe-zone pass: title + bottom chrome ride 60px lower, off the face) const SUB_GAP = 56; const LINE_PITCH_EM = 0.55; // v3 leading (was 0.82 — Anton: minus a third) @@ -175,12 +178,12 @@ function laurelHtml(side: 'l' | 'r', lpA: string, lpB: string, lblId: string, lb return `
-
BEST
${lblLine2}
+
BEST
${lblLine2}
`; } function seg(id: string, plain: string, bold: string): string { - return `${plain} ${bold}`; + return `${plain} ${bold}`; } function generate(meta: RunMeta, timings: WordTimings, opts: RecipeOptions = {}) { @@ -212,15 +215,15 @@ function generate(meta: RunMeta, timings: WordTimings, opts: RecipeOptions = {}) .fader { opacity: 0; animation: fadeon .45s cubic-bezier(.2,.7,.3,1) both; } @keyframes fadeon { from { opacity: 0; } to { opacity: 1; } } - #tagl { position: absolute; z-index: 2; left: ${p(87)}px; top: ${p(61)}px; font-weight: 700; font-size: ${p(18)}px; letter-spacing: ${p(2)}px; } - #tagr { position: absolute; z-index: 2; right: ${p(61)}px; top: ${p(61)}px; font-weight: 700; font-size: ${p(18)}px; letter-spacing: ${p(2)}px; } + #tagl-chrome { position: absolute; z-index: 2; left: ${p(87)}px; top: ${p(61)}px; font-weight: 700; font-size: ${p(18)}px; letter-spacing: ${p(2)}px; } + #tagr-chrome { position: absolute; z-index: 2; right: ${p(61)}px; top: ${p(61)}px; font-weight: 700; font-size: ${p(18)}px; letter-spacing: ${p(2)}px; } /* curation 2026-07-21: joint row rises 393→225; the whole spoken block (cue title + subtitle + credits) drops +206 as one unit (relative spacing untouched); bottom brand logos removed */ #joint-row { position: absolute; z-index: 2; left: 0; top: ${p(225)}px; width: ${p(736)}px; height: ${p(65)}px; } /* v4: A CREATOR! JOINT is ONE plain text entity — one font, one size, one baseline (the three-span mixed-face version read as a broken line) */ - #joint { position: absolute; top: ${p(4)}px; left: 0; width: ${p(736)}px; text-align: center; + #joint-chrome { position: absolute; top: ${p(4)}px; left: 0; width: ${p(736)}px; text-align: center; font-weight: 700; font-size: ${p(27)}px; letter-spacing: ${p(4)}px; } .laurel { position: absolute; top: 0; width: ${p(78)}px; height: ${p(65)}px; } #laurel-l { left: ${p(63)}px; } @@ -230,10 +233,10 @@ function generate(meta: RunMeta, timings: WordTimings, opts: RecipeOptions = {}) display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: ${p(9)}px; font-weight: 700; line-height: 1.05; letter-spacing: .5px; } - #subtitle-row { position: absolute; z-index: 2; left: 0; top: ${p(989)}px; width: ${p(736)}px; text-align: center; } - #subtitle { display: inline-block; font-weight: 700; font-size: ${p(21)}px; letter-spacing: ${p(2)}px; } + #subtitle-row { position: absolute; z-index: 2; left: 0; top: ${p(SUB_TOP)}px; width: ${p(736)}px; text-align: center; } + #subtitle-chrome { display: inline-block; font-weight: 700; font-size: ${p(21)}px; letter-spacing: ${p(2)}px; } - #credits { position: absolute; z-index: 2; left: 0; top: ${p(1070)}px; width: ${p(736)}px; + #credits { position: absolute; z-index: 2; left: 0; top: ${p(1130)}px; width: ${p(736)}px; display: flex; flex-direction: column; align-items: center; gap: ${p(9)}px; } .crow { display: flex; gap: ${p(17)}px; justify-content: center; font-size: ${p(19)}px; font-weight: 400; letter-spacing: .5px; } .seg { position: relative; z-index: 2; white-space: nowrap; } @@ -269,17 +272,17 @@ function generate(meta: RunMeta, timings: WordTimings, opts: RecipeOptions = {}) -
THE INTERNET
-
PRESENTS
+
THE INTERNET
+
PRESENTS
${laurelHtml('l', 'lp1', 'lp2', 'lbl1', 'PICTURE')} -
A CREATOR! JOINT
+
A CREATOR! JOINT
${laurelHtml('r', 'lp3', 'lp4', 'lbl2', 'ACT')}
-
INSPIRED BY ACTUAL EVENTS
+
INSPIRED BY ACTUAL EVENTS
diff --git a/refs/html/hook-078 (0-00-42-23)/recipe.ts b/refs/html/hook-078 (0-00-42-23)/recipe.ts index 7cdfeaf..4f2d597 100644 --- a/refs/html/hook-078 (0-00-42-23)/recipe.ts +++ b/refs/html/hook-078 (0-00-42-23)/recipe.ts @@ -45,9 +45,18 @@ export const LADDER: Row078[] = [ { cls: 't22', maxC: Infinity, tops: [591, 611, 631] }, ]; const LINE1_TOP = 571; -// Curation 2026-07-21: whole stack rides 27px lower (line-1 ink top ≈598); applied to every line so -// the ladder's leading survives; bottom-block clearance re-checked (L3 cap ink bottom ≈946 < 980). -const STACK_DROP = 27; +// Safe-zone pass 2026-09-03: the stack is BOTTOM-anchored — its last line's ink lands on STACK_BOTTOM +// whatever the line count, so a short beat no longer floats mid-frame over the face (it used to hang +// from LINE1_TOP). Line pitch is still the row's tops, read relative to LINE1_TOP. INK_EM is the italic +// caps' ink depth below a line's top (0.1em .w padding + the glyphs), rounded up; the wordIn rise adds a +// few transient px below it, which is why STACK_BOTTOM sits 29px above the 1089px margin. +const STACK_BOTTOM = 1060; +const INK_EM = 1.0; +export function stackTop078(row: Row078, L: number): number { + const fs = Number(row.cls.slice(1)); + const lastRel = L === 1 ? 0 : row.tops[L - 2] - LINE1_TOP; + return STACK_BOTTOM - lastRel - INK_EM * fs; +} // Caps held near their pre-bump absolute reach (sheet: hard vertical ceiling vs the bottom block). const CAP_L3 = LADDER.findIndex((r) => r.cls === 't93'); const CAP_L4 = LADDER.findIndex((r) => r.cls === 't76'); @@ -123,7 +132,8 @@ function generate(meta: RunMeta, timings: WordTimings, opts: RecipeOptions = {}) const ids = lines.map((_, k) => `b${beat.i}l${k + 1}`); const row = rowFor(C, lines.length, demotionFor(demote, ...ids)); const winMs = winMsFor(timings.beats, idx, meta.durationSec); - const divs = lines.map((ln, k) => lineHtml(ln, ids[k], row.cls, p((k === 0 ? LINE1_TOP : row.tops[k - 1]) + STACK_DROP))); + const top0 = stackTop078(row, lines.length); + const divs = lines.map((ln, k) => lineHtml(ln, ids[k], row.cls, p(top0 + (k === 0 ? 0 : row.tops[k - 1] - LINE1_TOP)))); return `
${divs.join('\n')} @@ -144,15 +154,18 @@ ${divs.join('\n')} body { position:relative; font-family:'Albert Sans',sans-serif; } .vid { position:absolute; inset:0; width:${p(736)}px; height:${p(1312)}px; object-fit:cover; z-index:0; } - /* persistent credits chrome — fixed text, revealed once at clip start, up for the whole video */ + /* persistent credits chrome — fixed text, revealed once at clip start, up for the whole video. Every + word span carries a -chrome id (the engine labels a run by its direct parent) so the safe-zone + report sets the dressing aside from the spoken stack. */ .chrome { position:absolute; z-index:2; color:#e62129; white-space:nowrap; } #tagl { left:${p(110)}px; top:${p(172)}px; font-weight:700; font-size:${p(15)}px; letter-spacing:${p(1)}px; line-height:${p(22)}px; } #tagr { right:${p(146)}px; top:${p(172)}px; text-align:right; font-weight:700; font-size:${p(15)}px; letter-spacing:${p(1)}px; line-height:${p(22)}px; } - /* bottom block — ONE anchor, four lines top-to-bottom; #sub2's ink bottom lands ~1084px (<1089px) */ - #basedon { left:${p(73)}px; width:${p(554)}px; top:${p(980)}px; text-align:center; font-weight:700; font-size:${p(14)}px; letter-spacing:${p(2)}px; line-height:1; } - #embreve { left:${p(73)}px; width:${p(554)}px; top:${p(999)}px; text-align:center; font-weight:800; font-size:${p(41)}px; letter-spacing:${p(3)}px; line-height:1; } - #sub1 { left:${p(73)}px; width:${p(554)}px; top:${p(1051)}px; text-align:center; font-weight:800; font-size:${p(14)}px; letter-spacing:${ph(0.5)}px; line-height:1; } - #sub2 { left:${p(73)}px; width:${p(554)}px; top:${p(1070)}px; text-align:center; font-weight:800; font-size:${p(14)}px; letter-spacing:${ph(0.5)}px; line-height:1; } + /* bottom block — ONE anchor, four lines top-to-bottom, under the bottom-anchored stack (980 → 1085): + the whole block now sits at or under the 1089px margin (chrome, reported not fixed) */ + #basedon { left:${p(73)}px; width:${p(554)}px; top:${p(1085)}px; text-align:center; font-weight:700; font-size:${p(14)}px; letter-spacing:${p(2)}px; line-height:1; } + #embreve { left:${p(73)}px; width:${p(554)}px; top:${p(1104)}px; text-align:center; font-weight:800; font-size:${p(41)}px; letter-spacing:${p(3)}px; line-height:1; } + #sub1 { left:${p(73)}px; width:${p(554)}px; top:${p(1156)}px; text-align:center; font-weight:800; font-size:${p(14)}px; letter-spacing:${ph(0.5)}px; line-height:1; } + #sub2 { left:${p(73)}px; width:${p(554)}px; top:${p(1175)}px; text-align:center; font-weight:800; font-size:${p(14)}px; letter-spacing:${ph(0.5)}px; line-height:1; } /* beat gate — the one safe reveal recipe; z-index + delay + duration come inline per cue */ @keyframes cueWin { 0%,99.99%{opacity:1} 100%{opacity:0} } @@ -187,12 +200,12 @@ ${divs.join('\n')} -
THE INTERNET
-
PRESENTS
-
BASED ON A TRUE STORY
-
COMING SOON
-
LIKE, REALLY SOON
-
TRUST US :P
+
THE INTERNET
+
PRESENTS
+
BASED ON A TRUE STORY
+
COMING SOON
+
LIKE, REALLY SOON
+
TRUST US :P
${cues.join('\n')} diff --git a/refs/html/hook-082 (0-00-43-14)/recipe.ts b/refs/html/hook-082 (0-00-43-14)/recipe.ts index a1d9f7d..f52423d 100644 --- a/refs/html/hook-082 (0-00-43-14)/recipe.ts +++ b/refs/html/hook-082 (0-00-43-14)/recipe.ts @@ -149,9 +149,10 @@ ${lines.join('\n')} @keyframes cueGate { 0%, 94% { opacity: 1; } 100% { opacity: 0; } } @keyframes cueHold { 0%, 100% { opacity: 1; } } /* FINAL beat only — rides the video end, no fade */ .cue { position: absolute; top: ${p(740)}px; width: ${p(580)}px; height: ${p(336)}px; - display: flex; flex-direction: column; justify-content: center; align-items: flex-start; + display: flex; flex-direction: column; justify-content: flex-end; align-items: flex-start; /* was center: the stack now sits on the box's bottom edge (1076), clear of the face */ opacity: 0; animation-timing-function: linear; animation-fill-mode: both; } - /* cue anchor — the ONLY thing that varies horizontally per beat; top/height never move. + /* cue anchor — the ONLY thing that varies horizontally per beat; top/height never move (the stack's + BOTTOM is the fixed edge now, so a taller beat grows upward). text-align is set HERE and inherited down into .line (which sets none of its own). */ .aL { left: ${p(64)}px; right: auto; text-align: left; } .aR { left: auto; right: ${p(100)}px; text-align: right; } diff --git a/refs/html/hook-084 (0-00-06-16)/recipe.ts b/refs/html/hook-084 (0-00-06-16)/recipe.ts index 40ba801..0852c28 100644 --- a/refs/html/hook-084 (0-00-06-16)/recipe.ts +++ b/refs/html/hook-084 (0-00-06-16)/recipe.ts @@ -39,7 +39,7 @@ const FS: Record = { // height caps: the first ladder row a beat with L lines may use (keeps stack + note-b above the credits // chrome — the window is now 992−720 = 272px, hence the tighter caps) const L_CAP_ROW: Record = { 1: 0, 2: 2, 3: 5, 4: 7 }; -const T0 = 720; // title stack top +const T0 = 780; // title stack top (720 → 780 with the credits, safe-zone pass: off the face; the 272px window is unchanged) const ADV = 1.1133; // line advance ×fs (prefab 167px @ fs150·0.8345) const INK_H = 1.17; // measured Bodoni ink bottom ×fs (incl old-style digit descenders) const CENTER_FACTOR = 498 / 640; // ladder maxC is calibrated to the 498px budget; centered lines get 640px @@ -189,8 +189,13 @@ ${body} // v3: the bottom logos are GONE — a deterministic recipe cannot know whose logo to show. In their // place: static IN REAL TIME, styled exactly like the top kickers, revealed once at clip start. -function chromeClusters(text: string, baseMs: number, K: number): string { - return clustersOf084(text, 3).map((cl, j) => clusterSpan(cl, baseMs + j * K, false, false, null)).join(''); +// Chrome text wrappers carry a `-chrome` id (the engine labels a run by its DIRECT parent's id), so the +// safe-zone report can set dressing aside from the spoken line. Unique per span: {owner}-g{j}-chrome. +function chromeSpan(text: string, delayMs: number, id: string): string { + return `${escapeHtml(text)}`; +} +function chromeClusters(owner: string, text: string, baseMs: number, K: number): string { + return clustersOf084(text, 3).map((cl, j) => chromeSpan(cl, baseMs + j * K, `${owner}-g${j + 1}-chrome`)).join(''); } interface Credit { role: string; name: string; } @@ -202,11 +207,11 @@ const CREDIT_ROWS: Credit[][] = [ function creditHtml(c: Credit, index: number, last: boolean): string { const base = creditBaseMs084(index); + const id = `cr${index + 1}`; const roleClusters = clustersOf084(c.role, 3); - const roleSpans = roleClusters.map((cl, j) => clusterSpan(cl, base + j * 32, false, false, null)).join(''); + const roleSpans = roleClusters.map((cl, j) => chromeSpan(cl, base + j * 32, `${id}-g${j + 1}-chrome`)).join(''); const nameSpans = clustersOf084(c.name, 3) - .map((cl, j) => clusterSpan(cl, base + (roleClusters.length + j) * 32, false, false, null)).join(''); - const id = `cr${index + 1}`; + .map((cl, j) => chromeSpan(cl, base + (roleClusters.length + j) * 32, `${id}-g${roleClusters.length + j + 1}-chrome`)).join(''); return `${roleSpans}${nameSpans}`; } @@ -255,7 +260,7 @@ function generate(meta: RunMeta, timings: WordTimings, opts: RecipeOptions = {}) #kickr { position: absolute; z-index: 2; right: ${p(52)}px; top: ${p(67)}px; font-family: 'Bodoni Moda', serif; font-weight: 600; font-size: ${p(15)}px; letter-spacing: ${p(1)}px; } #tagline { position: absolute; z-index: 2; left: 0; top: ${p(206)}px; width: ${p(736)}px; text-align: center; font-weight: 600; font-size: ${p(23)}px; letter-spacing: ${p(3)}px; } /* v3: a third closer to the kickers */ .crow { position: absolute; z-index: 2; left: 0; width: ${p(736)}px; text-align: center; font-size: ${p(14)}px; } - #crow1 { top: ${p(992)}px; } #crow2 { top: ${p(1019)}px; } #crow3 { top: ${p(1046)}px; } + #crow1 { top: ${p(1052)}px; } #crow2 { top: ${p(1079)}px; } #crow3 { top: ${p(1106)}px; } /* 992/1019/1046 + 60 */ .credit { display: inline-block; white-space: nowrap; margin-right: ${p(38)}px; } .credit.last { margin-right: 0; } .credit .role { font-weight: 400; opacity: 0.92; } @@ -283,14 +288,14 @@ function generate(meta: RunMeta, timings: WordTimings, opts: RecipeOptions = {}) -
${chromeClusters('THE INTERNET', ROLE_BASE_MS_084['kicker-left']!, 70)}
-
${chromeClusters('PRESENTS', ROLE_BASE_MS_084['kicker-right']!, 70)}
+
${chromeClusters('kickl', 'THE INTERNET', ROLE_BASE_MS_084['kicker-left']!, 70)}
+
${chromeClusters('kickr', 'PRESENTS', ROLE_BASE_MS_084['kicker-right']!, 70)}
-
${chromeClusters('A CREATOR! JOINT', ROLE_BASE_MS_084.tagline!, 65)}
+
${chromeClusters('tagline', 'A CREATOR! JOINT', ROLE_BASE_MS_084.tagline!, 65)}
${crows} - + ${cues.join('\n')} diff --git a/refs/html/hook-097-peak/recipe.ts b/refs/html/hook-097-peak/recipe.ts index 34c7960..872b3a9 100644 --- a/refs/html/hook-097-peak/recipe.ts +++ b/refs/html/hook-097-peak/recipe.ts @@ -27,6 +27,10 @@ const STRIP_W = 818; const FR_H2 = 150; // frame height when a page has a two-line frame const FR_H1 = 108; // …when every frame is single-line const BAND_CHROME = 62; // lab above + marks below +// safe-zone pass 2026-09-03: 800/960 → 700/860 — the bottom band (up to 212px tall, tilted) ran ~90px +// past the 17% bottom margin +const STRIP_TOP_T = 700; +const STRIP_TOP_B = 860; export const ANGLES_097: Array<[number, number]> = [[-3, 5.5], [4, -2.5], [-5, 2.5]]; const LINE_EM = 1.24; // span box: line-height 1 + 0.06/0.18em descender padding const STACK2_EM = 2.14; // two stacked lines (.ln2 pulls -0.34em) @@ -151,6 +155,8 @@ function generate(meta: RunMeta, timings: WordTimings, opts: RecipeOptions = {}) const ids = frames.map((_, f) => `${key}f${f + 1}`); const row = sizeRow097( normC097(sumMax097(frames), F), + // lab/mk ids end in -chrome (safe-zone dressing); a bounds FAIL on them still names the bare + // `${key}lab` / `${key}mk` (the ladder's id shape stops at the hyphen), which is the key here demotionFor(demote, key, ...ids, `${key}lab`, `${key}mk`), ); const fs = fsOf097(row); @@ -179,17 +185,17 @@ function generate(meta: RunMeta, timings: WordTimings, opts: RecipeOptions = {}) const anim = j === 0 ? `${top ? 'seT' : 'seB'}${beat.i} ${SLIDE_MS}ms cubic-bezier(.2,.7,.3,1) ${beat.cueDelayMs}ms both` : `${top ? 'slT' : 'slB'}${beat.i} ${SLIDE_MS}ms cubic-bezier(.2,.7,.3,1) ${pgStarts[j] - SLIDE_MS}ms both`; - const rotStyle = ` style="top:${p(top ? 800 : 960)}px; height:${p(frH + BAND_CHROME)}px; transform:rotate(${ang}deg); animation:${anim}"`; + const rotStyle = ` style="top:${p(top ? STRIP_TOP_T : STRIP_TOP_B)}px; height:${p(frH + BAND_CHROME)}px; transform:rotate(${ang}deg); animation:${anim}"`; // lower-third layout: a bottom-anchor band overlaps the TOP band's lower edge (by design), which // would fully cover the top band's bottom marks row → verify FAIL[occluded]. Multi-page beats // therefore omit the marks on top-anchored strips; a beat's lone strip keeps its full chrome. const mksRow = top && pages.length > 1 ? '' - : `\n
${top ? MK_T : MK_B}
`; + : `\n
${top ? MK_T : MK_B}
`; return `
-
${top ? LAB_T : LAB_B}
+
${top ? LAB_T : LAB_B}
${frHtml}${mksRow}
`; @@ -247,7 +253,9 @@ ${slideKfs.join('\n')} font-family: "Roboto Mono", monospace; font-weight: 600; font-size: ${p(14)}px; letter-spacing: ${p(3)}px; line-height: 1.3; color: #b9b8b4; white-space: nowrap; opacity: 0; animation-name: labIn; animation-duration: ${POP_MS}ms; animation-fill-mode: both; } - .mks { position: absolute; left: ${p(72)}px; bottom: ${p(8)}px; + /* safe-zone pass: left 72 → 110 — the band starts at -41, so the marks began at x=31 against the 44px (6%) + left margin, and the tilt pushes the low corner further out */ + .mks { position: absolute; left: ${p(110)}px; bottom: ${p(8)}px; font-family: "Roboto Mono", monospace; font-weight: 600; font-size: ${p(14)}px; letter-spacing: ${p(2)}px; line-height: 1.3; color: #cfcecb; white-space: nowrap; opacity: 0; animation-name: labIn; animation-duration: ${POP_MS}ms; animation-fill-mode: both; } diff --git a/refs/html/hook-107-peak/recipe.ts b/refs/html/hook-107-peak/recipe.ts index 3927774..66cc078 100644 --- a/refs/html/hook-107-peak/recipe.ts +++ b/refs/html/hook-107-peak/recipe.ts @@ -43,12 +43,15 @@ export const PAPER_SHAPES_107 = [ ]; export const PAPER_ROT_107 = [-3, 2.5, 2]; export const SLOTS: SlotDef[] = [ - { anchor: 'left:61px; top:85px;', font: 'sans', sizes: [162, 120, 92, 72, 58, 46, 38], budget: 594 }, - { anchor: 'left:169px; top:245px;', font: 'sans', sizes: [72, 58, 46, 38, 32], budget: 486 }, - { anchor: 'right:81px; top:315px;', font: 'serif', sizes: [90, 72, 58, 46, 38, 32], budget: 480 }, - { anchor: 'right:81px; top:875px;', font: 'serif', sizes: [86, 70, 58, 46, 38, 32], budget: 480 }, - { anchor: 'left:119px; top:985px;', font: 'serif', sizes: [92, 74, 60, 48, 40, 32], budget: 536 }, - { anchor: 'right:81px; top:1040px;', font: 'sans', sizes: [80, 64, 52, 42, 34, 28], budget: 486 }, +// safe-zone pass 2026-09-03: tops compressed toward the middle, t' = 148 + (t - 85) × 0.87 — slot 1 ran +// ~18px above the 11% top margin and slots 5/6 up to ~67px under the 17% bottom one + { anchor: 'left:61px; top:148px;', font: 'sans', sizes: [162, 120, 92, 72, 58, 46, 38], budget: 594 }, + { anchor: 'left:169px; top:287px;', font: 'sans', sizes: [72, 58, 46, 38, 32], budget: 486 }, +// (right anchors 81 → 92: 81 IS the 11% right margin, and the italic serif overshoots it by up to 10px) + { anchor: 'right:92px; top:348px;', font: 'serif', sizes: [90, 72, 58, 46, 38, 32], budget: 480 }, + { anchor: 'right:92px; top:835px;', font: 'serif', sizes: [86, 70, 58, 46, 38, 32], budget: 480 }, + { anchor: 'left:119px; top:931px;', font: 'serif', sizes: [92, 74, 60, 48, 40, 32], budget: 536 }, + { anchor: 'right:92px; top:979px;', font: 'sans', sizes: [80, 64, 52, 42, 34, 28], budget: 486 }, ]; export const STICKER_SIZES = [57, 48, 40, 34, 29, 25]; const STICKER_BUDGET = 330; @@ -224,7 +227,7 @@ ${SLOTS.map((s, i) => ` .p${i + 1} { ${s.anchor.replace(/(\d+)px/g, (_, n) => ` animate. v3: the pop runs 120%→100% with a fade-in — the paper LANDS on the frame instead of inflating from nothing. Gradient calibrated through the engine (fallback: flat #e2140c). */ /* v4: 40px left of the v3 spot — at 192 the plate cut through the right-anchored slot words */ - .stkr { position:absolute; left:${p(152)}px; top:${p(810)}px; width:${p(368)}px; height:${p(143)}px; + .stkr { position:absolute; left:${p(152)}px; top:${p(779)}px; width:${p(368)}px; height:${p(143)}px; /* 810, compressed with the slots */ transform-origin:center center; z-index:3; } .paper { position:absolute; inset:0; z-index:1; background:linear-gradient(135deg, #ff2a1c 0%, #e2140c 55%, #c20f08 100%); diff --git a/refs/html/hook-114-peak/recipe.ts b/refs/html/hook-114-peak/recipe.ts index 939a68f..5dc09fc 100644 --- a/refs/html/hook-114-peak/recipe.ts +++ b/refs/html/hook-114-peak/recipe.ts @@ -28,7 +28,7 @@ const LINE_GAP = 6; // tight stack (curation 2026-07-21; was flex gap 31) // (lower third, 810) — beats alternate zones; the engine (≥0.6.0) verifies through the cue gate, so // cross-beat top reuse is safe. Same pass: tight leading, the transplanted star under the accent // word (pink, ×2 cap height, TL/BR/BL/TR corner pattern), a single drawn underline, white accent. -const POS_TOPS = [150, 810]; +const POS_TOPS = [150, 830]; // lower slot 810 → 830 (nearer the bottom safe margin) const STAR_COLOR = '#ec307d'; // transplanted pink — kept verbatim (art-directed) const STAR_CLIP = 'polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)'; const ADV_EM = 0.48; // Gochi Hand advance em/char — MEASURED off real renders (0.58 was the ladder's conservative budget, ~20% wide) diff --git a/refs/html/hook-126 Comp 1-peak/recipe.ts b/refs/html/hook-126 Comp 1-peak/recipe.ts index ba53c22..2d777ea 100644 --- a/refs/html/hook-126 Comp 1-peak/recipe.ts +++ b/refs/html/hook-126 Comp 1-peak/recipe.ts @@ -28,8 +28,11 @@ import type { WordTiming, WordTimings } from '../../../pipeline/scripts/synth-wo // Slot geometry (reference px): the prefab's five words re-composed as SIDE-EDGE offsets that hug the // anchored frame edge — the offset family IS the ragged outer edge of the scatter. export const BASES = [108, 76, 86, 76, 80]; -export const OFFSETS = [40, 132, 24, 100, 60]; // L ≥ 3 — the staircase -export const OFFSETS_EDGE = [24, 72]; // L ≤ 2 — short beats go right out to the edge +// Safe-zone pass 2026-09-03: every offset +60 — the 16:9 safe zone is a 6% inset (77px), and the +// old family started 24px from the frame edge. The ragged shape is unchanged; INNER_RUN is measured +// from the same edge, so the inward budget shrinks by the same 60 and the centre band stays clear. +export const OFFSETS = [100, 192, 84, 160, 120]; // L ≥ 3 — the staircase +export const OFFSETS_EDGE = [84, 132]; // L ≤ 2 — short beats take the tighter family const TOP1 = 48; // The composition lives in the outer band: a line runs at most INNER_RUN px in from its anchored // edge, so the 160px band across frame centre (560…720) never carries caption ink. diff --git a/refs/html/hook-210-peak/recipe.ts b/refs/html/hook-210-peak/recipe.ts index 423037c..7b6f021 100644 --- a/refs/html/hook-210-peak/recipe.ts +++ b/refs/html/hook-210-peak/recipe.ts @@ -129,7 +129,7 @@ ${pagesHtml} /* beat gate — the one safe reveal recipe; delay+duration set inline per beat */ @keyframes cueWin { 0%, 99.99% { opacity: 1; } 100% { opacity: 0; } } - .cue { position: absolute; left: ${p(81)}px; top: ${p(95)}px; width: ${p(574)}px; height: ${p(420)}px; /* raised from 175 per curation 2026-07-21 */ + .cue { position: absolute; left: ${p(81)}px; top: ${p(150)}px; width: ${p(574)}px; height: ${p(420)}px; /* 175 → 95 (curation 2026-07-21) → 150 (safe-zone pass: ink ran ~35px above the 11% top margin) */ opacity: 0; animation-name: cueWin; animation-timing-function: linear; animation-fill-mode: forwards; } diff --git a/refs/html/hook-224-peak/recipe.ts b/refs/html/hook-224-peak/recipe.ts index 4de7d57..01371dc 100644 --- a/refs/html/hook-224-peak/recipe.ts +++ b/refs/html/hook-224-peak/recipe.ts @@ -167,7 +167,7 @@ ${lines.join('\n')} /* beat gate — the one safe reveal recipe; delay+duration + z-index come inline per cue */ @keyframes cueWin { 0%,99.99%{opacity:1} 100%{opacity:0} } - .cue { position: absolute; left: ${p(79)}px; top: ${p(683)}px; width: ${p(574)}px; + .cue { position: absolute; left: ${p(79)}px; top: ${p(803)}px; width: ${p(574)}px; /* 683 → 803: the three-line stack sits on the bottom safe margin */ display: flex; flex-direction: column; align-items: flex-start; opacity: 0; animation-name: cueWin; animation-timing-function: linear; animation-fill-mode: forwards; } diff --git a/refs/html/hook-230-peak/recipe.ts b/refs/html/hook-230-peak/recipe.ts index 76b75b0..2848729 100644 --- a/refs/html/hook-230-peak/recipe.ts +++ b/refs/html/hook-230-peak/recipe.ts @@ -201,7 +201,7 @@ function generate(meta: RunMeta, timings: WordTimings, opts: RecipeOptions = {}) (never flex around animated children). Last shadow layer = the dark ground. */ .head { position: absolute; left: 0; width: ${p(736)}px; } .top { top: ${p(250)}px; } /* upper block — the arc's old zone, now a straight title */ - .bot { top: ${p(880)}px; } /* lower block — the body in the lower third */ + .bot { top: ${p(930)}px; } /* lower block — the body in the lower third (880 → 930, nearer the bottom safe margin; 3 lines still clear it) */ .hl { display: block; white-space: nowrap; line-height: 1; text-align: center; font-family: 'Archivo Narrow', 'Archivo', sans-serif; font-weight: 500; letter-spacing: 0; color: #f8f6f4; diff --git a/refs/html/hook-244-peak/recipe.ts b/refs/html/hook-244-peak/recipe.ts index 6674d4a..8809ac4 100644 --- a/refs/html/hook-244-peak/recipe.ts +++ b/refs/html/hook-244-peak/recipe.ts @@ -133,7 +133,7 @@ function lineDiv(ws: WordTiming[], id: string, rowCls: string, p: (n: number) => function beatHtml(beat: BeatTiming, isLast: boolean, p: (n: number) => number, demote: Record): string { const N = beat.i; const clamp = clampMsFor(beat.cueDelayMs, beat.cueDurMs); - const top = N % 2 === 1 ? 165 : 800; + const top = N % 2 === 1 ? 165 : 850; // lower slot 800 → 850 (nearer the bottom safe margin) let ln = 0; const supportDivs = (group: WordTiming[]): string[] => { if (!group.length) return []; diff --git a/refs/html/template-030/recipe.md b/refs/html/template-030/recipe.md new file mode 100644 index 0000000..ece1f06 --- /dev/null +++ b/refs/html/template-030/recipe.md @@ -0,0 +1,82 @@ +# RECIPE — template-030 (9:16 · 736×1312 @ 30fps) — Sticker + +RESOLUTION: px below are authored at the source template's own canvas, 480×864; the compiled module +scales them per run (`pipeline/recipes/template-lib.ts`, `canvasFor`): portrait SCALE = W/480 for sizes +and horizontal offsets, H/864 for vertical anchors; landscape SCALE = (H/864)×1.6. `em` values and ms +timings never scale; the manifest carries the run's real W/H. The reference 9:16 canvas 736×1312 is +SCALE 1.533. + +## 1. IDENTITY + +Every word sits on its own rounded sand plate in Baloo 2 ExtraBold, popping in as it is spoken; the +beat's keyword sits in-line on a lavender plate in white italic Fraunces at 1.2×, and the closing beat's +payoff word takes the larger 1.47× plate. Blocks roam: centred, left with a stepped indent, centred, +right, by beat. + +## 2. SKELETON + +``` + + + +``` + +## 3. PER-BEAT ASSEMBLY + +``` +
+
+
{word}…
+
+
+``` +- Words lowercase. Font 30px; keyword 36px; payoff 44px. Plate width = chars × 0.57em (Fraunces 0.54em) + + horizontal padding (20 / 26 / 32px) + 6px margin. +- Placement by beat number: alignment cycles centre · left · centre · right; `bottom` cycles + 170 · 195 · 285 · 235 · 260 · 185 px. Left blocks indent line j by 20j px. Measure = 392px. +- Capacity: a page takes up to 12 units / 50 chars; lines wrap by plate width to the measure, max 3 + lines; the payoff word always owns the last line. Overflow → demote (font × 0.92 per row, up to 4). +- Mid-beat pages switch at the successor's first word (`pgOn`/`pgOff`). + +## 4. WORDS + TIMING + +One span per word, `animation-delay` = `delayMs` VERBATIM; the cue gets `{cueDelayMs}` and a window that +runs to the next beat's start (last beat to the video's end). Inter-word gap is the plate margin. + +## 5. EMPHASIS + +Keyword = `accentIndex` (digit-bearing unit, else longest, tie → later) → `.kw`. In the LAST beat the +final unit takes `.pay` instead and no keyword is drawn. + +## BOUNDED VARIETY + +Colours, both fonts and sizes are fixed; the only variety axes are the alignment/bottom cycles above. + +## 6. VERIFY LOOP + +``` +{repo}/.veed-engine/veed-engine-cli {repo}/runs//final --verify +``` +bounds on a `b{N}…` id → the runner demotes that beat one row (font × 0.92, capacities recomputed) and +regenerates; ≤ 2 cycles → `--record`. Manifest: `{"render":{"width":W,"height":H,"fps":FPS,"duration":DUR}}`. +Ids sit on the element that wraps the text (`.cl` lines, `.hw`/`.sw` titles); demotion is keyed per beat. + +## 7. DO NOT + +No fonts, colours or keyframes beyond this sheet; no invented timing; no reading the frames; no depth +compositing (the source's behind-the-presenter layers are drawn on top here); never hand-edit the output. diff --git a/refs/html/template-030/recipe.ts b/refs/html/template-030/recipe.ts new file mode 100644 index 0000000..7a527cf --- /dev/null +++ b/refs/html/template-030/recipe.ts @@ -0,0 +1,116 @@ +/** + * template-030 "Sticker": every word on its own rounded sand plate (Baloo 2) popping in as spoken; + * the beat's keyword sits in-line on a lavender plate in white italic Fraunces; the closing beat's + * payoff word gets the larger plate. Blocks roam: centred / left / centred / right by beat. + */ +import { accentIndex } from '../../../pipeline/recipes/lib.ts'; +import { + type Unit, + DEMOTE_STEP, + canvasFor, + cueDiv, + docShell, + eachBeat, + escapeHtml, + pageGateStyle, + paginate, + templateRecipe, + unitDelay, + unitText, + wrapByWidth, +} from '../../../pipeline/recipes/template-lib.ts'; + +const FONT = 30; +const KW = 36; +const PAY = 44; +const AVG_EM = 0.57; // Baloo 2 800 lowercase incl. tracking +const AVG_EM_KW = 0.54; // Fraunces italic 900 +const MARGIN = 44; +const ALIGN = ['center', 'left', 'center', 'right'] as const; +const BOTTOM = [170, 195, 285, 235, 260, 185]; +const MAX_LINES = 3; + +export default templateRecipe('template-030', (meta, timings, opts) => { + const c = canvasFor(meta); + const measure = c.refWidth - 2 * MARGIN; + let wordSeq = 0; + + const cues = eachBeat(meta, timings, opts, 'lowercase', (b) => { + const kwIdx = b.isLast ? b.units.length - 1 : accentIndex(b.units); + const role = (u: Unit): 'w' | 'kw' | 'pay' => (b.units.indexOf(u) !== kwIdx ? 'w' : b.isLast ? 'pay' : 'kw'); + const fontOf = (r: string, f: number) => (r === 'pay' ? PAY : r === 'kw' ? KW : FONT) * f; + // plate width = ink + horizontal padding + 2x3px margin + const widthOf = (u: Unit, f: number) => { + const r = role(u); + const em = r === 'w' ? AVG_EM : AVG_EM_KW; + const pad = r === 'pay' ? 32 : r === 'kw' ? 26 : 20; + return u.chars * em * fontOf(r, f) + (pad + 6) * f; + }; + + let rows = b.rows; + let f = 1; + let pages: Unit[][] = []; + let pageLines: Unit[][][] = []; + for (; rows <= b.rows + 4; rows++) { + f = Math.pow(DEMOTE_STEP, rows); + pages = paginate(b.units, Math.floor(50 * f), 12); + pageLines = pages.map((p) => { + // the payoff word owns its own last line + const pay = p.find((u) => role(u) === 'pay'); + const head = pay ? p.slice(0, p.indexOf(pay)) : p; + const lines = head.length ? wrapByWidth(head, measure, (u) => widthOf(u, f), 0) : []; + if (pay) lines.push([pay]); + return lines; + }); + if (pageLines.every((ls) => ls.length <= MAX_LINES && ls.every((l) => l.reduce((a, u) => a + widthOf(u, f), 0) <= measure))) break; + } + + const align = ALIGN[(b.n - 1) % ALIGN.length]; + const bottom = BOTTOM[(b.n - 1) % BOTTOM.length]; + const pos = align === 'center' + ? `left:0;right:0;text-align:center;` + : align === 'left' + ? `left:${c.px(MARGIN)}px;right:0;text-align:left;` + : `left:0;right:${c.px(MARGIN + 12)}px;text-align:right;`; + + const pageDivs = pages.map((page, pi) => { + const pageId = `b${b.n}p${pi + 1}`; + const firstDelay = unitDelay(page[0]); + const nextStart = pi + 1 < pages.length ? unitDelay(pages[pi + 1][0]) : null; + const lines = pageLines[pi].map((line, li) => { + const indent = align === 'left' ? `margin-left:${c.px(20 * li)}px;` : ''; + const spans = line.map((u) => u.spans.map((s) => { + wordSeq++; + const r = role(u); + const cls = r === 'w' ? 'w' : `w ${r}`; + const fs = Math.round(fontOf(r, f) * c.s); + return `${escapeHtml(s.text)}`; + }).join('')).join(''); + return `
${spans}
`; + }).join('\n '); + return `
\n ${lines}\n
`; + }); + return cueDiv(b.n, b.cueDelayMs, b.winMs, pageDivs.join('\n')); + }); + + const css = ` + .cl { display:block; line-height:1; margin-bottom:${c.px(8)}px; } + .w { display:inline-block; opacity:0; font-family:'Baloo 2'; font-weight:800; letter-spacing:0.012em; + color:#472B3F; background:#FFF4E1; border-radius:${c.px(10)}px; + padding:${c.px(3)}px ${c.px(10)}px ${c.px(7)}px; margin:0 ${c.px(3)}px; + box-shadow:0 ${c.px(3)}px ${c.px(12)}px rgba(71,43,63,0.4); + animation:wIn 240ms cubic-bezier(.2,.7,.3,1) both; } + .kw { font-family:'Fraunces'; font-style:italic; font-weight:900; letter-spacing:0.0065em; + color:#FFFFFF; background:#7C5FC9; padding:${c.px(3)}px ${c.px(13)}px ${c.px(8)}px; } + .pay { font-family:'Fraunces'; font-style:italic; font-weight:900; letter-spacing:0.0016em; + color:#FFFFFF; background:#7C5FC9; padding:${c.px(4)}px ${c.px(16)}px ${c.px(10)}px; } + @keyframes wIn { 0%{opacity:0; transform:translateY(0.35em) scale(1.08)} 100%{opacity:1; transform:translateY(0) scale(1)} }`; + + return docShell({ + canvas: c, + videoPath: meta.videoPath, + fonts: ['Baloo+2:wght@800', 'Fraunces:ital,wght@1,900'], + css, + body: cues.join('\n'), + }); +}); diff --git a/refs/html/template-030/template.wv b/refs/html/template-030/template.wv new file mode 100644 index 0000000..6025197 --- /dev/null +++ b/refs/html/template-030/template.wv @@ -0,0 +1,68 @@ + + + + + + +
+
+
ideletedevery
+
productivityapp
+
on
+
+
+
+
+
myphoneexcept
+
thisone.hearme
+
out.
+
+
+
+
+
itplansmyweek
+
inonetap.
+
+
+
+
+
anditdoesnot
+
shamemewheni
+
fallbehind.
+
+
+
+
+
firstmonth
+
isfree.
+
+
+
+
+
worstcase,you
+
gobacktoyour
+
chaos.
+
+
diff --git a/refs/html/template-030/transcript.json b/refs/html/template-030/transcript.json new file mode 100644 index 0000000..429a452 --- /dev/null +++ b/refs/html/template-030/transcript.json @@ -0,0 +1,360 @@ +{ + "text": "i deleted every productivity app on my phone except this one. hear me out. it plans my week in one tap. and it does not shame me when i fall behind. first month is free. worst case, you go back to your chaos.", + "chunks": [ + { + "text": "i deleted every productivity app on", + "timestamp": [ + 0.16, + 1.92 + ], + "words": [ + { + "text": "i", + "timestamp": [ + 0.16, + 0.28 + ] + }, + { + "text": "deleted", + "timestamp": [ + 0.28, + 0.72 + ] + }, + { + "text": "every", + "timestamp": [ + 0.72, + 0.96 + ] + }, + { + "text": "productivity", + "timestamp": [ + 0.96, + 1.6 + ] + }, + { + "text": "app", + "timestamp": [ + 1.6, + 1.8 + ] + }, + { + "text": "on", + "timestamp": [ + 1.8, + 1.92 + ] + } + ] + }, + { + "text": "my phone except this one. hear me out.", + "timestamp": [ + 1.92, + 4.16 + ], + "words": [ + { + "text": "my", + "timestamp": [ + 1.92, + 2.04 + ] + }, + { + "text": "phone", + "timestamp": [ + 2.04, + 2.32 + ] + }, + { + "text": "except", + "timestamp": [ + 2.32, + 2.64 + ] + }, + { + "text": "this", + "timestamp": [ + 2.64, + 2.8 + ] + }, + { + "text": "one.", + "timestamp": [ + 2.8, + 3.36 + ] + }, + { + "text": "hear", + "timestamp": [ + 3.36, + 3.64 + ] + }, + { + "text": "me", + "timestamp": [ + 3.64, + 3.8 + ] + }, + { + "text": "out.", + "timestamp": [ + 3.8, + 4.16 + ] + } + ] + }, + { + "text": "it plans my week in one tap.", + "timestamp": [ + 4.16, + 6.08 + ], + "words": [ + { + "text": "it", + "timestamp": [ + 4.16, + 4.44 + ] + }, + { + "text": "plans", + "timestamp": [ + 4.44, + 4.76 + ] + }, + { + "text": "my", + "timestamp": [ + 4.76, + 4.92 + ] + }, + { + "text": "week", + "timestamp": [ + 4.92, + 5.08 + ] + }, + { + "text": "in", + "timestamp": [ + 5.08, + 5.28 + ] + }, + { + "text": "one", + "timestamp": [ + 5.28, + 5.48 + ] + }, + { + "text": "tap.", + "timestamp": [ + 5.48, + 6.08 + ] + } + ] + }, + { + "text": "and it does not shame me when i fall behind.", + "timestamp": [ + 6.08, + 8.39 + ], + "words": [ + { + "text": "and", + "timestamp": [ + 6.08, + 6.36 + ] + }, + { + "text": "it", + "timestamp": [ + 6.36, + 6.48 + ] + }, + { + "text": "does", + "timestamp": [ + 6.48, + 6.6 + ] + }, + { + "text": "not", + "timestamp": [ + 6.6, + 6.76 + ] + }, + { + "text": "shame", + "timestamp": [ + 6.76, + 7.04 + ] + }, + { + "text": "me", + "timestamp": [ + 7.04, + 7.2 + ] + }, + { + "text": "when", + "timestamp": [ + 7.2, + 7.32 + ] + }, + { + "text": "i", + "timestamp": [ + 7.32, + 7.44 + ] + }, + { + "text": "fall", + "timestamp": [ + 7.44, + 7.6 + ] + }, + { + "text": "behind.", + "timestamp": [ + 7.6, + 8.39 + ] + } + ] + }, + { + "text": "first month is free.", + "timestamp": [ + 8.4, + 9.76 + ], + "words": [ + { + "text": "first", + "timestamp": [ + 8.4, + 8.72 + ] + }, + { + "text": "month", + "timestamp": [ + 8.72, + 8.96 + ] + }, + { + "text": "is", + "timestamp": [ + 8.96, + 9.16 + ] + }, + { + "text": "free.", + "timestamp": [ + 9.16, + 9.76 + ] + } + ] + }, + { + "text": "worst case, you go back to your chaos.", + "timestamp": [ + 9.76, + 11.89 + ], + "words": [ + { + "text": "worst", + "timestamp": [ + 9.76, + 10.12 + ] + }, + { + "text": "case,", + "timestamp": [ + 10.12, + 10.4 + ] + }, + { + "text": "you", + "timestamp": [ + 10.4, + 10.68 + ] + }, + { + "text": "go", + "timestamp": [ + 10.68, + 10.84 + ] + }, + { + "text": "back", + "timestamp": [ + 10.84, + 11 + ] + }, + { + "text": "to", + "timestamp": [ + 11, + 11.12 + ] + }, + { + "text": "your", + "timestamp": [ + 11.12, + 11.24 + ] + }, + { + "text": "chaos.", + "timestamp": [ + 11.24, + 11.89 + ] + } + ] + } + ] +} diff --git a/refs/html/template-030/word-timings.json b/refs/html/template-030/word-timings.json new file mode 100644 index 0000000..975df35 --- /dev/null +++ b/refs/html/template-030/word-timings.json @@ -0,0 +1,230 @@ +{ + "beats": [ + { + "i": 1, + "startSec": 0.16, + "endSec": 1.92, + "cueDelayMs": 160, + "cueDurMs": 1760, + "words": [ + { + "w": "i", + "delayMs": 160 + }, + { + "w": "deleted", + "delayMs": 280 + }, + { + "w": "every", + "delayMs": 720 + }, + { + "w": "productivity", + "delayMs": 960 + }, + { + "w": "app", + "delayMs": 1600 + }, + { + "w": "on", + "delayMs": 1800 + } + ] + }, + { + "i": 2, + "startSec": 1.92, + "endSec": 4.16, + "cueDelayMs": 1920, + "cueDurMs": 2240, + "words": [ + { + "w": "my", + "delayMs": 1920 + }, + { + "w": "phone", + "delayMs": 2040 + }, + { + "w": "except", + "delayMs": 2320 + }, + { + "w": "this", + "delayMs": 2640 + }, + { + "w": "one.", + "delayMs": 2800 + }, + { + "w": "hear", + "delayMs": 3360 + }, + { + "w": "me", + "delayMs": 3640 + }, + { + "w": "out.", + "delayMs": 3800 + } + ] + }, + { + "i": 3, + "startSec": 4.16, + "endSec": 6.08, + "cueDelayMs": 4160, + "cueDurMs": 1920, + "words": [ + { + "w": "it", + "delayMs": 4160 + }, + { + "w": "plans", + "delayMs": 4440 + }, + { + "w": "my", + "delayMs": 4760 + }, + { + "w": "week", + "delayMs": 4920 + }, + { + "w": "in", + "delayMs": 5080 + }, + { + "w": "one", + "delayMs": 5280 + }, + { + "w": "tap.", + "delayMs": 5480 + } + ] + }, + { + "i": 4, + "startSec": 6.08, + "endSec": 8.39, + "cueDelayMs": 6080, + "cueDurMs": 2310, + "words": [ + { + "w": "and", + "delayMs": 6080 + }, + { + "w": "it", + "delayMs": 6360 + }, + { + "w": "does", + "delayMs": 6480 + }, + { + "w": "not", + "delayMs": 6600 + }, + { + "w": "shame", + "delayMs": 6760 + }, + { + "w": "me", + "delayMs": 7040 + }, + { + "w": "when", + "delayMs": 7200 + }, + { + "w": "i", + "delayMs": 7320 + }, + { + "w": "fall", + "delayMs": 7440 + }, + { + "w": "behind.", + "delayMs": 7600 + } + ] + }, + { + "i": 5, + "startSec": 8.4, + "endSec": 9.76, + "cueDelayMs": 8400, + "cueDurMs": 1360, + "words": [ + { + "w": "first", + "delayMs": 8400 + }, + { + "w": "month", + "delayMs": 8720 + }, + { + "w": "is", + "delayMs": 8960 + }, + { + "w": "free.", + "delayMs": 9160 + } + ] + }, + { + "i": 6, + "startSec": 9.76, + "endSec": 11.89, + "cueDelayMs": 9760, + "cueDurMs": 2130, + "words": [ + { + "w": "worst", + "delayMs": 9760 + }, + { + "w": "case,", + "delayMs": 10120 + }, + { + "w": "you", + "delayMs": 10400 + }, + { + "w": "go", + "delayMs": 10680 + }, + { + "w": "back", + "delayMs": 10840 + }, + { + "w": "to", + "delayMs": 11000 + }, + { + "w": "your", + "delayMs": 11120 + }, + { + "w": "chaos.", + "delayMs": 11240 + } + ] + } + ] +} diff --git a/refs/html/template-031/recipe.md b/refs/html/template-031/recipe.md new file mode 100644 index 0000000..5258562 --- /dev/null +++ b/refs/html/template-031/recipe.md @@ -0,0 +1,82 @@ +# RECIPE — template-031 (9:16 · 736×1312 @ 30fps) — Glyph rise + hero + +RESOLUTION: px below are authored at the source template's own canvas, 480×864; the compiled module +scales them per run (`pipeline/recipes/template-lib.ts`, `canvasFor`): portrait SCALE = W/480 for sizes +and horizontal offsets, H/864 for vertical anchors; landscape SCALE = (H/864)×1.6. `em` values and ms +timings never scale; the manifest carries the run's real W/H. The reference 9:16 canvas 736×1312 is +SCALE 1.533. + +## 1. IDENTITY + +All-caps wide-tracked Space Grotesk captions centred low in the frame; each glyph fades and rises +0.42em into place on a 22ms stagger from its word and holds until the cue's cut. The beat's strongest +word leaves the caption and lands at the top as a white Bodoni Moda hero under a rule that draws in +from the left, drifting up 3.5% over the beat. + +## 2. SKELETON + +``` + + +``` + +## 3. PER-BEAT ASSEMBLY + +``` +
+
+
{HERO}
+
+
T… …
+
+
+``` +- Caption font 28px; advance 0.71em (caps + tracking) → maxChars = floor(432 / (0.71 × 28)) = 21 per + line, 2 lines a page (page cap 2×maxChars − 2 chars, 8 units). Overflow → demote ×0.92 per row. +- Hero rungs by length: ≤4 chars 140px / −0.02em / top 2 / rule 146 · ≤6 chars 100px / −0.015em / top 12 / + rule 118 · longer 52px / 0.03em / top 25 / rule 86; and never wider than 432px (advance 0.66em). +- Hero window: starts 30ms before the word (clamped to the cue start), runs to the beat's end. + +## 4. WORDS + TIMING + +Glyph spans: word `delayMs` + 22ms × glyph index, VERBATIM base. Cue delay/duration per beat as in +template-030. The hero's `.hx` fires at the word's own `delayMs`. + +## 5. EMPHASIS + +Hero = `accentIndex` of beats with ≥ 3 units; it is REMOVED from the caption and drops trailing +sentence punctuation. Beats of 1-2 units keep every word in the caption and draw no hero. + +## BOUNDED VARIETY + +Hero rung choice follows word length only. + +## 6. VERIFY LOOP + +``` +{repo}/.veed-engine/veed-engine-cli {repo}/runs//final --verify +``` +bounds on a `b{N}…` id → the runner demotes that beat one row (font × 0.92, capacities recomputed) and +regenerates; ≤ 2 cycles → `--record`. Manifest: `{"render":{"width":W,"height":H,"fps":FPS,"duration":DUR}}`. +Ids sit on the element that wraps the text (`.cl` lines, `.hw`/`.sw` titles); demotion is keyed per beat. + +## 7. DO NOT + +No fonts, colours or keyframes beyond this sheet; no invented timing; no reading the frames; no depth +compositing (the source's behind-the-presenter layers are drawn on top here); never hand-edit the output. diff --git a/refs/html/template-031/recipe.ts b/refs/html/template-031/recipe.ts new file mode 100644 index 0000000..2255f47 --- /dev/null +++ b/refs/html/template-031/recipe.ts @@ -0,0 +1,113 @@ +/** + * template-031: all-caps wide-tracked Space Grotesk captions, centred low in the frame; each glyph + * fades and rises into place on a 22ms stagger from its word, then holds until the cue's cut. The + * beat's strongest word leaves the caption and lands as a Bodoni hero title at the top of the frame + * under a rule that draws in; the source composited it behind the presenter, here it sits on top. + */ +import { accentIndex } from '../../../pipeline/recipes/lib.ts'; +import { + type Unit, + DEMOTE_STEP, + canvasFor, + cueDiv, + docShell, + eachBeat, + glyphSpans, + pageGateStyle, + escapeHtml, + paginate, + templateRecipe, + unitDelay, + unitText, + wrapByChars, +} from '../../../pipeline/recipes/template-lib.ts'; + +const FONT = 28; +const AVG_EM = 0.71; // Space Grotesk 700 caps + 0.05em tracking +const MARGIN = 24; +const BOTTOM = 150; +const GLYPH_STEP = 22; +const MAX_LINES = 2; +const HERO_EM = 0.66; // Bodoni Moda 700 caps +const HERO_MIN_UNITS = 3; // a two-word beat keeps its words in the caption +// size rung by length: 4 chars fill the frame at 140px, 6 at 100px, longer words drop to the fine rung +const HERO_RUNGS = [ + { maxChars: 4, font: 140, ls: '-0.02em', top: 2, rule: 146 }, + { maxChars: 6, font: 100, ls: '-0.015em', top: 12, rule: 118 }, + { maxChars: 99, font: 52, ls: '0.03em', top: 25, rule: 86 }, +]; + +export default templateRecipe('template-031', (meta, timings, opts) => { + const c = canvasFor(meta); + const measure = c.refWidth - 2 * MARGIN; + + const cues = eachBeat(meta, timings, opts, 'uppercase', (b) => { + const heroIdx = b.units.length >= HERO_MIN_UNITS ? accentIndex(b.units) : -1; + const hero = heroIdx >= 0 ? b.units[heroIdx] : null; + const capUnits = hero ? b.units.filter((u) => u !== hero) : b.units; + let heroHtml = ''; + if (hero) { + const text = unitText(hero).replace(/[.,!?;:]+$/, ''); // a title carries no sentence punctuation + const rung = HERO_RUNGS.find((r) => text.length <= r.maxChars)!; + const font = Math.min(rung.font, measure / (HERO_EM * text.length)) * Math.pow(DEMOTE_STEP, b.rows); + const start = Math.max(b.cueDelayMs, unitDelay(hero) - 30); + const dur = Math.max(80, b.cueEndMs - start); + heroHtml = `
+
${escapeHtml(text)}
\n`; + } + let rows = b.rows; + let f = 1; + let maxChars = 0; + let pages: Unit[][] = []; + let pageLines: Unit[][][] = []; + for (; rows <= b.rows + 4; rows++) { + f = Math.pow(DEMOTE_STEP, rows); + maxChars = Math.floor(measure / (AVG_EM * FONT * f)); + pages = paginate(capUnits, maxChars * MAX_LINES - 2, 8); + pageLines = pages.map((p) => wrapByChars(p, maxChars)); + if (pageLines.every((ls) => ls.length <= MAX_LINES)) break; + } + const fs = Math.round(FONT * f * c.s); + + const pageDivs = pages.map((page, pi) => { + const pageId = `b${b.n}p${pi + 1}`; + const firstDelay = unitDelay(page[0]); + const nextStart = pi + 1 < pages.length ? unitDelay(pages[pi + 1][0]) : null; + const lines = pageLines[pi].map((line, li) => { + const units = line.map((u) => `${u.spans.map((s) => glyphSpans(s.text, s.delayMs, GLYPH_STEP, 'g')).join('')}`); + return `
${units.join(' ')}
`; + }).join('\n '); + return `
\n ${lines}\n
`; + }); + return cueDiv(b.n, b.cueDelayMs, b.winMs, heroHtml + pageDivs.join('\n')); + }); + + const css = ` + .cap { left:${c.px(MARGIN)}px; right:${c.px(MARGIN)}px; bottom:${c.py(BOTTOM)}px; text-align:center; + font-family:'Space Grotesk'; font-weight:700; letter-spacing:0.05em; color:#F5EFE3; line-height:1.25; + text-shadow:0 ${c.px(1)}px 0 rgba(11,14,24,0.62), 0 ${c.px(3)}px ${c.px(14)}px rgba(11,14,24,0.52); } + .cl { display:block; } + .u { display:inline-block; white-space:pre; } + .sp { display:inline-block; white-space:pre; } + .g { display:inline-block; opacity:0; animation:capRise 150ms cubic-bezier(0.16,0.84,0.32,1) both; } + @keyframes capRise { 0%{opacity:0; transform:translateY(0.42em)} 100%{opacity:1; transform:none} } + .hw { position:absolute; left:${c.px(MARGIN)}px; right:${c.px(MARGIN)}px; z-index:3; text-align:center; white-space:nowrap; line-height:1.2; + font-family:'Bodoni Moda'; font-weight:700; color:#FFFFFF; + text-shadow:0 ${c.px(2)}px 0 rgba(11,14,24,0.26), 0 ${c.px(12)}px ${c.px(30)}px rgba(11,14,24,0.30); + opacity:0; animation-name:cueWin, heroDrift; animation-timing-function:linear, linear; animation-fill-mode:forwards, forwards; } + .hx { display:inline-block; opacity:0; animation:heroIn 300ms cubic-bezier(0.16,0.84,0.32,1) both; } + .rg { position:absolute; left:0; width:${c.W}px; height:${c.px(3)}px; z-index:2; opacity:0; animation:cueWin linear forwards; } + .rb { display:block; width:${c.W}px; height:${c.px(3)}px; background:rgba(255,255,255,0.92); box-shadow:0 ${c.px(1)}px ${c.px(3)}px rgba(11,14,24,0.34); + transform-origin:0 50%; animation:ruleIn 460ms cubic-bezier(0.16,0.84,0.32,1) both; } + @keyframes heroIn { 0%{opacity:0; transform:translateY(${c.px(18)}px) scale(0.955)} 100%{opacity:1; transform:none} } + @keyframes heroDrift { 0%{transform:scale(1)} 100%{transform:scale(1.035)} } + @keyframes ruleIn { 0%{transform:scaleX(0)} 100%{transform:scaleX(1)} }`; + + return docShell({ + canvas: c, + videoPath: meta.videoPath, + fonts: ['Space+Grotesk:wght@700', 'Bodoni+Moda:wght@700'], + css, + body: cues.join('\n'), + }); +}); diff --git a/refs/html/template-031/template.wv b/refs/html/template-031/template.wv new file mode 100644 index 0000000..50ab29d --- /dev/null +++ b/refs/html/template-031/template.wv @@ -0,0 +1,93 @@ + + + + + + +
+
+
SPENDS
+
+
YOUR TEAM HOURS
+
+
+
+
+
SLIDES
+
+
TURNING DATA INTO
+
+
+
+
+
CLICK
+
+
WE JUST MADE
+
THAT ONE NEW
+
+
+
+
+
RELEASE
+
+
IN THIS
+
+
+
+
+
DASHBOARDS
+
+
REPORTS BECOME SHARE
+
READY ON BRAND
+
+
+
+
+
AUTOMATICALLY. EARLY
+
+
+
+
+
ACCESS
+
+
IS OPEN THIS WEEK.
+
+
+
+
+
MESSIEST
+
+
BRING YOUR
+
REPORT AND WATCH
+
+
diff --git a/refs/html/template-031/transcript.json b/refs/html/template-031/transcript.json new file mode 100644 index 0000000..99fcfca --- /dev/null +++ b/refs/html/template-031/transcript.json @@ -0,0 +1,343 @@ +{ + "text": "YOUR TEAM SPENDS HOURS TURNING DATA INTO SLIDES. WE JUST MADE THAT ONE CLICK NEW IN THIS RELEASE, REPORTS BECOME SHARE READY DASHBOARDS ON BRAND AUTOMATICALLY. EARLY ACCESS IS OPEN THIS WEEK. BRING YOUR MESSIEST REPORT AND WATCH", + "chunks": [ + { + "text": "YOUR TEAM SPENDS HOURS", + "timestamp": [ + 0.16, + 1.68 + ], + "words": [ + { + "text": "YOUR", + "timestamp": [ + 0.16, + 0.36 + ] + }, + { + "text": "TEAM", + "timestamp": [ + 0.36, + 0.68 + ] + }, + { + "text": "SPENDS", + "timestamp": [ + 0.68, + 1.27 + ] + }, + { + "text": "HOURS", + "timestamp": [ + 1.27, + 1.68 + ] + } + ] + }, + { + "text": "TURNING DATA INTO SLIDES.", + "timestamp": [ + 1.68, + 3.58 + ], + "words": [ + { + "text": "TURNING", + "timestamp": [ + 1.68, + 2.2 + ] + }, + { + "text": "DATA", + "timestamp": [ + 2.2, + 2.56 + ] + }, + { + "text": "INTO", + "timestamp": [ + 2.56, + 2.92 + ] + }, + { + "text": "SLIDES.", + "timestamp": [ + 2.92, + 3.58 + ] + } + ] + }, + { + "text": "WE JUST MADE THAT ONE CLICK NEW", + "timestamp": [ + 3.6, + 6.08 + ], + "words": [ + { + "text": "WE", + "timestamp": [ + 3.6, + 3.88 + ] + }, + { + "text": "JUST", + "timestamp": [ + 3.88, + 4.08 + ] + }, + { + "text": "MADE", + "timestamp": [ + 4.08, + 4.32 + ] + }, + { + "text": "THAT", + "timestamp": [ + 4.32, + 4.6 + ] + }, + { + "text": "ONE", + "timestamp": [ + 4.6, + 4.99 + ] + }, + { + "text": "CLICK", + "timestamp": [ + 4.99, + 5.79 + ] + }, + { + "text": "NEW", + "timestamp": [ + 5.79, + 6.08 + ] + } + ] + }, + { + "text": "IN THIS RELEASE,", + "timestamp": [ + 6.08, + 7.26 + ], + "words": [ + { + "text": "IN", + "timestamp": [ + 6.08, + 6.28 + ] + }, + { + "text": "THIS", + "timestamp": [ + 6.28, + 6.52 + ] + }, + { + "text": "RELEASE,", + "timestamp": [ + 6.52, + 7.26 + ] + } + ] + }, + { + "text": "REPORTS BECOME SHARE READY DASHBOARDS ON BRAND", + "timestamp": [ + 7.28, + 10.67 + ], + "words": [ + { + "text": "REPORTS", + "timestamp": [ + 7.28, + 7.92 + ] + }, + { + "text": "BECOME", + "timestamp": [ + 7.92, + 8.4 + ] + }, + { + "text": "SHARE", + "timestamp": [ + 8.4, + 8.72 + ] + }, + { + "text": "READY", + "timestamp": [ + 8.72, + 9.23 + ] + }, + { + "text": "DASHBOARDS", + "timestamp": [ + 9.23, + 9.95 + ] + }, + { + "text": "ON", + "timestamp": [ + 9.95, + 10.07 + ] + }, + { + "text": "BRAND", + "timestamp": [ + 10.07, + 10.67 + ] + } + ] + }, + { + "text": "AUTOMATICALLY. EARLY", + "timestamp": [ + 10.8, + 12.32 + ], + "words": [ + { + "text": "AUTOMATICALLY.", + "timestamp": [ + 10.8, + 11.91 + ] + }, + { + "text": "EARLY", + "timestamp": [ + 11.91, + 12.32 + ] + } + ] + }, + { + "text": "ACCESS IS OPEN THIS WEEK.", + "timestamp": [ + 12.32, + 14.22 + ], + "words": [ + { + "text": "ACCESS", + "timestamp": [ + 12.32, + 12.8 + ] + }, + { + "text": "IS", + "timestamp": [ + 12.8, + 13.12 + ] + }, + { + "text": "OPEN", + "timestamp": [ + 13.12, + 13.52 + ] + }, + { + "text": "THIS", + "timestamp": [ + 13.52, + 13.84 + ] + }, + { + "text": "WEEK.", + "timestamp": [ + 13.84, + 14.22 + ] + } + ] + }, + { + "text": "BRING YOUR MESSIEST REPORT AND WATCH", + "timestamp": [ + 14.24, + 16.95 + ], + "words": [ + { + "text": "BRING", + "timestamp": [ + 14.24, + 14.56 + ] + }, + { + "text": "YOUR", + "timestamp": [ + 14.56, + 14.8 + ] + }, + { + "text": "MESSIEST", + "timestamp": [ + 14.8, + 15.44 + ] + }, + { + "text": "REPORT", + "timestamp": [ + 15.44, + 15.919 + ] + }, + { + "text": "AND", + "timestamp": [ + 15.919, + 16.35 + ] + }, + { + "text": "WATCH", + "timestamp": [ + 16.35, + 16.95 + ] + } + ] + } + ] +} diff --git a/refs/html/template-031/word-timings.json b/refs/html/template-031/word-timings.json new file mode 100644 index 0000000..507e688 --- /dev/null +++ b/refs/html/template-031/word-timings.json @@ -0,0 +1,228 @@ +{ + "beats": [ + { + "i": 1, + "startSec": 0.16, + "endSec": 1.68, + "cueDelayMs": 160, + "cueDurMs": 1520, + "words": [ + { + "w": "YOUR", + "delayMs": 160 + }, + { + "w": "TEAM", + "delayMs": 360 + }, + { + "w": "SPENDS", + "delayMs": 680 + }, + { + "w": "HOURS", + "delayMs": 1270 + } + ] + }, + { + "i": 2, + "startSec": 1.68, + "endSec": 3.58, + "cueDelayMs": 1680, + "cueDurMs": 1900, + "words": [ + { + "w": "TURNING", + "delayMs": 1680 + }, + { + "w": "DATA", + "delayMs": 2200 + }, + { + "w": "INTO", + "delayMs": 2560 + }, + { + "w": "SLIDES.", + "delayMs": 2920 + } + ] + }, + { + "i": 3, + "startSec": 3.6, + "endSec": 6.08, + "cueDelayMs": 3600, + "cueDurMs": 2480, + "words": [ + { + "w": "WE", + "delayMs": 3600 + }, + { + "w": "JUST", + "delayMs": 3880 + }, + { + "w": "MADE", + "delayMs": 4080 + }, + { + "w": "THAT", + "delayMs": 4320 + }, + { + "w": "ONE", + "delayMs": 4600 + }, + { + "w": "CLICK", + "delayMs": 4990 + }, + { + "w": "NEW", + "delayMs": 5790 + } + ] + }, + { + "i": 4, + "startSec": 6.08, + "endSec": 7.26, + "cueDelayMs": 6080, + "cueDurMs": 1180, + "words": [ + { + "w": "IN", + "delayMs": 6080 + }, + { + "w": "THIS", + "delayMs": 6280 + }, + { + "w": "RELEASE,", + "delayMs": 6520 + } + ] + }, + { + "i": 5, + "startSec": 7.28, + "endSec": 10.67, + "cueDelayMs": 7280, + "cueDurMs": 3390, + "words": [ + { + "w": "REPORTS", + "delayMs": 7280 + }, + { + "w": "BECOME", + "delayMs": 7920 + }, + { + "w": "SHARE", + "delayMs": 8400 + }, + { + "w": "READY", + "delayMs": 8720 + }, + { + "w": "DASHBOARDS", + "delayMs": 9230 + }, + { + "w": "ON", + "delayMs": 9950 + }, + { + "w": "BRAND", + "delayMs": 10070 + } + ] + }, + { + "i": 6, + "startSec": 10.8, + "endSec": 12.32, + "cueDelayMs": 10800, + "cueDurMs": 1520, + "words": [ + { + "w": "AUTOMATICALLY.", + "delayMs": 10800 + }, + { + "w": "EARLY", + "delayMs": 11910 + } + ] + }, + { + "i": 7, + "startSec": 12.32, + "endSec": 14.22, + "cueDelayMs": 12320, + "cueDurMs": 1900, + "words": [ + { + "w": "ACCESS", + "delayMs": 12320 + }, + { + "w": "IS", + "delayMs": 12800 + }, + { + "w": "OPEN", + "delayMs": 13120 + }, + { + "w": "THIS", + "delayMs": 13520 + }, + { + "w": "WEEK.", + "delayMs": 13840 + } + ] + }, + { + "i": 8, + "startSec": 14.24, + "endSec": 16.95, + "cueDelayMs": 14240, + "cueDurMs": 2710, + "words": [ + { + "w": "BRING", + "delayMs": 14240 + }, + { + "w": "YOUR", + "delayMs": 14560 + }, + { + "w": "MESSIEST", + "delayMs": 14800 + }, + { + "w": "REPORT", + "delayMs": 15440 + }, + { + "w": "AND", + "delayMs": 15919 + }, + { + "w": "WATCH", + "delayMs": 16350 + } + ] + } + ] +} diff --git a/refs/html/template-032/recipe.md b/refs/html/template-032/recipe.md new file mode 100644 index 0000000..d353915 --- /dev/null +++ b/refs/html/template-032/recipe.md @@ -0,0 +1,76 @@ +# RECIPE — template-032 (9:16 · 736×1312 @ 30fps) — Typewriter + rotated title + +RESOLUTION: px below are authored at the source template's own canvas, 480×864; the compiled module +scales them per run (`pipeline/recipes/template-lib.ts`, `canvasFor`): portrait SCALE = W/480 for sizes +and horizontal offsets, H/864 for vertical anchors; landscape SCALE = (H/864)×1.6. `em` values and ms +timings never scale; the manifest carries the run's real W/H. The reference 9:16 canvas 736×1312 is +SCALE 1.533. + +## 1. IDENTITY + +Right-aligned Archivo SemiBold captions in the lower third with a per-glyph typewriter fade on a 30ms +stagger, warm cream ink over a soft shadow. The beat's strongest word leaves the caption and runs down +a frame edge as a giant acid-green Cinzel Black title rotated 90°, glyphs sliding in on a 48ms stagger; +the edge alternates left / right every two beats. + +## 2. SKELETON + +``` + + +``` + +## 3. PER-BEAT ASSEMBLY + +``` +
+
E…
+
…glyph units…
+
+``` +- Caption font 34px; advance 0.56em → maxChars = floor(422 / (0.56 × 34)) = 22 per line, 2 lines a page + (page cap 2×maxChars − 2, 9 units). Overflow → demote ×0.92 per row. +- Title: 72px, uppercase, advance 0.75em; runs downward from top 64px; shrinks so chars × 0.75em ≤ 760px. + `left` is the title's right edge (rotation about its top-left): 94px for beats 1-2, 476px for 3-4, + alternating every two beats. +- Title window: starts 70ms before the word (clamped to the cue start), runs to the beat's end. + +## 4. WORDS + TIMING + +Glyph spans: word `delayMs` + 30ms × glyph index (caption) / + 48ms × glyph index (title), VERBATIM +base. Caption casing is the transcript's own. + +## 5. EMPHASIS + +Title = `accentIndex` of beats with ≥ 3 units; REMOVED from the caption; trailing sentence punctuation +dropped. Beats of 1-2 units draw no title. + +## BOUNDED VARIETY + +Side alternation is the only variety axis. + +## 6. VERIFY LOOP + +``` +{repo}/.veed-engine/veed-engine-cli {repo}/runs//final --verify +``` +bounds on a `b{N}…` id → the runner demotes that beat one row (font × 0.92, capacities recomputed) and +regenerates; ≤ 2 cycles → `--record`. Manifest: `{"render":{"width":W,"height":H,"fps":FPS,"duration":DUR}}`. +Ids sit on the element that wraps the text (`.cl` lines, `.hw`/`.sw` titles); demotion is keyed per beat. + +## 7. DO NOT + +No fonts, colours or keyframes beyond this sheet; no invented timing; no reading the frames; no depth +compositing (the source's behind-the-presenter layers are drawn on top here); never hand-edit the output. diff --git a/refs/html/template-032/recipe.ts b/refs/html/template-032/recipe.ts new file mode 100644 index 0000000..fec71e9 --- /dev/null +++ b/refs/html/template-032/recipe.ts @@ -0,0 +1,107 @@ +/** + * template-032: right-aligned Archivo captions in the lower third with a fast per-glyph typewriter + * fade (30ms stagger). The beat's strongest word leaves the caption and runs down a frame edge as a + * giant green Cinzel title rotated 90 degrees, glyphs sliding in; the source composited it behind + * the presenter, here it sits on top. Sides alternate every two beats. + */ +import { accentIndex } from '../../../pipeline/recipes/lib.ts'; +import { + type Unit, + DEMOTE_STEP, + REF_H, + canvasFor, + cueDiv, + docShell, + eachBeat, + glyphSpans, + pageGateStyle, + escapeHtml, + paginate, + templateRecipe, + unitDelay, + unitText, + wrapByChars, +} from '../../../pipeline/recipes/template-lib.ts'; + +const FONT = 34; +const AVG_EM = 0.56; // Archivo 600 mixed case +const MARGIN = 29; +const BOTTOM = 190; +const GLYPH_STEP = 30; +const MAX_LINES = 2; +const TITLE_FONT = 72; +const TITLE_EM = 0.8; // Cinzel 900 caps +const TITLE_TOP = 64; +const TITLE_LEFT = [94, 476]; // rotated 90deg about its top-left, so `left` is the title's right edge +const TITLE_MIN_UNITS = 3; +const TITLE_STEP = 48; + +export default templateRecipe('template-032', (meta, timings, opts) => { + const c = canvasFor(meta); + const measure = c.refWidth - 2 * MARGIN; + + const cues = eachBeat(meta, timings, opts, 'none', (b) => { + const titleIdx = b.units.length >= TITLE_MIN_UNITS ? accentIndex(b.units) : -1; + const title = titleIdx >= 0 ? b.units[titleIdx] : null; + const capUnits = title ? b.units.filter((u) => u !== title) : b.units; + let titleHtml = ''; + if (title) { + const text = unitText(title).toUpperCase().replace(/[.,!?;:]+$/, ''); // a title carries no sentence punctuation + const font = Math.min(TITLE_FONT, (REF_H - TITLE_TOP - 40) / (TITLE_EM * text.length)) * Math.pow(DEMOTE_STEP, b.rows); + const left = TITLE_LEFT[Math.floor((b.n - 1) / 2) % 2]; + const start = Math.max(b.cueDelayMs, unitDelay(title) - 70); + const dur = Math.max(80, b.cueEndMs - start); + const glyphs = [...text].map((ch, k) => `${escapeHtml(ch)}`).join(''); + titleHtml = `
${glyphs}
\n`; + } + let rows = b.rows; + let f = 1; + let maxChars = 0; + let pages: Unit[][] = []; + let pageLines: Unit[][][] = []; + for (; rows <= b.rows + 4; rows++) { + f = Math.pow(DEMOTE_STEP, rows); + maxChars = Math.floor(measure / (AVG_EM * FONT * f)); + pages = paginate(capUnits, maxChars * MAX_LINES - 2, 9); + pageLines = pages.map((p) => wrapByChars(p, maxChars)); + if (pageLines.every((ls) => ls.length <= MAX_LINES)) break; + } + const fs = Math.round(FONT * f * c.s); + + const pageDivs = pages.map((page, pi) => { + const pageId = `b${b.n}p${pi + 1}`; + const firstDelay = unitDelay(page[0]); + const nextStart = pi + 1 < pages.length ? unitDelay(pages[pi + 1][0]) : null; + const lines = pageLines[pi].map((line, li) => { + const units = line.map((u) => `${u.spans.map((s) => glyphSpans(s.text, s.delayMs, GLYPH_STEP, 'g')).join('')}`); + return `
${units.join(' ')}
`; + }).join('\n '); + return `
\n ${lines}\n
`; + }); + return cueDiv(b.n, b.cueDelayMs, b.winMs, titleHtml + pageDivs.join('\n')); + }); + + const css = ` + .cap { left:${c.px(MARGIN)}px; right:${c.px(MARGIN)}px; bottom:${c.py(BOTTOM)}px; text-align:right; + font-family:'Archivo'; font-weight:600; letter-spacing:0.0081em; color:#F4EDDF; line-height:1.28; + text-shadow:0 ${c.px(2)}px ${c.px(9)}px rgba(2,2,1,0.38), 0 ${c.px(1)}px ${c.px(2)}px rgba(2,2,1,0.3); } + .cl { display:block; } + .u { display:inline-block; white-space:pre; } + .sp { display:inline-block; white-space:pre; } + .g { display:inline-block; opacity:0; animation:capG 120ms cubic-bezier(.2,.7,.3,1) forwards; } + @keyframes capG { from{opacity:0} to{opacity:1} } + .swg { position:absolute; inset:0; z-index:3; opacity:0; animation:cueWin linear forwards; } + .sw { position:absolute; transform-origin:0 0; transform:rotate(90deg); white-space:nowrap; + font-family:'Cinzel'; font-weight:900; letter-spacing:0.01em; color:#96FF1A; + text-shadow:0 ${c.px(4)}px ${c.px(18)}px rgba(2,2,1,0.38), 0 ${c.px(2)}px ${c.px(4)}px rgba(2,2,1,0.3); } + .sc { display:inline-block; opacity:0; animation:scIn 380ms cubic-bezier(.16,.84,.28,1) both; } + @keyframes scIn { 0%{opacity:0; transform:translateX(-0.16em)} 100%{opacity:1; transform:none} }`; + + return docShell({ + canvas: c, + videoPath: meta.videoPath, + fonts: ['Archivo:wght@600', 'Cinzel:wght@900'], + css, + body: cues.join('\n'), + }); +}); diff --git a/refs/html/template-032/template.wv b/refs/html/template-032/template.wv new file mode 100644 index 0000000..1a0c244 --- /dev/null +++ b/refs/html/template-032/template.wv @@ -0,0 +1,82 @@ + + + + + + +
+
SUPPOSEDLY
+
+
Einstein called
+
compound interest.
+
+
+
+
WORLD
+
+
The eighth
+
wonder of the
+
+
+
+
SIMPLE
+
+
The idea is
+
+
+
+
EARNS
+
+
Your money
+
+
+
+
EARNINGS
+
+
Then those start
+
earning too.
+
+
+
+
10%
+
+
At a year, your
+
money doubles
+
+
+
+
ROUGHLY
+
+
every seven years.
+
+
+
+
+
Start early.
+
+
diff --git a/refs/html/template-032/transcript.json b/refs/html/template-032/transcript.json new file mode 100644 index 0000000..abc83e4 --- /dev/null +++ b/refs/html/template-032/transcript.json @@ -0,0 +1,336 @@ +{ + "text": "Einstein supposedly called compound interest. The eighth wonder of the world. The idea is simple. Your money earns. Then those earnings start earning too. At 10% a year, your money doubles roughly every seven years. Start early.", + "chunks": [ + { + "text": "Einstein supposedly called compound interest.", + "timestamp": [ + 0.23, + 2.32 + ], + "words": [ + { + "text": "Einstein", + "timestamp": [ + 0.23, + 0.64 + ] + }, + { + "text": "supposedly", + "timestamp": [ + 0.64, + 1.24 + ] + }, + { + "text": "called", + "timestamp": [ + 1.24, + 1.48 + ] + }, + { + "text": "compound", + "timestamp": [ + 1.48, + 2 + ] + }, + { + "text": "interest.", + "timestamp": [ + 2, + 2.32 + ] + } + ] + }, + { + "text": "The eighth wonder of the world.", + "timestamp": [ + 2.32, + 4.32 + ], + "words": [ + { + "text": "The", + "timestamp": [ + 2.32, + 2.64 + ] + }, + { + "text": "eighth", + "timestamp": [ + 2.64, + 3.03 + ] + }, + { + "text": "wonder", + "timestamp": [ + 3.03, + 3.28 + ] + }, + { + "text": "of", + "timestamp": [ + 3.28, + 3.48 + ] + }, + { + "text": "the", + "timestamp": [ + 3.48, + 3.64 + ] + }, + { + "text": "world.", + "timestamp": [ + 3.64, + 4.32 + ] + } + ] + }, + { + "text": "The idea is simple.", + "timestamp": [ + 4.32, + 5.68 + ], + "words": [ + { + "text": "The", + "timestamp": [ + 4.32, + 4.6 + ] + }, + { + "text": "idea", + "timestamp": [ + 4.6, + 5 + ] + }, + { + "text": "is", + "timestamp": [ + 5, + 5.35 + ] + }, + { + "text": "simple.", + "timestamp": [ + 5.35, + 5.68 + ] + } + ] + }, + { + "text": "Your money earns.", + "timestamp": [ + 5.68, + 6.76 + ], + "words": [ + { + "text": "Your", + "timestamp": [ + 5.68, + 5.92 + ] + }, + { + "text": "money", + "timestamp": [ + 5.92, + 6.24 + ] + }, + { + "text": "earns.", + "timestamp": [ + 6.24, + 6.76 + ] + } + ] + }, + { + "text": "Then those earnings start earning too.", + "timestamp": [ + 6.76, + 8.88 + ], + "words": [ + { + "text": "Then", + "timestamp": [ + 6.76, + 7.04 + ] + }, + { + "text": "those", + "timestamp": [ + 7.04, + 7.24 + ] + }, + { + "text": "earnings", + "timestamp": [ + 7.24, + 7.64 + ] + }, + { + "text": "start", + "timestamp": [ + 7.64, + 7.95 + ] + }, + { + "text": "earning", + "timestamp": [ + 7.95, + 8.28 + ] + }, + { + "text": "too.", + "timestamp": [ + 8.28, + 8.88 + ] + } + ] + }, + { + "text": "At 10% a year, your money doubles", + "timestamp": [ + 8.88, + 11.2 + ], + "words": [ + { + "text": "At", + "timestamp": [ + 8.88, + 9.2 + ] + }, + { + "text": "10%", + "timestamp": [ + 9.2, + 9.72 + ] + }, + { + "text": "a", + "timestamp": [ + 9.72, + 9.96 + ] + }, + { + "text": "year,", + "timestamp": [ + 9.96, + 10.2 + ] + }, + { + "text": "your", + "timestamp": [ + 10.2, + 10.44 + ] + }, + { + "text": "money", + "timestamp": [ + 10.44, + 10.75 + ] + }, + { + "text": "doubles", + "timestamp": [ + 10.75, + 11.2 + ] + } + ] + }, + { + "text": "roughly every seven years.", + "timestamp": [ + 11.2, + 12.88 + ], + "words": [ + { + "text": "roughly", + "timestamp": [ + 11.2, + 11.6 + ] + }, + { + "text": "every", + "timestamp": [ + 11.6, + 11.88 + ] + }, + { + "text": "seven", + "timestamp": [ + 11.88, + 12.16 + ] + }, + { + "text": "years.", + "timestamp": [ + 12.16, + 12.88 + ] + } + ] + }, + { + "text": "Start early.", + "timestamp": [ + 12.88, + 14 + ], + "words": [ + { + "text": "Start", + "timestamp": [ + 12.88, + 13.35 + ] + }, + { + "text": "early.", + "timestamp": [ + 13.35, + 14 + ] + } + ] + } + ] +} diff --git a/refs/html/template-032/word-timings.json b/refs/html/template-032/word-timings.json new file mode 100644 index 0000000..d211220 --- /dev/null +++ b/refs/html/template-032/word-timings.json @@ -0,0 +1,224 @@ +{ + "beats": [ + { + "i": 1, + "startSec": 0.23, + "endSec": 2.32, + "cueDelayMs": 230, + "cueDurMs": 2090, + "words": [ + { + "w": "Einstein", + "delayMs": 230 + }, + { + "w": "supposedly", + "delayMs": 640 + }, + { + "w": "called", + "delayMs": 1240 + }, + { + "w": "compound", + "delayMs": 1480 + }, + { + "w": "interest.", + "delayMs": 2000 + } + ] + }, + { + "i": 2, + "startSec": 2.32, + "endSec": 4.32, + "cueDelayMs": 2320, + "cueDurMs": 2000, + "words": [ + { + "w": "The", + "delayMs": 2320 + }, + { + "w": "eighth", + "delayMs": 2640 + }, + { + "w": "wonder", + "delayMs": 3030 + }, + { + "w": "of", + "delayMs": 3280 + }, + { + "w": "the", + "delayMs": 3480 + }, + { + "w": "world.", + "delayMs": 3640 + } + ] + }, + { + "i": 3, + "startSec": 4.32, + "endSec": 5.68, + "cueDelayMs": 4320, + "cueDurMs": 1360, + "words": [ + { + "w": "The", + "delayMs": 4320 + }, + { + "w": "idea", + "delayMs": 4600 + }, + { + "w": "is", + "delayMs": 5000 + }, + { + "w": "simple.", + "delayMs": 5350 + } + ] + }, + { + "i": 4, + "startSec": 5.68, + "endSec": 6.76, + "cueDelayMs": 5680, + "cueDurMs": 1080, + "words": [ + { + "w": "Your", + "delayMs": 5680 + }, + { + "w": "money", + "delayMs": 5920 + }, + { + "w": "earns.", + "delayMs": 6240 + } + ] + }, + { + "i": 5, + "startSec": 6.76, + "endSec": 8.88, + "cueDelayMs": 6760, + "cueDurMs": 2120, + "words": [ + { + "w": "Then", + "delayMs": 6760 + }, + { + "w": "those", + "delayMs": 7040 + }, + { + "w": "earnings", + "delayMs": 7240 + }, + { + "w": "start", + "delayMs": 7640 + }, + { + "w": "earning", + "delayMs": 7950 + }, + { + "w": "too.", + "delayMs": 8280 + } + ] + }, + { + "i": 6, + "startSec": 8.88, + "endSec": 11.2, + "cueDelayMs": 8880, + "cueDurMs": 2320, + "words": [ + { + "w": "At", + "delayMs": 8880 + }, + { + "w": "10%", + "delayMs": 9200 + }, + { + "w": "a", + "delayMs": 9720 + }, + { + "w": "year,", + "delayMs": 9960 + }, + { + "w": "your", + "delayMs": 10200 + }, + { + "w": "money", + "delayMs": 10440 + }, + { + "w": "doubles", + "delayMs": 10750 + } + ] + }, + { + "i": 7, + "startSec": 11.2, + "endSec": 12.88, + "cueDelayMs": 11200, + "cueDurMs": 1680, + "words": [ + { + "w": "roughly", + "delayMs": 11200 + }, + { + "w": "every", + "delayMs": 11600 + }, + { + "w": "seven", + "delayMs": 11880 + }, + { + "w": "years.", + "delayMs": 12160 + } + ] + }, + { + "i": 8, + "startSec": 12.88, + "endSec": 14, + "cueDelayMs": 12880, + "cueDurMs": 1120, + "words": [ + { + "w": "Start", + "delayMs": 12880 + }, + { + "w": "early.", + "delayMs": 13350 + } + ] + } + ] +} diff --git a/refs/html/template-033/recipe.md b/refs/html/template-033/recipe.md new file mode 100644 index 0000000..86fd3ea --- /dev/null +++ b/refs/html/template-033/recipe.md @@ -0,0 +1,67 @@ +# RECIPE — template-033 (9:16 · 736×1312 @ 30fps) — Ghost ink + +RESOLUTION: px below are authored at the source template's own canvas, 480×864; the compiled module +scales them per run (`pipeline/recipes/template-lib.ts`, `canvasFor`): portrait SCALE = W/480 for sizes +and horizontal offsets, H/864 for vertical anchors; landscape SCALE = (H/864)×1.6. `em` values and ms +timings never scale; the manifest carries the run's real W/H. The reference 9:16 canvas 736×1312 is +SCALE 1.533. + +## 1. IDENTITY + +The whole beat stands in Newsreader Medium at 42% ink from the cue's start; each word takes on full +ink at the instant it is spoken (260ms ease) and holds. One italic accent per beat. A centred block +hanging from a fixed last baseline in the lower third, warm cream over a soft dark halo. No motion. + +## 2. SKELETON + +``` + + +``` + +## 3. PER-BEAT ASSEMBLY + +``` +
+
+
You……
+
+
+``` +- Font 40px; advance 0.47em → maxChars = floor(412 / (0.47 × 40)) = 21 per line; a page takes up to + 2×maxChars chars / 10 units and may wrap to 3 lines (the block grows upward). Overflow → demote ×0.92. +- `lightMs` = min(260, cueEnd − delay), never below 60. + +## 4. WORDS + TIMING + +One span per word at its `delayMs` VERBATIM; the last word of a line carries `.e` (no trailing gap). + +## 5. EMPHASIS + +Accent = `accentIndex` → `.i` (italic only, same size, same ink). + +## BOUNDED VARIETY + +None: one face, one size, one motion. + +## 6. VERIFY LOOP + +``` +{repo}/.veed-engine/veed-engine-cli {repo}/runs//final --verify +``` +bounds on a `b{N}…` id → the runner demotes that beat one row (font × 0.92, capacities recomputed) and +regenerates; ≤ 2 cycles → `--record`. Manifest: `{"render":{"width":W,"height":H,"fps":FPS,"duration":DUR}}`. +Ids sit on the element that wraps the text (`.cl` lines, `.hw`/`.sw` titles); demotion is keyed per beat. + +## 7. DO NOT + +No fonts, colours or keyframes beyond this sheet; no invented timing; no reading the frames; no depth +compositing (the source's behind-the-presenter layers are drawn on top here); never hand-edit the output. diff --git a/refs/html/template-033/recipe.ts b/refs/html/template-033/recipe.ts new file mode 100644 index 0000000..882ed09 --- /dev/null +++ b/refs/html/template-033/recipe.ts @@ -0,0 +1,87 @@ +/** + * template-033 "Ghost ink": the whole beat stands in Newsreader at 42% ink from the cue's start; each + * word takes on full ink at the instant it is spoken and holds. One italic accent per beat, centred + * block hanging from a fixed last baseline. No motion. + */ +import { accentIndex } from '../../../pipeline/recipes/lib.ts'; +import { + type Unit, + DEMOTE_STEP, + canvasFor, + cueDiv, + docShell, + eachBeat, + escapeHtml, + pageGateStyle, + paginate, + templateRecipe, + unitDelay, + wrapByChars, +} from '../../../pipeline/recipes/template-lib.ts'; + +const FONT = 40; +const AVG_EM = 0.47; // Newsreader 500 mixed case +const MARGIN = 34; +const BOTTOM = 116; +const LIGHT_MS = 260; +const MAX_LINES = 3; // the block grows upward, so a third line is safe + +export default templateRecipe('template-033', (meta, timings, opts) => { + const c = canvasFor(meta); + const measure = c.refWidth - 2 * MARGIN; + let wordSeq = 0; + + const cues = eachBeat(meta, timings, opts, 'none', (b) => { + const accent = accentIndex(b.units); + let rows = b.rows; + let f = 1; + let maxChars = 0; + let pages: Unit[][] = []; + let pageLines: Unit[][][] = []; + for (; rows <= b.rows + 4; rows++) { + f = Math.pow(DEMOTE_STEP, rows); + maxChars = Math.floor(measure / (AVG_EM * FONT * f)); + pages = paginate(b.units, maxChars * 2, 10); + pageLines = pages.map((p) => wrapByChars(p, maxChars)); + if (pageLines.every((ls) => ls.length <= MAX_LINES)) break; + } + const fs = Math.round(FONT * f * c.s); + + const pageDivs = pages.map((page, pi) => { + const pageId = `b${b.n}p${pi + 1}`; + const firstDelay = unitDelay(page[0]); + const nextStart = pi + 1 < pages.length ? unitDelay(pages[pi + 1][0]) : null; + const lines = pageLines[pi].map((line, li) => { + const spans = line.map((u, ui) => u.spans.map((s, si) => { + wordSeq++; + const last = ui === line.length - 1 && si === u.spans.length - 1; + const cls = ['w', last ? 'e' : '', b.units.indexOf(u) === accent ? 'i' : ''].filter(Boolean).join(' '); + const dur = Math.max(60, Math.min(LIGHT_MS, b.cueEndMs - s.delayMs)); + return `${escapeHtml(s.text)}`; + }).join('')).join(''); + return `
${spans}
`; + }).join('\n '); + return `
\n ${lines}\n
`; + }); + return cueDiv(b.n, b.cueDelayMs, b.winMs, pageDivs.join('\n')); + }); + + const css = ` + .cap { left:${c.px(MARGIN)}px; right:${c.px(MARGIN)}px; bottom:${c.py(BOTTOM)}px; text-align:center; + font-family:'Newsreader'; font-weight:500; letter-spacing:0.0038em; line-height:1.34; color:#F5EFE4; + text-shadow:0 0.05em 0.42em rgba(26,18,11,0.62); } + .cl { display:block; white-space:nowrap; } + .w { display:inline-block; margin-right:0.26em; opacity:0.42; + animation-name:wLight; animation-timing-function:cubic-bezier(0.22,0.61,0.36,1); animation-fill-mode:forwards; } + .e { margin-right:0; } + .i { font-style:italic; } + @keyframes wLight { 0%{opacity:0.42} 100%{opacity:1} }`; + + return docShell({ + canvas: c, + videoPath: meta.videoPath, + fonts: ['Newsreader:ital,wght@0,500;1,500'], + css, + body: cues.join('\n'), + }); +}); diff --git a/refs/html/template-033/template.wv b/refs/html/template-033/template.wv new file mode 100644 index 0000000..ecd7294 --- /dev/null +++ b/refs/html/template-033/template.wv @@ -0,0 +1,68 @@ + + + + + + +
+
+
Youforgetfacts
+
indays,but
+
+
+
+
+
yourememberstories
+
fordecades.
+
+
+
+
+
Thereisa
+
reason.Astory
+
+
+
+
+
linkseachfact
+
toanemotion.
+
+
+
+
+
Anemotionishowthe
+
brainfilesthings.
+
+
+
+
+
Soifyouwanttobe
+
+
+
+
+
remembered,stop
+
listing.Start
+
telling.
+
+
diff --git a/refs/html/template-033/transcript.json b/refs/html/template-033/transcript.json new file mode 100644 index 0000000..780646c --- /dev/null +++ b/refs/html/template-033/transcript.json @@ -0,0 +1,362 @@ +{ + "text": "You forget facts in days, but you remember stories for decades. There is a reason. A story links each fact to an emotion. An emotion is how the brain files things. So if you want to be remembered, stop listing. Start telling.", + "chunks": [ + { + "text": "You forget facts in days, but", + "timestamp": [ + 0.16, + 1.759 + ], + "words": [ + { + "text": "You", + "timestamp": [ + 0.16, + 0.28 + ] + }, + { + "text": "forget", + "timestamp": [ + 0.28, + 0.56 + ] + }, + { + "text": "facts", + "timestamp": [ + 0.56, + 0.92 + ] + }, + { + "text": "in", + "timestamp": [ + 0.92, + 1.12 + ] + }, + { + "text": "days,", + "timestamp": [ + 1.12, + 1.44 + ] + }, + { + "text": "but", + "timestamp": [ + 1.44, + 1.759 + ] + } + ] + }, + { + "text": "you remember stories for decades.", + "timestamp": [ + 1.76, + 3.599 + ], + "words": [ + { + "text": "you", + "timestamp": [ + 1.76, + 1.96 + ] + }, + { + "text": "remember", + "timestamp": [ + 1.96, + 2.32 + ] + }, + { + "text": "stories", + "timestamp": [ + 2.32, + 2.72 + ] + }, + { + "text": "for", + "timestamp": [ + 2.72, + 2.92 + ] + }, + { + "text": "decades.", + "timestamp": [ + 2.92, + 3.599 + ] + } + ] + }, + { + "text": "There is a reason. A story", + "timestamp": [ + 3.6, + 5.199 + ], + "words": [ + { + "text": "There", + "timestamp": [ + 3.6, + 3.88 + ] + }, + { + "text": "is", + "timestamp": [ + 3.88, + 4.04 + ] + }, + { + "text": "a", + "timestamp": [ + 4.04, + 4.2 + ] + }, + { + "text": "reason.", + "timestamp": [ + 4.2, + 4.72 + ] + }, + { + "text": "A", + "timestamp": [ + 4.72, + 5 + ] + }, + { + "text": "story", + "timestamp": [ + 5, + 5.199 + ] + } + ] + }, + { + "text": "links each fact to an emotion.", + "timestamp": [ + 5.2, + 6.959 + ], + "words": [ + { + "text": "links", + "timestamp": [ + 5.2, + 5.56 + ] + }, + { + "text": "each", + "timestamp": [ + 5.56, + 5.76 + ] + }, + { + "text": "fact", + "timestamp": [ + 5.76, + 6 + ] + }, + { + "text": "to", + "timestamp": [ + 6, + 6.2 + ] + }, + { + "text": "an", + "timestamp": [ + 6.2, + 6.36 + ] + }, + { + "text": "emotion.", + "timestamp": [ + 6.36, + 6.959 + ] + } + ] + }, + { + "text": "An emotion is how the brain files things.", + "timestamp": [ + 6.96, + 9.519 + ], + "words": [ + { + "text": "An", + "timestamp": [ + 6.96, + 7.24 + ] + }, + { + "text": "emotion", + "timestamp": [ + 7.24, + 7.64 + ] + }, + { + "text": "is", + "timestamp": [ + 7.64, + 7.76 + ] + }, + { + "text": "how", + "timestamp": [ + 7.76, + 7.96 + ] + }, + { + "text": "the", + "timestamp": [ + 7.96, + 8.16 + ] + }, + { + "text": "brain", + "timestamp": [ + 8.16, + 8.44 + ] + }, + { + "text": "files", + "timestamp": [ + 8.44, + 8.88 + ] + }, + { + "text": "things.", + "timestamp": [ + 8.88, + 9.519 + ] + } + ] + }, + { + "text": "So if you want to be", + "timestamp": [ + 9.52, + 10.359 + ], + "words": [ + { + "text": "So", + "timestamp": [ + 9.52, + 9.76 + ] + }, + { + "text": "if", + "timestamp": [ + 9.76, + 9.84 + ] + }, + { + "text": "you", + "timestamp": [ + 9.84, + 9.96 + ] + }, + { + "text": "want", + "timestamp": [ + 9.96, + 10.12 + ] + }, + { + "text": "to", + "timestamp": [ + 10.12, + 10.24 + ] + }, + { + "text": "be", + "timestamp": [ + 10.24, + 10.359 + ] + } + ] + }, + { + "text": "remembered, stop listing. Start telling.", + "timestamp": [ + 10.36, + 13.433 + ], + "words": [ + { + "text": "remembered,", + "timestamp": [ + 10.36, + 11.04 + ] + }, + { + "text": "stop", + "timestamp": [ + 11.04, + 11.44 + ] + }, + { + "text": "listing.", + "timestamp": [ + 11.44, + 12.24 + ] + }, + { + "text": "Start", + "timestamp": [ + 12.24, + 12.64 + ] + }, + { + "text": "telling.", + "timestamp": [ + 12.64, + 13.433 + ] + } + ] + } + ] +} diff --git a/refs/html/template-033/word-timings.json b/refs/html/template-033/word-timings.json new file mode 100644 index 0000000..d2f037d --- /dev/null +++ b/refs/html/template-033/word-timings.json @@ -0,0 +1,235 @@ +{ + "beats": [ + { + "i": 1, + "startSec": 0.16, + "endSec": 1.759, + "cueDelayMs": 160, + "cueDurMs": 1599, + "words": [ + { + "w": "You", + "delayMs": 160 + }, + { + "w": "forget", + "delayMs": 280 + }, + { + "w": "facts", + "delayMs": 560 + }, + { + "w": "in", + "delayMs": 920 + }, + { + "w": "days,", + "delayMs": 1120 + }, + { + "w": "but", + "delayMs": 1440 + } + ] + }, + { + "i": 2, + "startSec": 1.76, + "endSec": 3.599, + "cueDelayMs": 1760, + "cueDurMs": 1839, + "words": [ + { + "w": "you", + "delayMs": 1760 + }, + { + "w": "remember", + "delayMs": 1960 + }, + { + "w": "stories", + "delayMs": 2320 + }, + { + "w": "for", + "delayMs": 2720 + }, + { + "w": "decades.", + "delayMs": 2920 + } + ] + }, + { + "i": 3, + "startSec": 3.6, + "endSec": 5.199, + "cueDelayMs": 3600, + "cueDurMs": 1599, + "words": [ + { + "w": "There", + "delayMs": 3600 + }, + { + "w": "is", + "delayMs": 3880 + }, + { + "w": "a", + "delayMs": 4040 + }, + { + "w": "reason.", + "delayMs": 4200 + }, + { + "w": "A", + "delayMs": 4720 + }, + { + "w": "story", + "delayMs": 5000 + } + ] + }, + { + "i": 4, + "startSec": 5.2, + "endSec": 6.959, + "cueDelayMs": 5200, + "cueDurMs": 1759, + "words": [ + { + "w": "links", + "delayMs": 5200 + }, + { + "w": "each", + "delayMs": 5560 + }, + { + "w": "fact", + "delayMs": 5760 + }, + { + "w": "to", + "delayMs": 6000 + }, + { + "w": "an", + "delayMs": 6200 + }, + { + "w": "emotion.", + "delayMs": 6360 + } + ] + }, + { + "i": 5, + "startSec": 6.96, + "endSec": 9.519, + "cueDelayMs": 6960, + "cueDurMs": 2559, + "words": [ + { + "w": "An", + "delayMs": 6960 + }, + { + "w": "emotion", + "delayMs": 7240 + }, + { + "w": "is", + "delayMs": 7640 + }, + { + "w": "how", + "delayMs": 7760 + }, + { + "w": "the", + "delayMs": 7960 + }, + { + "w": "brain", + "delayMs": 8160 + }, + { + "w": "files", + "delayMs": 8440 + }, + { + "w": "things.", + "delayMs": 8880 + } + ] + }, + { + "i": 6, + "startSec": 9.52, + "endSec": 10.359, + "cueDelayMs": 9520, + "cueDurMs": 839, + "words": [ + { + "w": "So", + "delayMs": 9520 + }, + { + "w": "if", + "delayMs": 9760 + }, + { + "w": "you", + "delayMs": 9840 + }, + { + "w": "want", + "delayMs": 9960 + }, + { + "w": "to", + "delayMs": 10120 + }, + { + "w": "be", + "delayMs": 10240 + } + ] + }, + { + "i": 7, + "startSec": 10.36, + "endSec": 13.433, + "cueDelayMs": 10360, + "cueDurMs": 3073, + "words": [ + { + "w": "remembered,", + "delayMs": 10360 + }, + { + "w": "stop", + "delayMs": 11040 + }, + { + "w": "listing.", + "delayMs": 11440 + }, + { + "w": "Start", + "delayMs": 12240 + }, + { + "w": "telling.", + "delayMs": 12640 + } + ] + } + ] +} diff --git a/refs/html/template-034/recipe.md b/refs/html/template-034/recipe.md new file mode 100644 index 0000000..f608cd0 --- /dev/null +++ b/refs/html/template-034/recipe.md @@ -0,0 +1,84 @@ +# RECIPE — template-034 (9:16 · 736×1312 @ 30fps) — Countdown board + +RESOLUTION: px below are authored at the source template's own canvas, 480×864; the compiled module +scales them per run (`pipeline/recipes/template-lib.ts`, `canvasFor`): portrait SCALE = W/480 for sizes +and horizontal offsets, H/864 for vertical anchors; landscape SCALE = (H/864)×1.6. `em` values and ms +timings never scale; the manifest carries the run's real W/H. The reference 9:16 canvas 736×1312 is +SCALE 1.533. + +## 1. IDENTITY + +Archivo Black caps on a left-aligned board, one or two words a line, white with a hard 3×4px offset +shadow; each word rises 0.3em in. The beat's strongest word pops in on a yellow plate (overshoot +ease), and the closing beat's short tail after its last sentence end lands on red plates at 72px, one +word a line. When the opening beat announces a count ("three ways", "5 tips"), the beats that follow +carry giant red numerals top-right. + +## 2. SKELETON + +``` + + +``` + +## 3. PER-BEAT ASSEMBLY + +``` +
+
{numeral}
+
+
TWICE
+
…
+
+
+``` +- Font 52px; advance 0.70em → maxChars = floor(392 / (0.70 × 52)) = 10 per line, 2 lines a page (page + cap 2×maxChars − 1 chars, 4 units). Overflow (a line over maxChars) → demote ×0.92, up to 5 rows. +- Each page is its own gate: from its first word to the next page's first word (last page → cue end). +- CTA: in the LAST beat, the units after its last sentence end (≤ 2 units, each ≤ 7 chars) form the final + page, one unit a line at 72px (`.clC`) on `.cta` plates. +- Numerals: count = the first number word / digit (> 1) in beat 1; beat k (2 ≤ k ≤ count + 1) shows + k − 1 at 340px (two digits at 238px). No announced count → no numerals. + +## 4. WORDS + TIMING + +One span per word at its `delayMs` VERBATIM; the numeral fires at the beat's `cueDelayMs`. + +## 5. EMPHASIS + +Plate word = `accentIndex` → `.hl` (+ `.lead` when it starts its line). CTA plates as above. + +## BOUNDED VARIETY + +None beyond the count-driven numerals. + +## 6. VERIFY LOOP + +``` +{repo}/.veed-engine/veed-engine-cli {repo}/runs//final --verify +``` +bounds on a `b{N}…` id → the runner demotes that beat one row (font × 0.92, capacities recomputed) and +regenerates; ≤ 2 cycles → `--record`. Manifest: `{"render":{"width":W,"height":H,"fps":FPS,"duration":DUR}}`. +Ids sit on the element that wraps the text (`.cl` lines, `.hw`/`.sw` titles); demotion is keyed per beat. + +## 7. DO NOT + +No fonts, colours or keyframes beyond this sheet; no invented timing; no reading the frames; no depth +compositing (the source's behind-the-presenter layers are drawn on top here); never hand-edit the output. diff --git a/refs/html/template-034/recipe.ts b/refs/html/template-034/recipe.ts new file mode 100644 index 0000000..c10297f --- /dev/null +++ b/refs/html/template-034/recipe.ts @@ -0,0 +1,139 @@ +/** + * template-034 "Countdown board": Archivo Black caps on a left-aligned board, one or two words a line, + * hard offset shadows; the beat's strongest word pops in on a yellow plate, and the closing call to + * action lands on red plates at a larger size. When the opening beat announces a count ("three + * ways"), the beats that follow carry giant red numerals top-right; the source composited them + * behind the presenter, here they sit on top. + */ +import { accentIndex } from '../../../pipeline/recipes/lib.ts'; +import { + type Unit, + DEMOTE_STEP, + canvasFor, + cueDiv, + docShell, + eachBeat, + escapeHtml, + paginate, + templateRecipe, + unitDelay, + unitText, + wrapByChars, +} from '../../../pipeline/recipes/template-lib.ts'; + +const FONT = 52; +const CTA_FONT = 72; +const AVG_EM = 0.70; // Archivo Black caps incl. -0.02em tracking +const LEFT = 34; +const WIDTH = 392; +const BOTTOM = 210; +const MAX_LINES = 2; +const MAX_CTA_CHARS = 7; // 392px / (0.70 * 72px) +const NUMERAL_FONT = 340; +const NUMBER_WORDS: Record = { two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10 }; + +// The count the opening beat announces ("three ways", "5 tips"), else 0. +function announcedCount(words: { w: string }[]): number { + for (const { w } of words) { + const t = w.toLowerCase().replace(/[^a-z0-9]/g, ''); + if (NUMBER_WORDS[t]) return NUMBER_WORDS[t]; + if (/^\d{1,2}$/.test(t) && Number(t) > 1) return Number(t); + } + return 0; +} + +// Units after the last sentence end, when they are few and short enough for the CTA plates. +function ctaTail(units: Unit[]): number { + for (let k = units.length - 2; k >= 0 && units.length - 1 - k <= 2; k--) { + if (/[.!?]$/.test(unitText(units[k]))) { + const tail = units.slice(k + 1); + return tail.every((u) => u.chars <= MAX_CTA_CHARS) ? tail.length : 0; + } + } + return 0; +} + +export default templateRecipe('template-034', (meta, timings, opts) => { + const c = canvasFor(meta); + let wordSeq = 0; + + const count = timings.beats.length > 1 ? announcedCount(timings.beats[0].words) : 0; + const cues = eachBeat(meta, timings, opts, 'uppercase', (b) => { + const hl = accentIndex(b.units); + // beat k (1-based) after the opener carries numeral k - 1 while the count lasts + const numeral = count && b.n >= 2 && b.n - 1 <= count ? String(b.n - 1) : ''; + const numeralHtml = numeral + ? `
${numeral}
\n` + : ''; + let rows = b.rows; + let f = 1; + let maxChars = 0; + let pages: Unit[][] = []; + let pageLines: Unit[][][] = []; + for (; rows <= b.rows + 5; rows++) { + f = Math.pow(DEMOTE_STEP, rows); + maxChars = Math.floor(WIDTH / (AVG_EM * FONT * f)); + // the closing beat's short tail after its last sentence end ("SAVE THIS.") is the CTA page + const tail = b.isLast ? ctaTail(b.units) : 0; + const head = tail ? b.units.slice(0, -tail) : b.units; + pages = head.length ? paginate(head, maxChars * MAX_LINES - 1, 4) : []; + if (tail) pages.push(b.units.slice(-tail)); + pageLines = pages.map((p, i) => (tail && i === pages.length - 1 ? p.map((u) => [u]) : wrapByChars(p, maxChars))); + if (pageLines.every((ls) => ls.length <= MAX_LINES && ls.every((l) => l.reduce((a, u) => a + u.chars, 0) + l.length - 1 <= maxChars))) break; + } + const fs = Math.round(FONT * f * c.s); + const ctaFs = Math.round(CTA_FONT * f * c.s); + + const pageDivs = pages.map((page, pi) => { + const pageId = `b${b.n}p${pi + 1}`; + const firstDelay = unitDelay(page[0]); + const endMs = pi + 1 < pages.length ? unitDelay(pages[pi + 1][0]) : b.cueEndMs; + const lines = pageLines[pi]; + const cta = b.isLast && pi === pages.length - 1 && ctaTail(b.units) > 0; + const lineDivs = lines.map((line, li) => { + const spans = line.map((u, ui) => u.spans.map((s) => { + wordSeq++; + const plate = cta ? 'cta' : b.units.indexOf(u) === hl ? 'hl' : ''; + const lead = plate && ui === 0 ? (cta ? 'leadC' : 'lead') : ''; + const cls = ['w', plate, lead].filter(Boolean).join(' '); + return `${escapeHtml(s.text)}`; + }).join('')).join(''); + return `
${spans}
`; + }).join('\n '); + // each page is its own gate: from its first word to the next page's first word + return `
\n ${lineDivs}\n
`; + }); + return cueDiv(b.n, b.cueDelayMs, b.winMs, numeralHtml + pageDivs.join('\n')); + }); + + const shadow = `${c.px(3)}px ${c.px(4)}px 0 #101010, 0 ${c.px(3)}px ${c.px(13)}px rgba(25,25,25,0.38), 0 ${c.px(1)}px ${c.px(3)}px rgba(25,25,25,0.3)`; + const css = ` + .board { left:${c.px(LEFT)}px; bottom:${c.py(BOTTOM)}px; width:${c.px(WIDTH)}px; z-index:2; } + .cl { display:block; text-align:left; font-family:'Archivo Black'; font-weight:400; letter-spacing:-0.02em; line-height:1; padding:0 0 ${c.px(6)}px; } + .clC { display:block; text-align:left; font-family:'Archivo Black'; font-weight:400; letter-spacing:-0.03em; line-height:1; padding:0 0 ${c.px(9)}px; } + .w { display:inline-block; opacity:0; line-height:1; margin-right:0.22em; padding:${c.px(5)}px 0 ${c.px(10)}px; color:#FFFFFF; + text-shadow:${shadow}; + animation-name:wIn; animation-duration:200ms; animation-timing-function:cubic-bezier(.2,.7,.3,1); animation-fill-mode:both; } + @keyframes wIn { 0%{opacity:0; transform:translateY(0.3em)} 100%{opacity:1; transform:none} } + .hl { background:#FFE500; color:#101010; padding:${c.px(5)}px ${c.px(11)}px ${c.px(10)}px; text-shadow:none; box-shadow:${shadow}; + animation-name:wPop; animation-duration:260ms; animation-timing-function:cubic-bezier(.34,1.4,.64,1); } + @keyframes wPop { 0%{opacity:0; transform:translateY(0.18em) scale(.86)} 100%{opacity:1; transform:none} } + .cta { background:#FF2E12; color:#101010; padding:${c.px(6)}px ${c.px(13)}px ${c.px(12)}px; text-shadow:none; + box-shadow:${c.px(4)}px ${c.px(5)}px 0 #101010, 0 ${c.px(3)}px ${c.px(13)}px rgba(25,25,25,0.38), 0 ${c.px(1)}px ${c.px(3)}px rgba(25,25,25,0.3); + animation-name:wPop; animation-duration:260ms; animation-timing-function:cubic-bezier(.34,1.4,.64,1); } + .idx { position:absolute; right:${c.px(34)}px; top:${c.py(34)}px; width:${c.px(380)}px; text-align:right; z-index:3; } + .ig { display:inline-block; opacity:0; line-height:1; padding:0 0 0.06em; font-family:'Archivo Black'; font-weight:400; letter-spacing:-0.045em; color:#FF2E12; + text-shadow:0 ${c.px(7)}px ${c.px(32)}px rgba(25,25,25,0.38), 0 ${c.px(4)}px ${c.px(7)}px rgba(25,25,25,0.3); + animation:idxIn 420ms cubic-bezier(.2,.7,.3,1) both; } + @keyframes idxIn { 0%{opacity:0; transform:translateY(${c.px(26)}px) scale(1.06)} 100%{opacity:1; transform:none} } + .lead { margin-left:-${c.px(11)}px; } + .leadC { margin-left:-${c.px(13)}px; }`; + + return docShell({ + canvas: c, + videoPath: meta.videoPath, + fonts: ['Archivo+Black'], + css, + body: cues.join('\n'), + }); +}); diff --git a/refs/html/template-034/template.wv b/refs/html/template-034/template.wv new file mode 100644 index 0000000..40113eb --- /dev/null +++ b/refs/html/template-034/template.wv @@ -0,0 +1,103 @@ + + + + + + +
+
+
THREEWAYS
+
TOMAKE
+
+
+
ASMALL
+
ROOMFEEL
+
+
+
TWICE
+
ASBIG.
+
+
+
+
1
+
+
MIRRORS
+
OPPOSITE
+
+
+
WINDOWS.
+
+
+
+
2
+
+
CURTAINS
+
HUNGHIGH
+
+
+
RIGHT
+
UNDERTHE
+
+
+
CEILING.
+
+
+
+
3
+
+
ONEBIG
+
PIECE
+
+
+
INSTEAD
+
OFFIVE
+
+
+
SMALL
+
ONES.
+
+
+
+
+
CLUTTER
+
SHRINKSA
+
+
+
ROOM.
+
+
+
SAVE
+
THIS.
+
+
diff --git a/refs/html/template-034/transcript.json b/refs/html/template-034/transcript.json new file mode 100644 index 0000000..571e5e6 --- /dev/null +++ b/refs/html/template-034/transcript.json @@ -0,0 +1,295 @@ +{ + "text": "THREE WAYS TO MAKE A SMALL ROOM FEEL TWICE AS BIG. MIRRORS OPPOSITE WINDOWS. CURTAINS HUNG HIGH RIGHT UNDER THE CEILING. ONE BIG PIECE INSTEAD OF FIVE SMALL ONES. CLUTTER SHRINKS A ROOM. SAVE THIS.", + "chunks": [ + { + "text": "THREE WAYS TO MAKE A SMALL ROOM FEEL TWICE AS BIG.", + "timestamp": [ + 0.24, + 3.12 + ], + "words": [ + { + "text": "THREE", + "timestamp": [ + 0.24, + 0.36 + ] + }, + { + "text": "WAYS", + "timestamp": [ + 0.36, + 0.64 + ] + }, + { + "text": "TO", + "timestamp": [ + 0.64, + 0.72 + ] + }, + { + "text": "MAKE", + "timestamp": [ + 0.72, + 0.84 + ] + }, + { + "text": "A", + "timestamp": [ + 0.84, + 1 + ] + }, + { + "text": "SMALL", + "timestamp": [ + 1, + 1.2 + ] + }, + { + "text": "ROOM", + "timestamp": [ + 1.2, + 1.44 + ] + }, + { + "text": "FEEL", + "timestamp": [ + 1.44, + 1.68 + ] + }, + { + "text": "TWICE", + "timestamp": [ + 1.68, + 1.96 + ] + }, + { + "text": "AS", + "timestamp": [ + 1.96, + 2.12 + ] + }, + { + "text": "BIG.", + "timestamp": [ + 2.12, + 3.12 + ] + } + ] + }, + { + "text": "MIRRORS OPPOSITE WINDOWS.", + "timestamp": [ + 3.12, + 4.96 + ], + "words": [ + { + "text": "MIRRORS", + "timestamp": [ + 3.12, + 3.6 + ] + }, + { + "text": "OPPOSITE", + "timestamp": [ + 3.6, + 3.96 + ] + }, + { + "text": "WINDOWS.", + "timestamp": [ + 3.96, + 4.96 + ] + } + ] + }, + { + "text": "CURTAINS HUNG HIGH RIGHT UNDER THE CEILING.", + "timestamp": [ + 4.96, + 6.96 + ], + "words": [ + { + "text": "CURTAINS", + "timestamp": [ + 4.96, + 5.52 + ] + }, + { + "text": "HUNG", + "timestamp": [ + 5.52, + 5.72 + ] + }, + { + "text": "HIGH", + "timestamp": [ + 5.72, + 5.88 + ] + }, + { + "text": "RIGHT", + "timestamp": [ + 5.88, + 6.04 + ] + }, + { + "text": "UNDER", + "timestamp": [ + 6.04, + 6.2 + ] + }, + { + "text": "THE", + "timestamp": [ + 6.2, + 6.36 + ] + }, + { + "text": "CEILING.", + "timestamp": [ + 6.36, + 6.96 + ] + } + ] + }, + { + "text": "ONE BIG PIECE INSTEAD OF FIVE SMALL ONES.", + "timestamp": [ + 6.96, + 10.32 + ], + "words": [ + { + "text": "ONE", + "timestamp": [ + 7.92, + 8.2 + ] + }, + { + "text": "BIG", + "timestamp": [ + 8.2, + 8.4 + ] + }, + { + "text": "PIECE", + "timestamp": [ + 8.4, + 8.68 + ] + }, + { + "text": "INSTEAD", + "timestamp": [ + 8.68, + 9 + ] + }, + { + "text": "OF", + "timestamp": [ + 9, + 9.16 + ] + }, + { + "text": "FIVE", + "timestamp": [ + 9.16, + 9.36 + ] + }, + { + "text": "SMALL", + "timestamp": [ + 9.36, + 9.6 + ] + }, + { + "text": "ONES.", + "timestamp": [ + 9.6, + 10.32 + ] + } + ] + }, + { + "text": "CLUTTER SHRINKS A ROOM. SAVE THIS.", + "timestamp": [ + 10.32, + 13.399 + ], + "words": [ + { + "text": "CLUTTER", + "timestamp": [ + 10.32, + 10.84 + ] + }, + { + "text": "SHRINKS", + "timestamp": [ + 10.84, + 11.16 + ] + }, + { + "text": "A", + "timestamp": [ + 11.16, + 11.32 + ] + }, + { + "text": "ROOM.", + "timestamp": [ + 11.32, + 12.16 + ] + }, + { + "text": "SAVE", + "timestamp": [ + 12.16, + 12.64 + ] + }, + { + "text": "THIS.", + "timestamp": [ + 12.64, + 13.399 + ] + } + ] + } + ] +} diff --git a/refs/html/template-034/word-timings.json b/refs/html/template-034/word-timings.json new file mode 100644 index 0000000..6797142 --- /dev/null +++ b/refs/html/template-034/word-timings.json @@ -0,0 +1,189 @@ +{ + "beats": [ + { + "i": 1, + "startSec": 0.24, + "endSec": 3.12, + "cueDelayMs": 240, + "cueDurMs": 2880, + "words": [ + { + "w": "THREE", + "delayMs": 240 + }, + { + "w": "WAYS", + "delayMs": 360 + }, + { + "w": "TO", + "delayMs": 640 + }, + { + "w": "MAKE", + "delayMs": 720 + }, + { + "w": "A", + "delayMs": 840 + }, + { + "w": "SMALL", + "delayMs": 1000 + }, + { + "w": "ROOM", + "delayMs": 1200 + }, + { + "w": "FEEL", + "delayMs": 1440 + }, + { + "w": "TWICE", + "delayMs": 1680 + }, + { + "w": "AS", + "delayMs": 1960 + }, + { + "w": "BIG.", + "delayMs": 2120 + } + ] + }, + { + "i": 2, + "startSec": 3.12, + "endSec": 4.96, + "cueDelayMs": 3120, + "cueDurMs": 1840, + "words": [ + { + "w": "MIRRORS", + "delayMs": 3120 + }, + { + "w": "OPPOSITE", + "delayMs": 3600 + }, + { + "w": "WINDOWS.", + "delayMs": 3960 + } + ] + }, + { + "i": 3, + "startSec": 4.96, + "endSec": 6.96, + "cueDelayMs": 4960, + "cueDurMs": 2000, + "words": [ + { + "w": "CURTAINS", + "delayMs": 4960 + }, + { + "w": "HUNG", + "delayMs": 5520 + }, + { + "w": "HIGH", + "delayMs": 5720 + }, + { + "w": "RIGHT", + "delayMs": 5880 + }, + { + "w": "UNDER", + "delayMs": 6040 + }, + { + "w": "THE", + "delayMs": 6200 + }, + { + "w": "CEILING.", + "delayMs": 6360 + } + ] + }, + { + "i": 4, + "startSec": 6.96, + "endSec": 10.32, + "cueDelayMs": 6960, + "cueDurMs": 3360, + "words": [ + { + "w": "ONE", + "delayMs": 7920 + }, + { + "w": "BIG", + "delayMs": 8200 + }, + { + "w": "PIECE", + "delayMs": 8400 + }, + { + "w": "INSTEAD", + "delayMs": 8680 + }, + { + "w": "OF", + "delayMs": 9000 + }, + { + "w": "FIVE", + "delayMs": 9160 + }, + { + "w": "SMALL", + "delayMs": 9360 + }, + { + "w": "ONES.", + "delayMs": 9600 + } + ] + }, + { + "i": 5, + "startSec": 10.32, + "endSec": 13.399, + "cueDelayMs": 10320, + "cueDurMs": 3079, + "words": [ + { + "w": "CLUTTER", + "delayMs": 10320 + }, + { + "w": "SHRINKS", + "delayMs": 10840 + }, + { + "w": "A", + "delayMs": 11160 + }, + { + "w": "ROOM.", + "delayMs": 11320 + }, + { + "w": "SAVE", + "delayMs": 12160 + }, + { + "w": "THIS.", + "delayMs": 12640 + } + ] + } + ] +} diff --git a/refs/html/template-035/recipe.md b/refs/html/template-035/recipe.md new file mode 100644 index 0000000..4a7df41 --- /dev/null +++ b/refs/html/template-035/recipe.md @@ -0,0 +1,62 @@ +# RECIPE — template-035 (9:16 · 736×1312 @ 30fps) — Verdict stamp + +RESOLUTION: px below are authored at the source template's own canvas, 480×864; the compiled module +scales them per run (`pipeline/recipes/template-lib.ts`, `canvasFor`): portrait SCALE = W/480 for sizes +and horizontal offsets, H/864 for vertical anchors; landscape SCALE = (H/864)×1.6. `em` values and ms +timings never scale; the manifest carries the run's real W/H. The reference 9:16 canvas 736×1312 is +SCALE 1.533. The shell (reset · body · .vid · cueWin/.cue · .pg with pgOn/pgOff/pgMid) is the shared +one in `template-lib.ts` `docShell`; the SKELETON lists what this recipe adds. + +## 1. IDENTITY + +Condensed all-caps Barlow Semi Condensed captions low in the frame, each word fading and rising in +over 150ms with a hard ink seat and a wide low halo; the beat's strongest word leaves the caption and +slams in above it as an Anton stamp on an orange slab with a hard offset shadow, tilted −2.4° / 1.9° +by beat, a die-drop squash on impact, holding until the beat ends. + +## 2. SKELETON + +``` +fonts: Anton · Barlow+Semi+Condensed:wght@700 +.cap { left:53px; right:53px; bottom:150px; center; 'Barlow Semi Condensed' 700; letter-spacing:.03em; color:#F4EFE6; + text-shadow:0 2px 0 rgba(22,19,15,.92), 0 0 12px rgba(22,19,15,.6) } .cl { line-height:1.08; padding:1px 0 } +.g { inline-block; opacity:0; animation:capG 150ms cubic-bezier(.2,.7,.3,1) forwards } @keyframes capG { 0%{opacity:0; translateY(10px)} 100%{opacity:1} } +.st { absolute; left:0; right:0; bottom:243px; flex; center; z-index:6; opacity:0; animation:cueWin linear forwards } +.sr { inline-block; transform:rotate(±) } .sk { block; 'Anton'; line-height:.86; letter-spacing:-.03em; color:#16130F; background:#FF6A13; + padding:14px 18px 12px; box-shadow:6px 6px 0 #16130F; animation:stHit 230ms cubic-bezier(.3,.9,.2,1) both } +@keyframes stHit { 0%{scale(1.14,1.3)} 52%{scale(1.03,.93)} 78%{scale(.99,1.02)} 100%{scale(1)} } +``` + +## 3. PER-BEAT ASSEMBLY + +- Caption 30px; advance 0.58em → maxChars = floor(374 / (0.58 × 30)) = 21 per line, 2 lines a page (cap + 2×maxChars − 2 chars, 8 units); overflow → demote ×0.92 per row. +- Stamp 96px Anton, advance 0.5em, shrunk so the slab (36px padding) stays inside 408px; window from the + word's delay to the beat's end; tilt −2.4° on odd beats, 1.9° on even. + +## 4. WORDS + TIMING + +One span per word (glyph spans where the recipe types), `animation-delay` = `delayMs` VERBATIM; the cue gets `{cueDelayMs}` and a window running to the next beat's start (the last beat to the video's end). Mid-beat pages switch at the successor's first word. + +## 5. EMPHASIS + +Stamp = `accentIndex` of beats with ≥ 2 units, REMOVED from the caption; a one-word beat stays in the caption. + +## BOUNDED VARIETY + +Tilt alternation only. + +## 6. VERIFY LOOP + +``` +{repo}/.veed-engine/veed-engine-cli {repo}/runs//final --verify +``` +bounds on a `b{N}…` id → the runner demotes that beat one row (font × 0.92, capacities recomputed) and +regenerates; ≤ 2 cycles → `--record`. Manifest: `{"render":{"width":W,"height":H,"fps":FPS,"duration":DUR}}`. +Ids sit on the element that wraps the text; demotion is keyed per beat. + +## 7. DO NOT + +No fonts, colours or keyframes beyond this sheet; no invented timing; no reading the frames; no depth +compositing (layers the source composited behind the presenter are drawn on top here); never hand-edit +the output. diff --git a/refs/html/template-035/recipe.ts b/refs/html/template-035/recipe.ts new file mode 100644 index 0000000..95a1511 --- /dev/null +++ b/refs/html/template-035/recipe.ts @@ -0,0 +1,99 @@ +/** + * template-035 "Verdict stamp": condensed all-caps Barlow captions low in the frame, each word fading + * and rising in; the beat's strongest word leaves the caption and slams in above it as an Anton + * stamp on an orange slab (die-drop squash, alternating tilt), holding until the next stamp. + */ +import { accentIndex } from '../../../pipeline/recipes/lib.ts'; +import { + type Unit, + DEMOTE_STEP, + canvasFor, + cueDiv, + docShell, + eachBeat, + escapeHtml, + glyphSpans, + pageGateStyle, + paginate, + templateRecipe, + unitDelay, + unitText, + wrapByChars, +} from '../../../pipeline/recipes/template-lib.ts'; + +const FONT = 30; +const AVG_EM = 0.58; // Barlow Semi Condensed 700 caps + 0.03em tracking +const MARGIN = 53; +const BOTTOM = 150; +const MAX_LINES = 2; +const STAMP_FONT = 96; +const STAMP_EM = 0.52; // Anton caps +const STAMP_BOTTOM = 243; +const STAMP_MIN_UNITS = 2; +const TILT = ['-2.4deg', '1.9deg']; + +export default templateRecipe('template-035', (meta, timings, opts) => { + const c = canvasFor(meta); + const measure = c.refWidth - 2 * MARGIN; + + const cues = eachBeat(meta, timings, opts, 'uppercase', (b) => { + const stampIdx = b.units.length >= STAMP_MIN_UNITS ? accentIndex(b.units) : -1; + const stamp = stampIdx >= 0 ? b.units[stampIdx] : null; + const capUnits = stamp ? b.units.filter((u) => u !== stamp) : b.units; + let stampHtml = ''; + if (stamp) { + const text = unitText(stamp); + const font = Math.min(STAMP_FONT, (c.refWidth - 2 * 36 - 36) / (STAMP_EM * text.length)) * Math.pow(DEMOTE_STEP, b.rows); + const start = unitDelay(stamp); + stampHtml = `
${escapeHtml(text)}
\n`; + } + let rows = b.rows; + let f = 1; + let maxChars = 0; + let pages: Unit[][] = []; + let pageLines: Unit[][][] = []; + for (; rows <= b.rows + 4; rows++) { + f = Math.pow(DEMOTE_STEP, rows); + maxChars = Math.floor(measure / (AVG_EM * FONT * f)); + pages = capUnits.length ? paginate(capUnits, maxChars * MAX_LINES - 2, 8) : []; + pageLines = pages.map((p) => wrapByChars(p, maxChars)); + if (pageLines.every((ls) => ls.length <= MAX_LINES)) break; + } + const fs = Math.round(FONT * f * c.s); + const pageDivs = pages.map((page, pi) => { + const pageId = `b${b.n}p${pi + 1}`; + const firstDelay = unitDelay(page[0]); + const nextStart = pi + 1 < pages.length ? unitDelay(pages[pi + 1][0]) : null; + const lines = pageLines[pi].map((line, li) => { + const units = line.map((u) => `${u.spans.map((s) => glyphSpans(s.text, s.delayMs, 0, 'g')).join('')}`); + return `
${units.join(' ')}
`; + }).join('\n '); + return `
\n ${lines}\n
`; + }); + return cueDiv(b.n, b.cueDelayMs, b.winMs, stampHtml + pageDivs.join('\n')); + }); + + const css = ` + .cap { left:${c.px(MARGIN)}px; right:${c.px(MARGIN)}px; bottom:${c.py(BOTTOM)}px; text-align:center; z-index:2; + font-family:'Barlow Semi Condensed'; font-weight:700; letter-spacing:0.03em; color:#F4EFE6; + text-shadow:0 ${c.px(2)}px 0 rgba(22,19,15,.92), 0 0 ${c.px(12)}px rgba(22,19,15,.6); } + .cl { display:block; line-height:1.08; padding:${c.px(1)}px 0; } + .u { display:inline-block; white-space:pre; } + .sp { display:inline-block; white-space:pre; } + .g { display:inline-block; opacity:0; animation:capG 150ms cubic-bezier(.2,.7,.3,1) forwards; } + @keyframes capG { 0%{opacity:0; transform:translateY(${c.px(10)}px)} 100%{opacity:1; transform:none} } + .st { position:absolute; left:0; right:0; bottom:${c.py(STAMP_BOTTOM)}px; display:flex; justify-content:center; z-index:6; opacity:0; animation:cueWin linear forwards; } + .sr { display:inline-block; transform-origin:50% 50%; } + .sk { display:block; font-family:'Anton'; font-weight:400; line-height:.86; letter-spacing:-0.03em; color:#16130F; background:#FF6A13; + padding:${c.px(14)}px ${c.px(18)}px ${c.px(12)}px; box-shadow:${c.px(6)}px ${c.px(6)}px 0 #16130F; + animation-name:stHit; animation-duration:230ms; animation-timing-function:cubic-bezier(.3,.9,.2,1); animation-fill-mode:both; } + @keyframes stHit { 0%{transform:scale(1.14,1.3)} 52%{transform:scale(1.03,.93)} 78%{transform:scale(.99,1.02)} 100%{transform:scale(1,1)} }`; + + return docShell({ + canvas: c, + videoPath: meta.videoPath, + fonts: ['Anton', 'Barlow+Semi+Condensed:wght@700'], + css, + body: cues.join('\n'), + }); +}); diff --git a/refs/html/template-035/template.wv b/refs/html/template-035/template.wv new file mode 100644 index 0000000..c2de42d --- /dev/null +++ b/refs/html/template-035/template.wv @@ -0,0 +1,72 @@ + + + + + + +
+
BOTCHED
+
+
EVERY REPAIR
+
I GET CALLED
+
+
+
+
SOMEBODY
+
+
OUT TO STARTED
+
WITH RUSHING
+
+
+
+
DIAGNOSE
+
+
MY RULE IS
+
TWICE FIX ONCE
+
+
+
+
PROBLEM
+
+
THE IS NEVER WHERE
+
THE NOISE IS. SLOW
+
+
+
+
CHEAPEST
+
+
IS SMOOTH AND SMOOTH
+
IS FAST LESSON
+
+
+
+
TRADE.
+
+
IN THIS
+
+
diff --git a/refs/html/template-035/transcript.json b/refs/html/template-035/transcript.json new file mode 100644 index 0000000..76927b7 --- /dev/null +++ b/refs/html/template-035/transcript.json @@ -0,0 +1,332 @@ +{ + "text": "EVERY BOTCHED REPAIR I GET CALLED OUT TO STARTED WITH SOMEBODY RUSHING MY RULE IS DIAGNOSE TWICE FIX ONCE THE PROBLEM IS NEVER WHERE THE NOISE IS. SLOW IS SMOOTH AND SMOOTH IS FAST CHEAPEST LESSON IN THIS TRADE.", + "chunks": [ + { + "text": "EVERY BOTCHED REPAIR I GET CALLED", + "timestamp": [ + 0.16, + 1.89 + ], + "words": [ + { + "text": "EVERY", + "timestamp": [ + 0.16, + 0.36 + ] + }, + { + "text": "BOTCHED", + "timestamp": [ + 0.36, + 0.92 + ] + }, + { + "text": "REPAIR", + "timestamp": [ + 0.92, + 1.24 + ] + }, + { + "text": "I", + "timestamp": [ + 1.24, + 1.4 + ] + }, + { + "text": "GET", + "timestamp": [ + 1.4, + 1.64 + ] + }, + { + "text": "CALLED", + "timestamp": [ + 1.64, + 1.89 + ] + } + ] + }, + { + "text": "OUT TO STARTED WITH SOMEBODY RUSHING", + "timestamp": [ + 1.92, + 4.04 + ], + "words": [ + { + "text": "OUT", + "timestamp": [ + 1.92, + 2.12 + ] + }, + { + "text": "TO", + "timestamp": [ + 2.12, + 2.36 + ] + }, + { + "text": "STARTED", + "timestamp": [ + 2.36, + 2.68 + ] + }, + { + "text": "WITH", + "timestamp": [ + 2.68, + 2.92 + ] + }, + { + "text": "SOMEBODY", + "timestamp": [ + 2.92, + 3.28 + ] + }, + { + "text": "RUSHING", + "timestamp": [ + 3.28, + 4.04 + ] + } + ] + }, + { + "text": "MY RULE IS DIAGNOSE TWICE FIX ONCE", + "timestamp": [ + 4.08, + 7.16 + ], + "words": [ + { + "text": "MY", + "timestamp": [ + 4.08, + 4.4 + ] + }, + { + "text": "RULE", + "timestamp": [ + 4.4, + 4.64 + ] + }, + { + "text": "IS", + "timestamp": [ + 4.64, + 4.8 + ] + }, + { + "text": "DIAGNOSE", + "timestamp": [ + 4.8, + 5.4 + ] + }, + { + "text": "TWICE", + "timestamp": [ + 5.4, + 5.84 + ] + }, + { + "text": "FIX", + "timestamp": [ + 5.84, + 6.24 + ] + }, + { + "text": "ONCE", + "timestamp": [ + 6.24, + 7.16 + ] + } + ] + }, + { + "text": "THE PROBLEM IS NEVER WHERE THE NOISE IS. SLOW", + "timestamp": [ + 7.2, + 10.04 + ], + "words": [ + { + "text": "THE", + "timestamp": [ + 7.2, + 7.48 + ] + }, + { + "text": "PROBLEM", + "timestamp": [ + 7.48, + 7.92 + ] + }, + { + "text": "IS", + "timestamp": [ + 7.92, + 8.2 + ] + }, + { + "text": "NEVER", + "timestamp": [ + 8.2, + 8.36 + ] + }, + { + "text": "WHERE", + "timestamp": [ + 8.36, + 8.52 + ] + }, + { + "text": "THE", + "timestamp": [ + 8.52, + 8.68 + ] + }, + { + "text": "NOISE", + "timestamp": [ + 8.68, + 8.96 + ] + }, + { + "text": "IS.", + "timestamp": [ + 8.96, + 9.52 + ] + }, + { + "text": "SLOW", + "timestamp": [ + 9.52, + 10.04 + ] + } + ] + }, + { + "text": "IS SMOOTH AND SMOOTH IS FAST CHEAPEST LESSON", + "timestamp": [ + 10.04, + 13.64 + ], + "words": [ + { + "text": "IS", + "timestamp": [ + 10.04, + 10.24 + ] + }, + { + "text": "SMOOTH", + "timestamp": [ + 10.24, + 10.88 + ] + }, + { + "text": "AND", + "timestamp": [ + 10.88, + 11.24 + ] + }, + { + "text": "SMOOTH", + "timestamp": [ + 11.24, + 11.68 + ] + }, + { + "text": "IS", + "timestamp": [ + 11.68, + 11.96 + ] + }, + { + "text": "FAST", + "timestamp": [ + 11.96, + 12.72 + ] + }, + { + "text": "CHEAPEST", + "timestamp": [ + 12.72, + 13.28 + ] + }, + { + "text": "LESSON", + "timestamp": [ + 13.28, + 13.64 + ] + } + ] + }, + { + "text": "IN THIS TRADE.", + "timestamp": [ + 13.64, + 14.56 + ], + "words": [ + { + "text": "IN", + "timestamp": [ + 13.64, + 13.76 + ] + }, + { + "text": "THIS", + "timestamp": [ + 13.76, + 13.96 + ] + }, + { + "text": "TRADE.", + "timestamp": [ + 13.96, + 14.56 + ] + } + ] + } + ] +} diff --git a/refs/html/template-035/word-timings.json b/refs/html/template-035/word-timings.json new file mode 100644 index 0000000..de46caf --- /dev/null +++ b/refs/html/template-035/word-timings.json @@ -0,0 +1,214 @@ +{ + "beats": [ + { + "i": 1, + "startSec": 0.16, + "endSec": 1.89, + "cueDelayMs": 160, + "cueDurMs": 1730, + "words": [ + { + "w": "EVERY", + "delayMs": 160 + }, + { + "w": "BOTCHED", + "delayMs": 360 + }, + { + "w": "REPAIR", + "delayMs": 920 + }, + { + "w": "I", + "delayMs": 1240 + }, + { + "w": "GET", + "delayMs": 1400 + }, + { + "w": "CALLED", + "delayMs": 1640 + } + ] + }, + { + "i": 2, + "startSec": 1.92, + "endSec": 4.04, + "cueDelayMs": 1920, + "cueDurMs": 2120, + "words": [ + { + "w": "OUT", + "delayMs": 1920 + }, + { + "w": "TO", + "delayMs": 2120 + }, + { + "w": "STARTED", + "delayMs": 2360 + }, + { + "w": "WITH", + "delayMs": 2680 + }, + { + "w": "SOMEBODY", + "delayMs": 2920 + }, + { + "w": "RUSHING", + "delayMs": 3280 + } + ] + }, + { + "i": 3, + "startSec": 4.08, + "endSec": 7.16, + "cueDelayMs": 4080, + "cueDurMs": 3080, + "words": [ + { + "w": "MY", + "delayMs": 4080 + }, + { + "w": "RULE", + "delayMs": 4400 + }, + { + "w": "IS", + "delayMs": 4640 + }, + { + "w": "DIAGNOSE", + "delayMs": 4800 + }, + { + "w": "TWICE", + "delayMs": 5400 + }, + { + "w": "FIX", + "delayMs": 5840 + }, + { + "w": "ONCE", + "delayMs": 6240 + } + ] + }, + { + "i": 4, + "startSec": 7.2, + "endSec": 10.04, + "cueDelayMs": 7200, + "cueDurMs": 2840, + "words": [ + { + "w": "THE", + "delayMs": 7200 + }, + { + "w": "PROBLEM", + "delayMs": 7480 + }, + { + "w": "IS", + "delayMs": 7920 + }, + { + "w": "NEVER", + "delayMs": 8200 + }, + { + "w": "WHERE", + "delayMs": 8360 + }, + { + "w": "THE", + "delayMs": 8520 + }, + { + "w": "NOISE", + "delayMs": 8680 + }, + { + "w": "IS.", + "delayMs": 8960 + }, + { + "w": "SLOW", + "delayMs": 9520 + } + ] + }, + { + "i": 5, + "startSec": 10.04, + "endSec": 13.64, + "cueDelayMs": 10040, + "cueDurMs": 3600, + "words": [ + { + "w": "IS", + "delayMs": 10040 + }, + { + "w": "SMOOTH", + "delayMs": 10240 + }, + { + "w": "AND", + "delayMs": 10880 + }, + { + "w": "SMOOTH", + "delayMs": 11240 + }, + { + "w": "IS", + "delayMs": 11680 + }, + { + "w": "FAST", + "delayMs": 11960 + }, + { + "w": "CHEAPEST", + "delayMs": 12720 + }, + { + "w": "LESSON", + "delayMs": 13280 + } + ] + }, + { + "i": 6, + "startSec": 13.64, + "endSec": 14.56, + "cueDelayMs": 13640, + "cueDurMs": 920, + "words": [ + { + "w": "IN", + "delayMs": 13640 + }, + { + "w": "THIS", + "delayMs": 13760 + }, + { + "w": "TRADE.", + "delayMs": 13960 + } + ] + } + ] +} diff --git a/refs/html/template-036/recipe.md b/refs/html/template-036/recipe.md new file mode 100644 index 0000000..5adb948 --- /dev/null +++ b/refs/html/template-036/recipe.md @@ -0,0 +1,62 @@ +# RECIPE — template-036 (9:16 · 736×1312 @ 30fps) — Monogram + +RESOLUTION: px below are authored at the source template's own canvas, 480×864; the compiled module +scales them per run (`pipeline/recipes/template-lib.ts`, `canvasFor`): portrait SCALE = W/480 for sizes +and horizontal offsets, H/864 for vertical anchors; landscape SCALE = (H/864)×1.6. `em` values and ms +timings never scale; the manifest carries the run's real W/H. The reference 9:16 canvas 736×1312 is +SCALE 1.533. The shell (reset · body · .vid · cueWin/.cue · .pg with pgOn/pgOff/pgMid) is the shared +one in `template-lib.ts` `docShell`; the SKELETON lists what this recipe adds. + +## 1. IDENTITY + +Cream IBM Plex Mono caps in a left-aligned block just below centre, wide tracking and open leading, +words rising 7px in; the beat's keyword flips to rust on a cream chip in-line, and the beat's strongest +word leaves the caption as huge dark-plum Bodoni Moda wall type across the top of the frame (drawn on +top; the source composited it behind the presenter). + +## 2. SKELETON + +``` +fonts: IBM+Plex+Mono:wght@400;700 · Bodoni+Moda:wght@900 +.cap { left:40px; top:544px; width:384px; left; 'IBM Plex Mono' 400; letter-spacing:.09em; line-height:1.45; color:#F6EDDF; + text-shadow:0 3px 14px rgba(4,2,4,.38), 0 2px 3px rgba(4,2,4,.3) } +.w { inline-block; pre; opacity:0; animation:wIn 260ms cubic-bezier(.2,.7,.3,1) forwards } @keyframes wIn { from{opacity:0; translateY(7px)} } +.pve { 700; color:#B25430; background:#F6EDDF; border-radius:3px; line-height:1; padding:.13em .2em .03em; margin:0 -.06em; text-shadow:none } +.mon { absolute; z-index:3; pre; opacity:0; animation:cueWin linear forwards } +.moni { inline-block; opacity:0; 'Bodoni Moda' 900; line-height:1; letter-spacing:.015em; padding:.06em 0 .18em; color:#25192B; + animation:monIn 540ms cubic-bezier(.16,.84,.3,1) both } @keyframes monIn { from{opacity:0; translateY(20px)} } +``` + +## 3. PER-BEAT ASSEMBLY + +- Caption 30px; advance 0.69em → maxChars = floor(384 / (0.69 × 30)) = 18 per line, 2 lines a page (cap + 2×maxChars − 2, 8 units); overflow → demote ×0.92. +- Wall type at left 38 / top 128: 132px Bodoni, advance 0.62em, shrunk to fit 404px; window from the + word's delay to the beat's end. + +## 4. WORDS + TIMING + +One span per word (glyph spans where the recipe types), `animation-delay` = `delayMs` VERBATIM; the cue gets `{cueDelayMs}` and a window running to the next beat's start (the last beat to the video's end). Mid-beat pages switch at the successor's first word. + +## 5. EMPHASIS + +Wall = `accentIndex` of beats with ≥ 4 units, REMOVED from the caption, trailing punctuation dropped. Chip = `accentIndex` of the remaining caption units. + +## BOUNDED VARIETY + +None. + +## 6. VERIFY LOOP + +``` +{repo}/.veed-engine/veed-engine-cli {repo}/runs//final --verify +``` +bounds on a `b{N}…` id → the runner demotes that beat one row (font × 0.92, capacities recomputed) and +regenerates; ≤ 2 cycles → `--record`. Manifest: `{"render":{"width":W,"height":H,"fps":FPS,"duration":DUR}}`. +Ids sit on the element that wraps the text; demotion is keyed per beat. + +## 7. DO NOT + +No fonts, colours or keyframes beyond this sheet; no invented timing; no reading the frames; no depth +compositing (layers the source composited behind the presenter are drawn on top here); never hand-edit +the output. diff --git a/refs/html/template-036/recipe.ts b/refs/html/template-036/recipe.ts new file mode 100644 index 0000000..03fe723 --- /dev/null +++ b/refs/html/template-036/recipe.ts @@ -0,0 +1,102 @@ +/** + * template-036 "Monogram": cream IBM Plex Mono caps in a left-aligned block below centre, words rising + * in; the beat's keyword flips to rust on a cream chip in-line, and the beat's strongest word lifts + * out of the caption as huge dark-plum Bodoni wall type at the top. The source composited the wall + * type behind the presenter; here it sits on top. + */ +import { accentIndex } from '../../../pipeline/recipes/lib.ts'; +import { + type Unit, + DEMOTE_STEP, + canvasFor, + cueDiv, + docShell, + eachBeat, + escapeHtml, + pageGateStyle, + paginate, + templateRecipe, + unitDelay, + unitText, + wrapByChars, +} from '../../../pipeline/recipes/template-lib.ts'; + +const FONT = 30; +const AVG_EM = 0.69; // Plex Mono 0.6em + 0.09em tracking +const LEFT = 40; +const TOP = 544; +const WIDTH = 384; +const MAX_LINES = 2; +const WALL_FONT = 132; +const WALL_EM = 0.8; // Bodoni Moda 900 caps +const WALL_LEFT = 38; +const WALL_TOP = 128; +const WALL_MIN_UNITS = 4; + +export default templateRecipe('template-036', (meta, timings, opts) => { + const c = canvasFor(meta); + let wordSeq = 0; + + const cues = eachBeat(meta, timings, opts, 'uppercase', (b) => { + const wallIdx = b.units.length >= WALL_MIN_UNITS ? accentIndex(b.units) : -1; + const wall = wallIdx >= 0 ? b.units[wallIdx] : null; + const capUnits = wall ? b.units.filter((u) => u !== wall) : b.units; + const chip = capUnits.length ? capUnits[accentIndex(capUnits)] : null; + let wallHtml = ''; + if (wall) { + const text = unitText(wall).replace(/[.,!?;:]+$/, ''); + const font = Math.min(WALL_FONT, (c.refWidth - 2 * WALL_LEFT) / (WALL_EM * text.length)) * Math.pow(DEMOTE_STEP, b.rows); + const start = unitDelay(wall); + wallHtml = `
${escapeHtml(text)}
\n`; + } + let rows = b.rows; + let f = 1; + let maxChars = 0; + let pages: Unit[][] = []; + let pageLines: Unit[][][] = []; + for (; rows <= b.rows + 4; rows++) { + f = Math.pow(DEMOTE_STEP, rows); + maxChars = Math.floor(WIDTH / (AVG_EM * FONT * f)); + pages = capUnits.length ? paginate(capUnits, maxChars * MAX_LINES - 2, 8) : []; + pageLines = pages.map((p) => wrapByChars(p, maxChars)); + if (pageLines.every((ls) => ls.length <= MAX_LINES)) break; + } + const fs = Math.round(FONT * f * c.s); + const pageDivs = pages.map((page, pi) => { + const pageId = `b${b.n}p${pi + 1}`; + const firstDelay = unitDelay(page[0]); + const nextStart = pi + 1 < pages.length ? unitDelay(pages[pi + 1][0]) : null; + const lines = pageLines[pi].map((line, li) => { + const spans = line.map((u) => u.spans.map((s) => { + wordSeq++; + return `${escapeHtml(s.text)}`; + }).join('')); + return `
${spans.join(' ')}
`; + }).join('\n '); + return `
\n ${lines}\n
`; + }); + return cueDiv(b.n, b.cueDelayMs, b.winMs, wallHtml + pageDivs.join('\n')); + }); + + const css = ` + .cap { left:${c.px(LEFT)}px; top:${c.py(TOP)}px; width:${c.px(WIDTH)}px; text-align:left; z-index:2; + font-family:'IBM Plex Mono'; font-weight:400; letter-spacing:0.09em; line-height:1.45; color:#F6EDDF; + text-shadow:0 ${c.px(3)}px ${c.px(14)}px rgba(4,2,4,0.38), 0 ${c.px(2)}px ${c.px(3)}px rgba(4,2,4,0.3); } + .cl { display:block; white-space:pre; } + .w { display:inline-block; white-space:pre; opacity:0; animation-name:wIn; animation-duration:260ms; animation-fill-mode:forwards; animation-timing-function:cubic-bezier(.2,.7,.3,1); } + .sp { display:inline-block; white-space:pre; } + .pve { font-weight:700; color:#B25430; background:#F6EDDF; border-radius:${c.px(3)}px; line-height:1; padding:0.13em 0.20em 0.03em; margin:0 -0.06em; text-shadow:none; } + @keyframes wIn { from{opacity:0; transform:translateY(${c.px(7)}px)} to{opacity:1; transform:translateY(0)} } + .mon { position:absolute; z-index:3; white-space:pre; opacity:0; animation:cueWin linear forwards; } + .moni { display:inline-block; opacity:0; font-family:'Bodoni Moda'; font-weight:900; line-height:1; letter-spacing:0.015em; padding:0.06em 0 0.18em; color:#25192B; + animation-name:monIn; animation-duration:540ms; animation-fill-mode:both; animation-timing-function:cubic-bezier(.16,.84,.3,1); } + @keyframes monIn { from{opacity:0; transform:translateY(${c.px(20)}px)} to{opacity:1; transform:translateY(0)} }`; + + return docShell({ + canvas: c, + videoPath: meta.videoPath, + fonts: ['IBM+Plex+Mono:wght@400;700', 'Bodoni+Moda:wght@900'], + css, + body: cues.join('\n'), + }); +}); diff --git a/refs/html/template-036/template.wv b/refs/html/template-036/template.wv new file mode 100644 index 0000000..78ca64e --- /dev/null +++ b/refs/html/template-036/template.wv @@ -0,0 +1,82 @@ + + + + + + +
+
USES
+
+
EVERY TOOL
+
YOUR TEAM
+
+
+
+
COMPETITORS
+
+
YOUR USE TOO,
+
+
+
+
TOOLS
+
+
SO THE ARE
+
NOT THE EDGE
+
+
+
+
EVERYTHING
+
+
WHEN EVERYONE
+
CAN MAKE TASTE
+
+
+
+
BECOMES
+
+
THE ONLY
+
REAL MOAT.
+
+
+
+
PEOPLE
+
+
HIRE WHO CAN TELL
+
+
+
+
ALMOST
+
+
GOOD FROM GOOD
+
+
+
+
BUSINESS
+
+
THAT GAP IS
+
THE WHOLE
+
+
diff --git a/refs/html/template-036/transcript.json b/refs/html/template-036/transcript.json new file mode 100644 index 0000000..8d64667 --- /dev/null +++ b/refs/html/template-036/transcript.json @@ -0,0 +1,371 @@ +{ + "text": "EVERY TOOL YOUR TEAM USES, YOUR COMPETITORS USE TOO, SO THE TOOLS ARE NOT THE EDGE WHEN EVERYONE CAN MAKE EVERYTHING, TASTE BECOMES THE ONLY REAL MOAT. HIRE PEOPLE WHO CAN TELL GOOD FROM ALMOST GOOD THAT GAP IS THE WHOLE BUSINESS.", + "chunks": [ + { + "text": "EVERY TOOL YOUR TEAM USES,", + "timestamp": [ + 0.16, + 1.743 + ], + "words": [ + { + "text": "EVERY", + "timestamp": [ + 0.16, + 0.32 + ] + }, + { + "text": "TOOL", + "timestamp": [ + 0.32, + 0.56 + ] + }, + { + "text": "YOUR", + "timestamp": [ + 0.56, + 0.76 + ] + }, + { + "text": "TEAM", + "timestamp": [ + 0.76, + 1 + ] + }, + { + "text": "USES,", + "timestamp": [ + 1, + 1.743 + ] + } + ] + }, + { + "text": "YOUR COMPETITORS USE TOO,", + "timestamp": [ + 1.76, + 4.303 + ], + "words": [ + { + "text": "YOUR", + "timestamp": [ + 1.76, + 2.08 + ] + }, + { + "text": "COMPETITORS", + "timestamp": [ + 2.08, + 2.68 + ] + }, + { + "text": "USE", + "timestamp": [ + 2.68, + 2.92 + ] + }, + { + "text": "TOO,", + "timestamp": [ + 2.92, + 4.303 + ] + } + ] + }, + { + "text": "SO THE TOOLS ARE NOT THE EDGE", + "timestamp": [ + 4.32, + 6.143 + ], + "words": [ + { + "text": "SO", + "timestamp": [ + 4.32, + 4.56 + ] + }, + { + "text": "THE", + "timestamp": [ + 4.56, + 4.68 + ] + }, + { + "text": "TOOLS", + "timestamp": [ + 4.68, + 5 + ] + }, + { + "text": "ARE", + "timestamp": [ + 5, + 5.16 + ] + }, + { + "text": "NOT", + "timestamp": [ + 5.16, + 5.32 + ] + }, + { + "text": "THE", + "timestamp": [ + 5.32, + 5.48 + ] + }, + { + "text": "EDGE", + "timestamp": [ + 5.48, + 6.143 + ] + } + ] + }, + { + "text": "WHEN EVERYONE CAN MAKE EVERYTHING, TASTE", + "timestamp": [ + 6.16, + 8.84 + ], + "words": [ + { + "text": "WHEN", + "timestamp": [ + 6.16, + 6.48 + ] + }, + { + "text": "EVERYONE", + "timestamp": [ + 6.48, + 6.76 + ] + }, + { + "text": "CAN", + "timestamp": [ + 6.76, + 7 + ] + }, + { + "text": "MAKE", + "timestamp": [ + 7, + 7.2 + ] + }, + { + "text": "EVERYTHING,", + "timestamp": [ + 7.2, + 8.32 + ] + }, + { + "text": "TASTE", + "timestamp": [ + 8.32, + 8.84 + ] + } + ] + }, + { + "text": "BECOMES THE ONLY REAL MOAT.", + "timestamp": [ + 8.84, + 10.383 + ], + "words": [ + { + "text": "BECOMES", + "timestamp": [ + 8.84, + 9.16 + ] + }, + { + "text": "THE", + "timestamp": [ + 9.16, + 9.32 + ] + }, + { + "text": "ONLY", + "timestamp": [ + 9.32, + 9.52 + ] + }, + { + "text": "REAL", + "timestamp": [ + 9.52, + 9.8 + ] + }, + { + "text": "MOAT.", + "timestamp": [ + 9.8, + 10.383 + ] + } + ] + }, + { + "text": "HIRE PEOPLE WHO CAN TELL", + "timestamp": [ + 10.4, + 11.743 + ], + "words": [ + { + "text": "HIRE", + "timestamp": [ + 10.4, + 10.84 + ] + }, + { + "text": "PEOPLE", + "timestamp": [ + 10.84, + 11.16 + ] + }, + { + "text": "WHO", + "timestamp": [ + 11.16, + 11.4 + ] + }, + { + "text": "CAN", + "timestamp": [ + 11.4, + 11.56 + ] + }, + { + "text": "TELL", + "timestamp": [ + 11.56, + 11.743 + ] + } + ] + }, + { + "text": "GOOD FROM ALMOST GOOD", + "timestamp": [ + 11.76, + 13.24 + ], + "words": [ + { + "text": "GOOD", + "timestamp": [ + 11.76, + 12.04 + ] + }, + { + "text": "FROM", + "timestamp": [ + 12.04, + 12.28 + ] + }, + { + "text": "ALMOST", + "timestamp": [ + 12.28, + 12.64 + ] + }, + { + "text": "GOOD", + "timestamp": [ + 12.64, + 13.24 + ] + } + ] + }, + { + "text": "THAT GAP IS THE WHOLE BUSINESS.", + "timestamp": [ + 13.84, + 15.633 + ], + "words": [ + { + "text": "THAT", + "timestamp": [ + 13.84, + 14.12 + ] + }, + { + "text": "GAP", + "timestamp": [ + 14.12, + 14.4 + ] + }, + { + "text": "IS", + "timestamp": [ + 14.4, + 14.6 + ] + }, + { + "text": "THE", + "timestamp": [ + 14.6, + 14.76 + ] + }, + { + "text": "WHOLE", + "timestamp": [ + 14.76, + 15 + ] + }, + { + "text": "BUSINESS.", + "timestamp": [ + 15, + 15.633 + ] + } + ] + } + ] +} diff --git a/refs/html/template-036/word-timings.json b/refs/html/template-036/word-timings.json new file mode 100644 index 0000000..71cc2dd --- /dev/null +++ b/refs/html/template-036/word-timings.json @@ -0,0 +1,244 @@ +{ + "beats": [ + { + "i": 1, + "startSec": 0.16, + "endSec": 1.743, + "cueDelayMs": 160, + "cueDurMs": 1583, + "words": [ + { + "w": "EVERY", + "delayMs": 160 + }, + { + "w": "TOOL", + "delayMs": 320 + }, + { + "w": "YOUR", + "delayMs": 560 + }, + { + "w": "TEAM", + "delayMs": 760 + }, + { + "w": "USES,", + "delayMs": 1000 + } + ] + }, + { + "i": 2, + "startSec": 1.76, + "endSec": 4.303, + "cueDelayMs": 1760, + "cueDurMs": 2543, + "words": [ + { + "w": "YOUR", + "delayMs": 1760 + }, + { + "w": "COMPETITORS", + "delayMs": 2080 + }, + { + "w": "USE", + "delayMs": 2680 + }, + { + "w": "TOO,", + "delayMs": 2920 + } + ] + }, + { + "i": 3, + "startSec": 4.32, + "endSec": 6.143, + "cueDelayMs": 4320, + "cueDurMs": 1823, + "words": [ + { + "w": "SO", + "delayMs": 4320 + }, + { + "w": "THE", + "delayMs": 4560 + }, + { + "w": "TOOLS", + "delayMs": 4680 + }, + { + "w": "ARE", + "delayMs": 5000 + }, + { + "w": "NOT", + "delayMs": 5160 + }, + { + "w": "THE", + "delayMs": 5320 + }, + { + "w": "EDGE", + "delayMs": 5480 + } + ] + }, + { + "i": 4, + "startSec": 6.16, + "endSec": 8.84, + "cueDelayMs": 6160, + "cueDurMs": 2680, + "words": [ + { + "w": "WHEN", + "delayMs": 6160 + }, + { + "w": "EVERYONE", + "delayMs": 6480 + }, + { + "w": "CAN", + "delayMs": 6760 + }, + { + "w": "MAKE", + "delayMs": 7000 + }, + { + "w": "EVERYTHING,", + "delayMs": 7200 + }, + { + "w": "TASTE", + "delayMs": 8320 + } + ] + }, + { + "i": 5, + "startSec": 8.84, + "endSec": 10.383, + "cueDelayMs": 8840, + "cueDurMs": 1543, + "words": [ + { + "w": "BECOMES", + "delayMs": 8840 + }, + { + "w": "THE", + "delayMs": 9160 + }, + { + "w": "ONLY", + "delayMs": 9320 + }, + { + "w": "REAL", + "delayMs": 9520 + }, + { + "w": "MOAT.", + "delayMs": 9800 + } + ] + }, + { + "i": 6, + "startSec": 10.4, + "endSec": 11.743, + "cueDelayMs": 10400, + "cueDurMs": 1343, + "words": [ + { + "w": "HIRE", + "delayMs": 10400 + }, + { + "w": "PEOPLE", + "delayMs": 10840 + }, + { + "w": "WHO", + "delayMs": 11160 + }, + { + "w": "CAN", + "delayMs": 11400 + }, + { + "w": "TELL", + "delayMs": 11560 + } + ] + }, + { + "i": 7, + "startSec": 11.76, + "endSec": 13.24, + "cueDelayMs": 11760, + "cueDurMs": 1480, + "words": [ + { + "w": "GOOD", + "delayMs": 11760 + }, + { + "w": "FROM", + "delayMs": 12040 + }, + { + "w": "ALMOST", + "delayMs": 12280 + }, + { + "w": "GOOD", + "delayMs": 12640 + } + ] + }, + { + "i": 8, + "startSec": 13.84, + "endSec": 15.633, + "cueDelayMs": 13840, + "cueDurMs": 1793, + "words": [ + { + "w": "THAT", + "delayMs": 13840 + }, + { + "w": "GAP", + "delayMs": 14120 + }, + { + "w": "IS", + "delayMs": 14400 + }, + { + "w": "THE", + "delayMs": 14600 + }, + { + "w": "WHOLE", + "delayMs": 14760 + }, + { + "w": "BUSINESS.", + "delayMs": 15000 + } + ] + } + ] +} diff --git a/refs/html/template-037/recipe.md b/refs/html/template-037/recipe.md new file mode 100644 index 0000000..181bc08 --- /dev/null +++ b/refs/html/template-037/recipe.md @@ -0,0 +1,62 @@ +# RECIPE — template-037 (9:16 · 736×1312 @ 30fps) — Interrupt + +RESOLUTION: px below are authored at the source template's own canvas, 480×864; the compiled module +scales them per run (`pipeline/recipes/template-lib.ts`, `canvasFor`): portrait SCALE = W/480 for sizes +and horizontal offsets, H/864 for vertical anchors; landscape SCALE = (H/864)×1.6. `em` values and ms +timings never scale; the manifest carries the run's real W/H. The reference 9:16 canvas 736×1312 is +SCALE 1.533. The shell (reset · body · .vid · cueWin/.cue · .pg with pgOn/pgOff/pgMid) is the shared +one in `template-lib.ts` `docShell`; the SKELETON lists what this recipe adds. + +## 1. IDENTITY + +A poster per page. Cool-white Plex Mono sentence words rise in on a line above and a line below; the +beat's strongest word interrupts between them as huge dark Anton knocked out of a sharp colour block +that wipes open from the page's own side. Pages alternate left / right by beat (and within a beat); the +block is orange-red, ice-blue on every third beat. + +## 2. SKELETON + +``` +fonts: Anton · IBM+Plex+Mono:wght@600 +.ml { absolute; left:34px; width:412px; z-index:2; 'IBM Plex Mono' 600; letter-spacing:.08em; line-height:1.3; color:#E6F1F7; nowrap; + text-shadow:0 2px 9px rgba(11,11,14,.52), 0 1px 2px rgba(11,11,14,.4) } +.w { inline-block; opacity:0; animation:wIn 220ms cubic-bezier(.2,.7,.3,1) both } @keyframes wIn { 0%{opacity:0; translateY(9px)} } +.hero { absolute; left:34px; width:412px; z-index:3 } .sw { inline-block; padding:2px 14px } +.wipeL / .wipeR { clip-path polygon from the side; animation 190ms cubic-bezier(.16,1,.3,1) both } +.sRed { background:#FF2D00 } .sIce { background:#00E5FF } +.hw { inline-block; 'Anton'; line-height:.8; letter-spacing:.01em; padding:.08em 0 .06em; color:#0B0B0E; nowrap } +``` + +## 3. PER-BEAT ASSEMBLY + +- Line A at top 520, hero at 561, line B at 690. Sentence 18px, advance 0.68em → each line ≤ floor(412 / + (0.68 × 18)) = 33 chars; a page takes up to 2×maxChars chars / 9 units and splits at its own hero; + if either line overflows → demote ×0.92 (5 rows). +- Hero 114px Anton, advance 0.46em, shrunk so word + 28px padding ≤ 412px. + +## 4. WORDS + TIMING + +One span per word (glyph spans where the recipe types), `animation-delay` = `delayMs` VERBATIM; the cue gets `{cueDelayMs}` and a window running to the next beat's start (the last beat to the video's end). Mid-beat pages switch at the successor's first word. + +## 5. EMPHASIS + +Hero = `accentIndex` of each PAGE (the page's words before it go above, after it below), trailing punctuation dropped. + +## BOUNDED VARIETY + +Side and colour cycles only. + +## 6. VERIFY LOOP + +``` +{repo}/.veed-engine/veed-engine-cli {repo}/runs//final --verify +``` +bounds on a `b{N}…` id → the runner demotes that beat one row (font × 0.92, capacities recomputed) and +regenerates; ≤ 2 cycles → `--record`. Manifest: `{"render":{"width":W,"height":H,"fps":FPS,"duration":DUR}}`. +Ids sit on the element that wraps the text; demotion is keyed per beat. + +## 7. DO NOT + +No fonts, colours or keyframes beyond this sheet; no invented timing; no reading the frames; no depth +compositing (layers the source composited behind the presenter are drawn on top here); never hand-edit +the output. diff --git a/refs/html/template-037/recipe.ts b/refs/html/template-037/recipe.ts new file mode 100644 index 0000000..5bf6930 --- /dev/null +++ b/refs/html/template-037/recipe.ts @@ -0,0 +1,105 @@ +/** + * template-037 "Interrupt": a poster per page. Cool-white Plex Mono sentence words rise in above and + * below; the beat's strongest word interrupts between them as huge dark Anton knocked out of a sharp + * colour block that wipes open from the page's side. Pages alternate left / right; the block is red, + * ice-blue every third beat. The source composited nothing behind the presenter here. + */ +import { accentIndex } from '../../../pipeline/recipes/lib.ts'; +import { + type Unit, + DEMOTE_STEP, + canvasFor, + cueDiv, + docShell, + eachBeat, + escapeHtml, + pageGateStyle, + paginate, + templateRecipe, + unitDelay, + unitText, +} from '../../../pipeline/recipes/template-lib.ts'; + +const FONT = 18; +const AVG_EM = 0.68; // Plex Mono 0.6em + 0.08em tracking +const LEFT = 34; +const WIDTH = 412; +const TOP_A = 520; +const TOP_HERO = 561; +const TOP_B = 690; +const HERO_FONT = 114; +const HERO_EM = 0.5; // Anton caps +const HERO_PAD = 14; + +const lineChars = (l: Unit[]) => l.reduce((a, u) => a + u.chars, 0) + Math.max(0, l.length - 1); + +export default templateRecipe('template-037', (meta, timings, opts) => { + const c = canvasFor(meta); + let wordSeq = 0; + + const cues = eachBeat(meta, timings, opts, 'uppercase', (b) => { + let rows = b.rows; + let f = 1; + let maxChars = 0; + let pages: Unit[][] = []; + for (; rows <= b.rows + 5; rows++) { + f = Math.pow(DEMOTE_STEP, rows); + maxChars = Math.floor(WIDTH / (AVG_EM * FONT * f)); + pages = paginate(b.units, maxChars * 2, 9); + // every page splits at its hero into ONE line above and ONE below + const fits = pages.every((p) => { + const h = accentIndex(p); + return lineChars(p.slice(0, h)) <= maxChars && lineChars(p.slice(h + 1)) <= maxChars; + }); + if (fits) break; + } + const fs = Math.round(FONT * f * c.s); + const side = b.n % 2 === 1 ? 'left' : 'right'; + const colour = b.n % 3 === 0 ? 'sIce' : 'sRed'; + + const pageDivs = pages.map((page, pi) => { + const pageId = `b${b.n}p${pi + 1}`; + const firstDelay = unitDelay(page[0]); + const nextStart = pi + 1 < pages.length ? unitDelay(pages[pi + 1][0]) : null; + const h = accentIndex(page); + const hero = page[h]; + const heroText = unitText(hero).replace(/[.,!?;:]+$/, ''); + const heroFont = Math.min(HERO_FONT, (WIDTH - 2 * HERO_PAD) / (HERO_EM * heroText.length)) * f; + const pageSide = pi % 2 === 0 ? side : side === 'left' ? 'right' : 'left'; + const line = (units: Unit[], id: string, top: number) => units.length + ? `
${units.map((u) => u.spans.map((s) => { + wordSeq++; + return `${escapeHtml(s.text)}`; + }).join('')).join(' ')}
` + : ''; + const heroDiv = `
${escapeHtml(heroText)}
`; + return `
\n ${line(page.slice(0, h), `${pageId}a`, TOP_A)}\n ${heroDiv}\n ${line(page.slice(h + 1), `${pageId}b`, TOP_B)}\n
`; + }); + return cueDiv(b.n, b.cueDelayMs, b.winMs, pageDivs.join('\n')); + }); + + const css = ` + .poster { inset:0; } + .ml { position:absolute; left:${c.px(LEFT)}px; width:${c.px(WIDTH)}px; z-index:2; font-family:'IBM Plex Mono'; font-weight:600; + letter-spacing:0.08em; line-height:1.3; color:#E6F1F7; white-space:nowrap; + text-shadow:0 ${c.px(2)}px ${c.px(9)}px rgba(11,11,14,0.52), 0 ${c.px(1)}px ${c.px(2)}px rgba(11,11,14,0.40); } + .w { display:inline-block; opacity:0; animation-name:wIn; animation-duration:220ms; animation-timing-function:cubic-bezier(.2,.7,.3,1); animation-fill-mode:both; } + @keyframes wIn { 0%{opacity:0; transform:translateY(${c.px(9)}px)} 100%{opacity:1; transform:translateY(0)} } + .hero { position:absolute; left:${c.px(LEFT)}px; width:${c.px(WIDTH)}px; z-index:3; } + .sw { display:inline-block; padding:${c.px(2)}px ${c.px(HERO_PAD)}px; } + .wipeL { clip-path:polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%); animation-name:wipeL; animation-duration:190ms; animation-timing-function:cubic-bezier(.16,1,.3,1); animation-fill-mode:both; } + .wipeR { clip-path:polygon(100% 0%, 100% 0%, 100% 100%, 100% 100%); animation-name:wipeR; animation-duration:190ms; animation-timing-function:cubic-bezier(.16,1,.3,1); animation-fill-mode:both; } + @keyframes wipeL { 0%{clip-path:polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%)} 100%{clip-path:polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)} } + @keyframes wipeR { 0%{clip-path:polygon(100% 0%, 100% 0%, 100% 100%, 100% 100%)} 100%{clip-path:polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)} } + .sRed { background:#FF2D00; } + .sIce { background:#00E5FF; } + .hw { display:inline-block; font-family:'Anton'; font-weight:400; line-height:0.8; letter-spacing:0.01em; padding:0.08em 0 0.06em; color:#0B0B0E; white-space:nowrap; }`; + + return docShell({ + canvas: c, + videoPath: meta.videoPath, + fonts: ['Anton', 'IBM+Plex+Mono:wght@600'], + css, + body: cues.join('\n'), + }); +}); diff --git a/refs/html/template-037/template.wv b/refs/html/template-037/template.wv new file mode 100644 index 0000000..f654aad --- /dev/null +++ b/refs/html/template-037/template.wv @@ -0,0 +1,81 @@ + + + + + + +
+
+
I HAVE WATCHED FIVE
+
TECHNOLOGY
+
WAVES HIT THIS INDUSTRY.
+
+
+
+
+
THIS ONE IS
+
DIFFERENT
+ +
+
+
+
+
THE SCARCE THING IS NO LONGER
+
PRODUCTION
+ +
+
+
+
+
IT IS
+
ATTENTION
+ +
+
+
+
+
AND IT
+
ALWAYS
+
WAS.
+
+
+
+
+
MAKE FEWER
+
THINGS
+
MAKE THEM MATTER
+
+
+
+
+
THAT
+
ADVICE
+
HAS NEVER LOST.
+
+
diff --git a/refs/html/template-037/transcript.json b/refs/html/template-037/transcript.json new file mode 100644 index 0000000..ba65399 --- /dev/null +++ b/refs/html/template-037/transcript.json @@ -0,0 +1,334 @@ +{ + "text": "I HAVE WATCHED FIVE TECHNOLOGY WAVES HIT THIS INDUSTRY. THIS ONE IS DIFFERENT THE SCARCE THING IS NO LONGER PRODUCTION. IT IS ATTENTION AND IT ALWAYS WAS. MAKE FEWER THINGS, MAKE THEM MATTER THAT ADVICE HAS NEVER LOST.", + "chunks": [ + { + "text": "I HAVE WATCHED FIVE TECHNOLOGY WAVES HIT THIS INDUSTRY.", + "timestamp": [ + 0.16, + 3.44 + ], + "words": [ + { + "text": "I", + "timestamp": [ + 0.16, + 0.24 + ] + }, + { + "text": "HAVE", + "timestamp": [ + 0.24, + 0.36 + ] + }, + { + "text": "WATCHED", + "timestamp": [ + 0.36, + 0.72 + ] + }, + { + "text": "FIVE", + "timestamp": [ + 0.72, + 1 + ] + }, + { + "text": "TECHNOLOGY", + "timestamp": [ + 1, + 1.52 + ] + }, + { + "text": "WAVES", + "timestamp": [ + 1.52, + 1.96 + ] + }, + { + "text": "HIT", + "timestamp": [ + 1.96, + 2.2 + ] + }, + { + "text": "THIS", + "timestamp": [ + 2.2, + 2.4 + ] + }, + { + "text": "INDUSTRY.", + "timestamp": [ + 2.4, + 3.44 + ] + } + ] + }, + { + "text": "THIS ONE IS DIFFERENT", + "timestamp": [ + 3.44, + 4.8 + ], + "words": [ + { + "text": "THIS", + "timestamp": [ + 3.44, + 3.76 + ] + }, + { + "text": "ONE", + "timestamp": [ + 3.76, + 4 + ] + }, + { + "text": "IS", + "timestamp": [ + 4, + 4.2 + ] + }, + { + "text": "DIFFERENT", + "timestamp": [ + 4.2, + 4.8 + ] + } + ] + }, + { + "text": "THE SCARCE THING IS NO LONGER PRODUCTION.", + "timestamp": [ + 4.8, + 7.44 + ], + "words": [ + { + "text": "THE", + "timestamp": [ + 4.8, + 5.08 + ] + }, + { + "text": "SCARCE", + "timestamp": [ + 5.08, + 5.52 + ] + }, + { + "text": "THING", + "timestamp": [ + 5.52, + 5.72 + ] + }, + { + "text": "IS", + "timestamp": [ + 5.72, + 5.92 + ] + }, + { + "text": "NO", + "timestamp": [ + 5.92, + 6.12 + ] + }, + { + "text": "LONGER", + "timestamp": [ + 6.12, + 6.48 + ] + }, + { + "text": "PRODUCTION.", + "timestamp": [ + 6.48, + 7.44 + ] + } + ] + }, + { + "text": "IT IS ATTENTION", + "timestamp": [ + 7.44, + 9.04 + ], + "words": [ + { + "text": "IT", + "timestamp": [ + 7.44, + 7.72 + ] + }, + { + "text": "IS", + "timestamp": [ + 7.72, + 7.92 + ] + }, + { + "text": "ATTENTION", + "timestamp": [ + 7.92, + 9.04 + ] + } + ] + }, + { + "text": "AND IT ALWAYS WAS.", + "timestamp": [ + 9.04, + 10.48 + ], + "words": [ + { + "text": "AND", + "timestamp": [ + 9.04, + 9.32 + ] + }, + { + "text": "IT", + "timestamp": [ + 9.32, + 9.52 + ] + }, + { + "text": "ALWAYS", + "timestamp": [ + 9.52, + 9.8 + ] + }, + { + "text": "WAS.", + "timestamp": [ + 9.8, + 10.48 + ] + } + ] + }, + { + "text": "MAKE FEWER THINGS, MAKE THEM MATTER", + "timestamp": [ + 10.48, + 14.24 + ], + "words": [ + { + "text": "MAKE", + "timestamp": [ + 10.48, + 10.84 + ] + }, + { + "text": "FEWER", + "timestamp": [ + 10.84, + 11.24 + ] + }, + { + "text": "THINGS,", + "timestamp": [ + 11.24, + 12.4 + ] + }, + { + "text": "MAKE", + "timestamp": [ + 12.4, + 12.68 + ] + }, + { + "text": "THEM", + "timestamp": [ + 12.68, + 12.92 + ] + }, + { + "text": "MATTER", + "timestamp": [ + 12.92, + 14.24 + ] + } + ] + }, + { + "text": "THAT ADVICE HAS NEVER LOST.", + "timestamp": [ + 14.24, + 16.233 + ], + "words": [ + { + "text": "THAT", + "timestamp": [ + 14.24, + 14.56 + ] + }, + { + "text": "ADVICE", + "timestamp": [ + 14.56, + 15.08 + ] + }, + { + "text": "HAS", + "timestamp": [ + 15.08, + 15.32 + ] + }, + { + "text": "NEVER", + "timestamp": [ + 15.32, + 15.56 + ] + }, + { + "text": "LOST.", + "timestamp": [ + 15.56, + 16.233 + ] + } + ] + } + ] +} diff --git a/refs/html/template-037/word-timings.json b/refs/html/template-037/word-timings.json new file mode 100644 index 0000000..ab7bf2f --- /dev/null +++ b/refs/html/template-037/word-timings.json @@ -0,0 +1,219 @@ +{ + "beats": [ + { + "i": 1, + "startSec": 0.16, + "endSec": 3.44, + "cueDelayMs": 160, + "cueDurMs": 3280, + "words": [ + { + "w": "I", + "delayMs": 160 + }, + { + "w": "HAVE", + "delayMs": 240 + }, + { + "w": "WATCHED", + "delayMs": 360 + }, + { + "w": "FIVE", + "delayMs": 720 + }, + { + "w": "TECHNOLOGY", + "delayMs": 1000 + }, + { + "w": "WAVES", + "delayMs": 1520 + }, + { + "w": "HIT", + "delayMs": 1960 + }, + { + "w": "THIS", + "delayMs": 2200 + }, + { + "w": "INDUSTRY.", + "delayMs": 2400 + } + ] + }, + { + "i": 2, + "startSec": 3.44, + "endSec": 4.8, + "cueDelayMs": 3440, + "cueDurMs": 1360, + "words": [ + { + "w": "THIS", + "delayMs": 3440 + }, + { + "w": "ONE", + "delayMs": 3760 + }, + { + "w": "IS", + "delayMs": 4000 + }, + { + "w": "DIFFERENT", + "delayMs": 4200 + } + ] + }, + { + "i": 3, + "startSec": 4.8, + "endSec": 7.44, + "cueDelayMs": 4800, + "cueDurMs": 2640, + "words": [ + { + "w": "THE", + "delayMs": 4800 + }, + { + "w": "SCARCE", + "delayMs": 5080 + }, + { + "w": "THING", + "delayMs": 5520 + }, + { + "w": "IS", + "delayMs": 5720 + }, + { + "w": "NO", + "delayMs": 5920 + }, + { + "w": "LONGER", + "delayMs": 6120 + }, + { + "w": "PRODUCTION.", + "delayMs": 6480 + } + ] + }, + { + "i": 4, + "startSec": 7.44, + "endSec": 9.04, + "cueDelayMs": 7440, + "cueDurMs": 1600, + "words": [ + { + "w": "IT", + "delayMs": 7440 + }, + { + "w": "IS", + "delayMs": 7720 + }, + { + "w": "ATTENTION", + "delayMs": 7920 + } + ] + }, + { + "i": 5, + "startSec": 9.04, + "endSec": 10.48, + "cueDelayMs": 9040, + "cueDurMs": 1440, + "words": [ + { + "w": "AND", + "delayMs": 9040 + }, + { + "w": "IT", + "delayMs": 9320 + }, + { + "w": "ALWAYS", + "delayMs": 9520 + }, + { + "w": "WAS.", + "delayMs": 9800 + } + ] + }, + { + "i": 6, + "startSec": 10.48, + "endSec": 14.24, + "cueDelayMs": 10480, + "cueDurMs": 3760, + "words": [ + { + "w": "MAKE", + "delayMs": 10480 + }, + { + "w": "FEWER", + "delayMs": 10840 + }, + { + "w": "THINGS,", + "delayMs": 11240 + }, + { + "w": "MAKE", + "delayMs": 12400 + }, + { + "w": "THEM", + "delayMs": 12680 + }, + { + "w": "MATTER", + "delayMs": 12920 + } + ] + }, + { + "i": 7, + "startSec": 14.24, + "endSec": 16.233, + "cueDelayMs": 14240, + "cueDurMs": 1993, + "words": [ + { + "w": "THAT", + "delayMs": 14240 + }, + { + "w": "ADVICE", + "delayMs": 14560 + }, + { + "w": "HAS", + "delayMs": 15080 + }, + { + "w": "NEVER", + "delayMs": 15320 + }, + { + "w": "LOST.", + "delayMs": 15560 + } + ] + } + ] +} diff --git a/refs/html/template-038/recipe.md b/refs/html/template-038/recipe.md new file mode 100644 index 0000000..ee6cfa2 --- /dev/null +++ b/refs/html/template-038/recipe.md @@ -0,0 +1,61 @@ +# RECIPE — template-038 (9:16 · 736×1312 @ 30fps) — Ledgerboard + +RESOLUTION: px below are authored at the source template's own canvas, 480×864; the compiled module +scales them per run (`pipeline/recipes/template-lib.ts`, `canvasFor`): portrait SCALE = W/480 for sizes +and horizontal offsets, H/864 for vertical anchors; landscape SCALE = (H/864)×1.6. `em` values and ms +timings never scale; the manifest carries the run's real W/H. The reference 9:16 canvas 736×1312 is +SCALE 1.533. The shell (reset · body · .vid · cueWin/.cue · .pg with pgOn/pgOff/pgMid) is the shared +one in `template-lib.ts` `docShell`; the SKELETON lists what this recipe adds. + +## 1. IDENTITY + +Dark Plex Mono caps on sharp cream chips, one chip per word popping in, stacked left in a block low +in the frame; the beat's strongest word leaves the chips and lands as huge cream Anton wall type at the +top of the frame with a soft drop shadow (drawn on top; the source composited it behind the presenter). + +## 2. SKELETON + +``` +fonts: IBM+Plex+Mono:wght@600 · Anton +.blk { width:412px; z-index:2 } .ln { block; height:45px; line-height:45px } (second line margin-top:5px) +.w { inline-block; top; opacity:0; pre; 'IBM Plex Mono' 600; letter-spacing:.06em; line-height:1; color:#1A1512; background:#F0E6D2; + padding:9px 10px; margin-right:7px; box-shadow:0 3px 10px rgba(24,23,21,.32); animation:chipIn 190ms cubic-bezier(.2,.7,.3,1) both } +@keyframes chipIn { from{opacity:0; translateY(7px)} } +.wall { absolute; z-index:3; opacity:0; nowrap; line-height:1; padding:.1em 0 .15em; 'Anton'; letter-spacing:-.02em; color:#F0E6D2; + text-shadow:0 3px 22px rgba(24,23,21,.55), 0 1px 5px rgba(24,23,21,.4); animation:wallIn 340ms cubic-bezier(.16,1,.3,1) both } +@keyframes wallIn { from{opacity:0; translateY(18px)} } +``` + +## 3. PER-BEAT ASSEMBLY + +- Chip width = chars × 0.66em × 27px + 20px, 7px gap; lines wrap to 412px, 2 lines a page (cap 30 + chars, 8 units); overflow → demote ×0.92. Block left 34; top 609 for two lines, 634 for one. +- Wall type 225px Anton (advance 0.46em) shrunk to fit 432px; top 24 at 200px+, else 43; window from + the word's delay to the beat's end. + +## 4. WORDS + TIMING + +One span per word (glyph spans where the recipe types), `animation-delay` = `delayMs` VERBATIM; the cue gets `{cueDelayMs}` and a window running to the next beat's start (the last beat to the video's end). Mid-beat pages switch at the successor's first word. + +## 5. EMPHASIS + +Wall = `accentIndex` of beats with ≥ 2 units, REMOVED from the chips, trailing punctuation dropped. + +## BOUNDED VARIETY + +None. + +## 6. VERIFY LOOP + +``` +{repo}/.veed-engine/veed-engine-cli {repo}/runs//final --verify +``` +bounds on a `b{N}…` id → the runner demotes that beat one row (font × 0.92, capacities recomputed) and +regenerates; ≤ 2 cycles → `--record`. Manifest: `{"render":{"width":W,"height":H,"fps":FPS,"duration":DUR}}`. +Ids sit on the element that wraps the text; demotion is keyed per beat. + +## 7. DO NOT + +No fonts, colours or keyframes beyond this sheet; no invented timing; no reading the frames; no depth +compositing (layers the source composited behind the presenter are drawn on top here); never hand-edit +the output. diff --git a/refs/html/template-038/recipe.ts b/refs/html/template-038/recipe.ts new file mode 100644 index 0000000..c5e3da4 --- /dev/null +++ b/refs/html/template-038/recipe.ts @@ -0,0 +1,103 @@ +/** + * template-038 "Ledgerboard": dark Plex Mono caps on sharp cream chips, one chip per word popping in, + * in a left-aligned block low in the frame; the beat's strongest word leaves the chips and lands as + * huge cream Anton wall type at the top. The source composited the wall type behind the presenter; + * here it sits on top. + */ +import { accentIndex } from '../../../pipeline/recipes/lib.ts'; +import { + type Unit, + DEMOTE_STEP, + canvasFor, + cueDiv, + docShell, + eachBeat, + escapeHtml, + pageGateStyle, + paginate, + templateRecipe, + unitDelay, + unitText, + wrapByWidth, +} from '../../../pipeline/recipes/template-lib.ts'; + +const FONT = 27; +const AVG_EM = 0.66; // Plex Mono 0.6em + 0.06em tracking +const CHIP_PAD = 20; // 10px each side +const CHIP_GAP = 7; +const LEFT = 34; +const WIDTH = 412; +const TOP_2 = 609; // block top for two lines; one line sits centred on the same band +const LINE_H = 45; +const LINE_GAP = 5; +const MAX_LINES = 2; +const WALL_FONT = 225; +const WALL_EM = 0.5; // Anton caps +const WALL_LEFT = 24; + +export default templateRecipe('template-038', (meta, timings, opts) => { + const c = canvasFor(meta); + let wordSeq = 0; + + const cues = eachBeat(meta, timings, opts, 'uppercase', (b) => { + const wallIdx = b.units.length >= 2 ? accentIndex(b.units) : -1; + const wall = wallIdx >= 0 ? b.units[wallIdx] : null; + const capUnits = wall ? b.units.filter((u) => u !== wall) : b.units; + let wallHtml = ''; + if (wall) { + const text = unitText(wall).replace(/[.,!?;:]+$/, ''); + const font = Math.min(WALL_FONT, (c.refWidth - 2 * WALL_LEFT) / (WALL_EM * text.length)) * Math.pow(DEMOTE_STEP, b.rows); + const top = font >= 200 ? 24 : 43; + wallHtml = `
${escapeHtml(text)}
\n`; + } + let rows = b.rows; + let f = 1; + let pages: Unit[][] = []; + let pageLines: Unit[][][] = []; + const chipW = (u: Unit, f: number) => (u.chars * AVG_EM * FONT + CHIP_PAD) * f; + for (; rows <= b.rows + 4; rows++) { + f = Math.pow(DEMOTE_STEP, rows); + pages = capUnits.length ? paginate(capUnits, Math.floor(30 * f), 8) : []; + pageLines = pages.map((p) => wrapByWidth(p, WIDTH, (u) => chipW(u, f), CHIP_GAP)); + if (pageLines.every((ls) => ls.length <= MAX_LINES && ls.every((l) => l.reduce((a, u) => a + chipW(u, f), 0) + CHIP_GAP * (l.length - 1) <= WIDTH))) break; + } + const fs = Math.round(FONT * f * c.s); + const pageDivs = pages.map((page, pi) => { + const pageId = `b${b.n}p${pi + 1}`; + const firstDelay = unitDelay(page[0]); + const nextStart = pi + 1 < pages.length ? unitDelay(pages[pi + 1][0]) : null; + const lines = pageLines[pi]; + const top = lines.length === 1 ? TOP_2 + (LINE_H + LINE_GAP) / 2 : TOP_2; + const lineDivs = lines.map((line, li) => { + const spans = line.map((u) => u.spans.map((s) => { + wordSeq++; + return `${escapeHtml(s.text)}`; + }).join('')).join(''); + return `
${spans}
`; + }).join('\n '); + return `
\n ${lineDivs}\n
`; + }); + return cueDiv(b.n, b.cueDelayMs, b.winMs, wallHtml + pageDivs.join('\n')); + }); + + const css = ` + .blk { width:${c.px(WIDTH)}px; z-index:2; } + .ln { display:block; height:${c.px(LINE_H)}px; line-height:${c.px(LINE_H)}px; } + .w { display:inline-block; vertical-align:top; opacity:0; white-space:pre; font-family:'IBM Plex Mono'; font-weight:600; letter-spacing:0.06em; + line-height:1; color:#1A1512; background:#F0E6D2; padding:${c.px(9)}px ${c.px(10)}px; margin-right:${c.px(CHIP_GAP)}px; + box-shadow:0 ${c.px(3)}px ${c.px(10)}px rgba(24,23,21,0.32); + animation-name:chipIn; animation-duration:190ms; animation-timing-function:cubic-bezier(.2,.7,.3,1); animation-fill-mode:both; } + @keyframes chipIn { from{opacity:0; transform:translateY(${c.px(7)}px)} to{opacity:1; transform:translateY(0)} } + .wall { position:absolute; z-index:3; opacity:0; white-space:nowrap; line-height:1; padding:0.1em 0 0.15em; font-family:'Anton'; font-weight:400; letter-spacing:-0.02em; color:#F0E6D2; + text-shadow:0 ${c.px(3)}px ${c.px(22)}px rgba(24,23,21,0.55), 0 ${c.px(1)}px ${c.px(5)}px rgba(24,23,21,0.40); + animation-name:wallIn; animation-duration:340ms; animation-timing-function:cubic-bezier(.16,1,.3,1); animation-fill-mode:both; } + @keyframes wallIn { from{opacity:0; transform:translateY(${c.px(18)}px)} to{opacity:1; transform:translateY(0)} }`; + + return docShell({ + canvas: c, + videoPath: meta.videoPath, + fonts: ['IBM+Plex+Mono:wght@600', 'Anton'], + css, + body: cues.join('\n'), + }); +}); diff --git a/refs/html/template-038/template.wv b/refs/html/template-038/template.wv new file mode 100644 index 0000000..a7833b5 --- /dev/null +++ b/refs/html/template-038/template.wv @@ -0,0 +1,95 @@ + + + + + + +
+
BUILDING
+
+
MONTHTHREE
+
OFINPUBLIC
+
+
+
+
NUMBERS
+
+
HEREARETHE
+
+
+
+
NOBODY
+
+
POSTS
+
+
+
+
400
+
+
INREVENUE
+
+
+
+
200
+
+
USERS
+
+
+
+
DEFINITELY
+
+
ANDRENTIS
+
+
+
+
COVERED
+
+
NOT
+
+
+
+
CHURNS
+
+
BUTNOBODY
+
+
+
+
EVERYONE
+
+
WHOSIGNSUPSTAYS
+
+
+
+
THAT
+
+
ISWHYIAM
+
+
+
+
STILL
+
+
HERE
+
+
diff --git a/refs/html/template-038/transcript.json b/refs/html/template-038/transcript.json new file mode 100644 index 0000000..fce15d1 --- /dev/null +++ b/refs/html/template-038/transcript.json @@ -0,0 +1,370 @@ +{ + "text": "MONTH THREE OF BUILDING IN PUBLIC HERE ARE THE NUMBERS NOBODY POSTS 400 IN REVENUE 200 USERS AND RENT IS DEFINITELY NOT COVERED BUT NOBODY CHURNS EVERYONE WHO SIGNS UP STAYS THAT IS WHY I AM STILL HERE", + "chunks": [ + { + "text": "MONTH THREE OF BUILDING IN PUBLIC", + "timestamp": [ + 0.32, + 1.88 + ], + "words": [ + { + "text": "MONTH", + "timestamp": [ + 0.32, + 0.52 + ] + }, + { + "text": "THREE", + "timestamp": [ + 0.52, + 0.84 + ] + }, + { + "text": "OF", + "timestamp": [ + 0.84, + 1.12 + ] + }, + { + "text": "BUILDING", + "timestamp": [ + 1.12, + 1.4 + ] + }, + { + "text": "IN", + "timestamp": [ + 1.4, + 1.64 + ] + }, + { + "text": "PUBLIC", + "timestamp": [ + 1.64, + 1.88 + ] + } + ] + }, + { + "text": "HERE ARE THE NUMBERS", + "timestamp": [ + 1.88, + 2.64 + ], + "words": [ + { + "text": "HERE", + "timestamp": [ + 1.88, + 2.12 + ] + }, + { + "text": "ARE", + "timestamp": [ + 2.12, + 2.24 + ] + }, + { + "text": "THE", + "timestamp": [ + 2.24, + 2.36 + ] + }, + { + "text": "NUMBERS", + "timestamp": [ + 2.36, + 2.64 + ] + } + ] + }, + { + "text": "NOBODY POSTS", + "timestamp": [ + 2.64, + 3.68 + ], + "words": [ + { + "text": "NOBODY", + "timestamp": [ + 2.64, + 3.04 + ] + }, + { + "text": "POSTS", + "timestamp": [ + 3.04, + 3.68 + ] + } + ] + }, + { + "text": "400 IN REVENUE", + "timestamp": [ + 3.68, + 4.79 + ], + "words": [ + { + "text": "400", + "timestamp": [ + 3.68, + 4.16 + ] + }, + { + "text": "IN", + "timestamp": [ + 4.16, + 4.36 + ] + }, + { + "text": "REVENUE", + "timestamp": [ + 4.36, + 4.79 + ] + } + ] + }, + { + "text": "200 USERS", + "timestamp": [ + 4.79, + 5.56 + ], + "words": [ + { + "text": "200", + "timestamp": [ + 4.8, + 5.2 + ] + }, + { + "text": "USERS", + "timestamp": [ + 5.2, + 5.56 + ] + } + ] + }, + { + "text": "AND RENT IS DEFINITELY", + "timestamp": [ + 5.56, + 6.56 + ], + "words": [ + { + "text": "AND", + "timestamp": [ + 5.56, + 5.72 + ] + }, + { + "text": "RENT", + "timestamp": [ + 5.72, + 5.96 + ] + }, + { + "text": "IS", + "timestamp": [ + 5.96, + 6.12 + ] + }, + { + "text": "DEFINITELY", + "timestamp": [ + 6.12, + 6.56 + ] + } + ] + }, + { + "text": "NOT COVERED", + "timestamp": [ + 6.56, + 7.44 + ], + "words": [ + { + "text": "NOT", + "timestamp": [ + 6.56, + 6.8 + ] + }, + { + "text": "COVERED", + "timestamp": [ + 6.8, + 7.44 + ] + } + ] + }, + { + "text": "BUT NOBODY CHURNS", + "timestamp": [ + 7.44, + 8.68 + ], + "words": [ + { + "text": "BUT", + "timestamp": [ + 7.44, + 7.72 + ] + }, + { + "text": "NOBODY", + "timestamp": [ + 7.72, + 8.16 + ] + }, + { + "text": "CHURNS", + "timestamp": [ + 8.16, + 8.68 + ] + } + ] + }, + { + "text": "EVERYONE WHO SIGNS UP STAYS", + "timestamp": [ + 8.68, + 9.96 + ], + "words": [ + { + "text": "EVERYONE", + "timestamp": [ + 8.68, + 8.96 + ] + }, + { + "text": "WHO", + "timestamp": [ + 8.96, + 9.16 + ] + }, + { + "text": "SIGNS", + "timestamp": [ + 9.16, + 9.44 + ] + }, + { + "text": "UP", + "timestamp": [ + 9.44, + 9.6 + ] + }, + { + "text": "STAYS", + "timestamp": [ + 9.6, + 9.96 + ] + } + ] + }, + { + "text": "THAT IS WHY I AM", + "timestamp": [ + 9.96, + 10.72 + ], + "words": [ + { + "text": "THAT", + "timestamp": [ + 9.96, + 10.12 + ] + }, + { + "text": "IS", + "timestamp": [ + 10.12, + 10.24 + ] + }, + { + "text": "WHY", + "timestamp": [ + 10.24, + 10.36 + ] + }, + { + "text": "I", + "timestamp": [ + 10.36, + 10.52 + ] + }, + { + "text": "AM", + "timestamp": [ + 10.52, + 10.72 + ] + } + ] + }, + { + "text": "STILL HERE", + "timestamp": [ + 10.72, + 11.38 + ], + "words": [ + { + "text": "STILL", + "timestamp": [ + 10.72, + 10.92 + ] + }, + { + "text": "HERE", + "timestamp": [ + 10.92, + 11.38 + ] + } + ] + } + ] +} diff --git a/refs/html/template-038/word-timings.json b/refs/html/template-038/word-timings.json new file mode 100644 index 0000000..f270e17 --- /dev/null +++ b/refs/html/template-038/word-timings.json @@ -0,0 +1,255 @@ +{ + "beats": [ + { + "i": 1, + "startSec": 0.32, + "endSec": 1.88, + "cueDelayMs": 320, + "cueDurMs": 1560, + "words": [ + { + "w": "MONTH", + "delayMs": 320 + }, + { + "w": "THREE", + "delayMs": 520 + }, + { + "w": "OF", + "delayMs": 840 + }, + { + "w": "BUILDING", + "delayMs": 1120 + }, + { + "w": "IN", + "delayMs": 1400 + }, + { + "w": "PUBLIC", + "delayMs": 1640 + } + ] + }, + { + "i": 2, + "startSec": 1.88, + "endSec": 2.64, + "cueDelayMs": 1880, + "cueDurMs": 760, + "words": [ + { + "w": "HERE", + "delayMs": 1880 + }, + { + "w": "ARE", + "delayMs": 2120 + }, + { + "w": "THE", + "delayMs": 2240 + }, + { + "w": "NUMBERS", + "delayMs": 2360 + } + ] + }, + { + "i": 3, + "startSec": 2.64, + "endSec": 3.68, + "cueDelayMs": 2640, + "cueDurMs": 1040, + "words": [ + { + "w": "NOBODY", + "delayMs": 2640 + }, + { + "w": "POSTS", + "delayMs": 3040 + } + ] + }, + { + "i": 4, + "startSec": 3.68, + "endSec": 4.79, + "cueDelayMs": 3680, + "cueDurMs": 1110, + "words": [ + { + "w": "400", + "delayMs": 3680 + }, + { + "w": "IN", + "delayMs": 4160 + }, + { + "w": "REVENUE", + "delayMs": 4360 + } + ] + }, + { + "i": 5, + "startSec": 4.79, + "endSec": 5.56, + "cueDelayMs": 4790, + "cueDurMs": 770, + "words": [ + { + "w": "200", + "delayMs": 4800 + }, + { + "w": "USERS", + "delayMs": 5200 + } + ] + }, + { + "i": 6, + "startSec": 5.56, + "endSec": 6.56, + "cueDelayMs": 5560, + "cueDurMs": 1000, + "words": [ + { + "w": "AND", + "delayMs": 5560 + }, + { + "w": "RENT", + "delayMs": 5720 + }, + { + "w": "IS", + "delayMs": 5960 + }, + { + "w": "DEFINITELY", + "delayMs": 6120 + } + ] + }, + { + "i": 7, + "startSec": 6.56, + "endSec": 7.44, + "cueDelayMs": 6560, + "cueDurMs": 880, + "words": [ + { + "w": "NOT", + "delayMs": 6560 + }, + { + "w": "COVERED", + "delayMs": 6800 + } + ] + }, + { + "i": 8, + "startSec": 7.44, + "endSec": 8.68, + "cueDelayMs": 7440, + "cueDurMs": 1240, + "words": [ + { + "w": "BUT", + "delayMs": 7440 + }, + { + "w": "NOBODY", + "delayMs": 7720 + }, + { + "w": "CHURNS", + "delayMs": 8160 + } + ] + }, + { + "i": 9, + "startSec": 8.68, + "endSec": 9.96, + "cueDelayMs": 8680, + "cueDurMs": 1280, + "words": [ + { + "w": "EVERYONE", + "delayMs": 8680 + }, + { + "w": "WHO", + "delayMs": 8960 + }, + { + "w": "SIGNS", + "delayMs": 9160 + }, + { + "w": "UP", + "delayMs": 9440 + }, + { + "w": "STAYS", + "delayMs": 9600 + } + ] + }, + { + "i": 10, + "startSec": 9.96, + "endSec": 10.72, + "cueDelayMs": 9960, + "cueDurMs": 760, + "words": [ + { + "w": "THAT", + "delayMs": 9960 + }, + { + "w": "IS", + "delayMs": 10120 + }, + { + "w": "WHY", + "delayMs": 10240 + }, + { + "w": "I", + "delayMs": 10360 + }, + { + "w": "AM", + "delayMs": 10520 + } + ] + }, + { + "i": 11, + "startSec": 10.72, + "endSec": 11.38, + "cueDelayMs": 10720, + "cueDurMs": 660, + "words": [ + { + "w": "STILL", + "delayMs": 10720 + }, + { + "w": "HERE", + "delayMs": 10920 + } + ] + } + ] +} diff --git a/refs/html/template-039/recipe.md b/refs/html/template-039/recipe.md new file mode 100644 index 0000000..3e672ef --- /dev/null +++ b/refs/html/template-039/recipe.md @@ -0,0 +1,58 @@ +# RECIPE — template-039 (9:16 · 736×1312 @ 30fps) — Headline stack + +RESOLUTION: px below are authored at the source template's own canvas, 480×864; the compiled module +scales them per run (`pipeline/recipes/template-lib.ts`, `canvasFor`): portrait SCALE = W/480 for sizes +and horizontal offsets, H/864 for vertical anchors; landscape SCALE = (H/864)×1.6. `em` values and ms +timings never scale; the manifest carries the run's real W/H. The reference 9:16 canvas 736×1312 is +SCALE 1.533. The shell (reset · body · .vid · cueWin/.cue · .pg with pgOn/pgOff/pgMid) is the shared +one in `template-lib.ts` `docShell`; the SKELETON lists what this recipe adds. + +## 1. IDENTITY + +Heavy italic Jost caps, every word on a square red plate with an offset black shadow box, stacked +left from mid-frame and rising in; the beat's strongest word stamps in at 1.37× black-on-yellow with an +overshoot, and a number takes the 2.2× bleed plate that runs off the left edge. + +## 2. SKELETON + +``` +fonts: Jost:ital,wght@1,900 +.stack { left:26px; right:26px; top:520px; z-index:2; 'Jost' italic 900; left } .ln { block; line-height:1; padding-bottom:9px } +.w { inline-block; bottom; margin-right:9px; opacity:0; forwards; pre; transform-origin:left center } .bleed { margin-left:-26px } +.rc { 54px; letter-spacing:-.015em; padding:4px 10px 6px; background:#FF1F0F; color:#FFF; box-shadow:5px 5px 0 #0A0A0A; animation:wIn 130ms cubic-bezier(.16,.84,.3,1) } +.rh { 74px; letter-spacing:-.025em; padding:6px 14px 9px; background:#E9FF00; color:#0A0A0A; box-shadow:7px 7px 0 #0A0A0A; animation:hIn 210ms cubic-bezier(.34,1.3,.64,1) } +.rb { 118px; letter-spacing:-.035em; padding:9px 22px 14px; background:#E9FF00; color:#0A0A0A; box-shadow:11px 11px 0 #0A0A0A; animation:hIn 300ms } +@keyframes wIn { from{opacity:0; translateY(.24em)} } @keyframes hIn { from{opacity:0; scale(.8)} } +``` + +## 3. PER-BEAT ASSEMBLY + +- Plate width = chars × 0.6em × rung font + rung padding (20 / 28 / 44px), 9px gap; lines wrap to + 428px, 2 lines a page (cap 22 chars, 5 units); overflow → demote ×0.92 (all rungs together). + +## 4. WORDS + TIMING + +One span per word (glyph spans where the recipe types), `animation-delay` = `delayMs` VERBATIM; the cue gets `{cueDelayMs}` and a window running to the next beat's start (the last beat to the video's end). Mid-beat pages switch at the successor's first word. + +## 5. EMPHASIS + +Hero = `accentIndex`: digit-bearing → `.rb` (bleed when it starts its line), else `.rh`; every other word `.rc`. + +## BOUNDED VARIETY + +None. + +## 6. VERIFY LOOP + +``` +{repo}/.veed-engine/veed-engine-cli {repo}/runs//final --verify +``` +bounds on a `b{N}…` id → the runner demotes that beat one row (font × 0.92, capacities recomputed) and +regenerates; ≤ 2 cycles → `--record`. Manifest: `{"render":{"width":W,"height":H,"fps":FPS,"duration":DUR}}`. +Ids sit on the element that wraps the text; demotion is keyed per beat. + +## 7. DO NOT + +No fonts, colours or keyframes beyond this sheet; no invented timing; no reading the frames; no depth +compositing (layers the source composited behind the presenter are drawn on top here); never hand-edit +the output. diff --git a/refs/html/template-039/recipe.ts b/refs/html/template-039/recipe.ts new file mode 100644 index 0000000..4b932eb --- /dev/null +++ b/refs/html/template-039/recipe.ts @@ -0,0 +1,93 @@ +/** + * template-039 "Headline stack": heavy italic Jost caps, every word on a square red plate with an + * offset black shadow box, stacked left from mid-frame; the beat's strongest word stamps in at 1.37x + * black-on-yellow, and a number takes the giant bleed plate. + */ +import { accentIndex } from '../../../pipeline/recipes/lib.ts'; +import { + type Unit, + DEMOTE_STEP, + canvasFor, + cueDiv, + docShell, + eachBeat, + escapeHtml, + pageGateStyle, + paginate, + templateRecipe, + unitDelay, + unitText, + wrapByWidth, +} from '../../../pipeline/recipes/template-lib.ts'; + +const AVG_EM = 0.64; // Jost 900 italic caps +const MARGIN = 26; +const TOP = 520; +const GAP = 9; +const MAX_LINES = 2; +const RUNG = { + rc: { font: 54, padX: 20 }, + rh: { font: 74, padX: 28 }, + rb: { font: 118, padX: 44 }, +}; +type Rung = keyof typeof RUNG; + +export default templateRecipe('template-039', (meta, timings, opts) => { + const c = canvasFor(meta); + const measure = c.refWidth - 2 * MARGIN; + let wordSeq = 0; + + const cues = eachBeat(meta, timings, opts, 'uppercase', (b) => { + const heroIdx = accentIndex(b.units); + const rungOf = (u: Unit): Rung => (b.units.indexOf(u) !== heroIdx ? 'rc' : /\d/.test(unitText(u)) ? 'rb' : 'rh'); + const plateW = (u: Unit, f: number) => (u.chars * AVG_EM * RUNG[rungOf(u)].font + RUNG[rungOf(u)].padX) * f; + let rows = b.rows; + let f = 1; + let pages: Unit[][] = []; + let pageLines: Unit[][][] = []; + for (; rows <= b.rows + 4; rows++) { + f = Math.pow(DEMOTE_STEP, rows); + pages = paginate(b.units, Math.floor(22 * f), 5); + pageLines = pages.map((p) => wrapByWidth(p, measure, (u) => plateW(u, f), GAP)); + if (pageLines.every((ls) => ls.length <= MAX_LINES && ls.every((l) => l.reduce((a, u) => a + plateW(u, f), 0) + GAP * (l.length - 1) <= measure))) break; + } + const pageDivs = pages.map((page, pi) => { + const pageId = `b${b.n}p${pi + 1}`; + const firstDelay = unitDelay(page[0]); + const nextStart = pi + 1 < pages.length ? unitDelay(pages[pi + 1][0]) : null; + const lines = pageLines[pi].map((line, li) => { + const spans = line.map((u, ui) => u.spans.map((s) => { + wordSeq++; + const r = rungOf(u); + const cls = ['w', r, r === 'rb' && ui === 0 ? 'bleed' : ''].filter(Boolean).join(' '); + return `${escapeHtml(s.text)}`; + }).join('')).join(''); + return `
${spans}
`; + }).join('\n '); + return `
\n ${lines}\n
`; + }); + return cueDiv(b.n, b.cueDelayMs, b.winMs, pageDivs.join('\n')); + }); + + const css = ` + .stack { left:${c.px(MARGIN)}px; right:${c.px(MARGIN)}px; top:${c.py(TOP)}px; z-index:2; font-family:'Jost'; font-style:italic; font-weight:900; text-align:left; } + .ln { display:block; line-height:1; padding-bottom:${c.px(GAP)}px; } + .w { display:inline-block; vertical-align:bottom; margin-right:${c.px(GAP)}px; opacity:0; animation-fill-mode:forwards; white-space:pre; transform-origin:left center; } + .bleed { margin-left:-${c.px(MARGIN)}px; } + .rc { letter-spacing:-0.015em; padding:${c.px(4)}px ${c.px(10)}px ${c.px(6)}px; background:#FF1F0F; color:#FFFFFF; box-shadow:${c.px(5)}px ${c.px(5)}px 0 #0A0A0A; + animation-name:wIn; animation-duration:130ms; animation-timing-function:cubic-bezier(.16,.84,.3,1); } + .rh { letter-spacing:-0.025em; padding:${c.px(6)}px ${c.px(14)}px ${c.px(9)}px; background:#E9FF00; color:#0A0A0A; box-shadow:${c.px(7)}px ${c.px(7)}px 0 #0A0A0A; + animation-name:hIn; animation-duration:210ms; animation-timing-function:cubic-bezier(.34,1.3,.64,1); } + .rb { letter-spacing:-0.035em; padding:${c.px(9)}px ${c.px(22)}px ${c.px(14)}px; background:#E9FF00; color:#0A0A0A; box-shadow:${c.px(11)}px ${c.px(11)}px 0 #0A0A0A; + animation-name:hIn; animation-duration:300ms; animation-timing-function:cubic-bezier(.34,1.3,.64,1); } + @keyframes wIn { from{opacity:0; transform:translateY(0.24em)} to{opacity:1; transform:none} } + @keyframes hIn { from{opacity:0; transform:scale(0.8)} to{opacity:1; transform:none} }`; + + return docShell({ + canvas: c, + videoPath: meta.videoPath, + fonts: ['Jost:ital,wght@1,900'], + css, + body: cues.join('\n'), + }); +}); diff --git a/refs/html/template-039/template.wv b/refs/html/template-039/template.wv new file mode 100644 index 0000000..5cb1921 --- /dev/null +++ b/refs/html/template-039/template.wv @@ -0,0 +1,99 @@ + + + + + + +
+
+
IFIREDOUR
+
BIGGEST
+
+
+
+
+
CLIENTTWO
+
YEARSAGO
+
+
+
+
+
BEST
+
DECISION
+
+
+
+
+
WEEVER
+
MADE
+
+
+
+
+
THEYWERE
+
40%
+
+
+
+
+
OF
+
REVENUE
+
+
+
+
+
AND
+
100%
+
+
+
+
+
OFOUR
+
STRESS
+
+
+
+
+
WEREBUILT
+
AROUND
+
+
+
+
+
CUSTOMERS
+
WELOVE
+
+
+
+
+
THATISTHE
+
COMPANY
+
+
+
YOUARE
+
LOOKINGAT
+
+
diff --git a/refs/html/template-039/transcript.json b/refs/html/template-039/transcript.json new file mode 100644 index 0000000..cff298d --- /dev/null +++ b/refs/html/template-039/transcript.json @@ -0,0 +1,363 @@ +{ + "text": "I FIRED OUR BIGGEST CLIENT TWO YEARS AGO BEST DECISION WE EVER MADE THEY WERE 40% OF REVENUE AND 100% OF OUR STRESS WE REBUILT AROUND CUSTOMERS WE LOVE THAT IS THE COMPANY YOU ARE LOOKING AT", + "chunks": [ + { + "text": "I FIRED OUR BIGGEST", + "timestamp": [ + 0.16, + 1.04 + ], + "words": [ + { + "text": "I", + "timestamp": [ + 0.16, + 0.28 + ] + }, + { + "text": "FIRED", + "timestamp": [ + 0.28, + 0.6 + ] + }, + { + "text": "OUR", + "timestamp": [ + 0.6, + 0.76 + ] + }, + { + "text": "BIGGEST", + "timestamp": [ + 0.76, + 1.04 + ] + } + ] + }, + { + "text": "CLIENT TWO YEARS AGO", + "timestamp": [ + 1.04, + 2.32 + ], + "words": [ + { + "text": "CLIENT", + "timestamp": [ + 1.04, + 1.36 + ] + }, + { + "text": "TWO", + "timestamp": [ + 1.36, + 1.6 + ] + }, + { + "text": "YEARS", + "timestamp": [ + 1.6, + 1.8 + ] + }, + { + "text": "AGO", + "timestamp": [ + 1.8, + 2.32 + ] + } + ] + }, + { + "text": "BEST DECISION", + "timestamp": [ + 2.32, + 2.99 + ], + "words": [ + { + "text": "BEST", + "timestamp": [ + 2.32, + 2.68 + ] + }, + { + "text": "DECISION", + "timestamp": [ + 2.68, + 2.99 + ] + } + ] + }, + { + "text": "WE EVER MADE", + "timestamp": [ + 3, + 3.92 + ], + "words": [ + { + "text": "WE", + "timestamp": [ + 3, + 3.28 + ] + }, + { + "text": "EVER", + "timestamp": [ + 3.28, + 3.48 + ] + }, + { + "text": "MADE", + "timestamp": [ + 3.48, + 3.92 + ] + } + ] + }, + { + "text": "THEY WERE 40%", + "timestamp": [ + 3.92, + 5.12 + ], + "words": [ + { + "text": "THEY", + "timestamp": [ + 3.92, + 4.24 + ] + }, + { + "text": "WERE", + "timestamp": [ + 4.24, + 4.48 + ] + }, + { + "text": "40%", + "timestamp": [ + 4.48, + 5.12 + ] + } + ] + }, + { + "text": "OF REVENUE", + "timestamp": [ + 5.12, + 6.39 + ], + "words": [ + { + "text": "OF", + "timestamp": [ + 5.12, + 5.4 + ] + }, + { + "text": "REVENUE", + "timestamp": [ + 5.4, + 6.39 + ] + } + ] + }, + { + "text": "AND 100%", + "timestamp": [ + 6.4, + 8.08 + ], + "words": [ + { + "text": "AND", + "timestamp": [ + 6.4, + 6.96 + ] + }, + { + "text": "100%", + "timestamp": [ + 6.96, + 8.08 + ] + } + ] + }, + { + "text": "OF OUR STRESS", + "timestamp": [ + 8.08, + 9.28 + ], + "words": [ + { + "text": "OF", + "timestamp": [ + 8.08, + 8.36 + ] + }, + { + "text": "OUR", + "timestamp": [ + 8.36, + 8.56 + ] + }, + { + "text": "STRESS", + "timestamp": [ + 8.56, + 9.28 + ] + } + ] + }, + { + "text": "WE REBUILT AROUND", + "timestamp": [ + 9.28, + 10.48 + ], + "words": [ + { + "text": "WE", + "timestamp": [ + 9.28, + 9.6 + ] + }, + { + "text": "REBUILT", + "timestamp": [ + 9.6, + 10.2 + ] + }, + { + "text": "AROUND", + "timestamp": [ + 10.2, + 10.48 + ] + } + ] + }, + { + "text": "CUSTOMERS WE LOVE", + "timestamp": [ + 10.48, + 12.08 + ], + "words": [ + { + "text": "CUSTOMERS", + "timestamp": [ + 10.48, + 11.12 + ] + }, + { + "text": "WE", + "timestamp": [ + 11.12, + 11.32 + ] + }, + { + "text": "LOVE", + "timestamp": [ + 11.32, + 12.08 + ] + } + ] + }, + { + "text": "THAT IS THE COMPANY YOU ARE LOOKING AT", + "timestamp": [ + 12.08, + 14.26 + ], + "words": [ + { + "text": "THAT", + "timestamp": [ + 12.08, + 12.36 + ] + }, + { + "text": "IS", + "timestamp": [ + 12.36, + 12.52 + ] + }, + { + "text": "THE", + "timestamp": [ + 12.52, + 12.68 + ] + }, + { + "text": "COMPANY", + "timestamp": [ + 12.68, + 12.96 + ] + }, + { + "text": "YOU", + "timestamp": [ + 12.96, + 13.24 + ] + }, + { + "text": "ARE", + "timestamp": [ + 13.24, + 13.44 + ] + }, + { + "text": "LOOKING", + "timestamp": [ + 13.44, + 13.68 + ] + }, + { + "text": "AT", + "timestamp": [ + 13.68, + 14.26 + ] + } + ] + } + ] +} diff --git a/refs/html/template-039/word-timings.json b/refs/html/template-039/word-timings.json new file mode 100644 index 0000000..f1c7a64 --- /dev/null +++ b/refs/html/template-039/word-timings.json @@ -0,0 +1,251 @@ +{ + "beats": [ + { + "i": 1, + "startSec": 0.16, + "endSec": 1.04, + "cueDelayMs": 160, + "cueDurMs": 880, + "words": [ + { + "w": "I", + "delayMs": 160 + }, + { + "w": "FIRED", + "delayMs": 280 + }, + { + "w": "OUR", + "delayMs": 600 + }, + { + "w": "BIGGEST", + "delayMs": 760 + } + ] + }, + { + "i": 2, + "startSec": 1.04, + "endSec": 2.32, + "cueDelayMs": 1040, + "cueDurMs": 1280, + "words": [ + { + "w": "CLIENT", + "delayMs": 1040 + }, + { + "w": "TWO", + "delayMs": 1360 + }, + { + "w": "YEARS", + "delayMs": 1600 + }, + { + "w": "AGO", + "delayMs": 1800 + } + ] + }, + { + "i": 3, + "startSec": 2.32, + "endSec": 2.99, + "cueDelayMs": 2320, + "cueDurMs": 670, + "words": [ + { + "w": "BEST", + "delayMs": 2320 + }, + { + "w": "DECISION", + "delayMs": 2680 + } + ] + }, + { + "i": 4, + "startSec": 3, + "endSec": 3.92, + "cueDelayMs": 3000, + "cueDurMs": 920, + "words": [ + { + "w": "WE", + "delayMs": 3000 + }, + { + "w": "EVER", + "delayMs": 3280 + }, + { + "w": "MADE", + "delayMs": 3480 + } + ] + }, + { + "i": 5, + "startSec": 3.92, + "endSec": 5.12, + "cueDelayMs": 3920, + "cueDurMs": 1200, + "words": [ + { + "w": "THEY", + "delayMs": 3920 + }, + { + "w": "WERE", + "delayMs": 4240 + }, + { + "w": "40%", + "delayMs": 4480 + } + ] + }, + { + "i": 6, + "startSec": 5.12, + "endSec": 6.39, + "cueDelayMs": 5120, + "cueDurMs": 1270, + "words": [ + { + "w": "OF", + "delayMs": 5120 + }, + { + "w": "REVENUE", + "delayMs": 5400 + } + ] + }, + { + "i": 7, + "startSec": 6.4, + "endSec": 8.08, + "cueDelayMs": 6400, + "cueDurMs": 1680, + "words": [ + { + "w": "AND", + "delayMs": 6400 + }, + { + "w": "100%", + "delayMs": 6960 + } + ] + }, + { + "i": 8, + "startSec": 8.08, + "endSec": 9.28, + "cueDelayMs": 8080, + "cueDurMs": 1200, + "words": [ + { + "w": "OF", + "delayMs": 8080 + }, + { + "w": "OUR", + "delayMs": 8360 + }, + { + "w": "STRESS", + "delayMs": 8560 + } + ] + }, + { + "i": 9, + "startSec": 9.28, + "endSec": 10.48, + "cueDelayMs": 9280, + "cueDurMs": 1200, + "words": [ + { + "w": "WE", + "delayMs": 9280 + }, + { + "w": "REBUILT", + "delayMs": 9600 + }, + { + "w": "AROUND", + "delayMs": 10200 + } + ] + }, + { + "i": 10, + "startSec": 10.48, + "endSec": 12.08, + "cueDelayMs": 10480, + "cueDurMs": 1600, + "words": [ + { + "w": "CUSTOMERS", + "delayMs": 10480 + }, + { + "w": "WE", + "delayMs": 11120 + }, + { + "w": "LOVE", + "delayMs": 11320 + } + ] + }, + { + "i": 11, + "startSec": 12.08, + "endSec": 14.26, + "cueDelayMs": 12080, + "cueDurMs": 2180, + "words": [ + { + "w": "THAT", + "delayMs": 12080 + }, + { + "w": "IS", + "delayMs": 12360 + }, + { + "w": "THE", + "delayMs": 12520 + }, + { + "w": "COMPANY", + "delayMs": 12680 + }, + { + "w": "YOU", + "delayMs": 12960 + }, + { + "w": "ARE", + "delayMs": 13240 + }, + { + "w": "LOOKING", + "delayMs": 13440 + }, + { + "w": "AT", + "delayMs": 13680 + } + ] + } + ] +} diff --git a/refs/html/template-040/recipe.md b/refs/html/template-040/recipe.md new file mode 100644 index 0000000..9e98c93 --- /dev/null +++ b/refs/html/template-040/recipe.md @@ -0,0 +1,56 @@ +# RECIPE — template-040 (9:16 · 736×1312 @ 30fps) — Left block + +RESOLUTION: px below are authored at the source template's own canvas, 480×864; the compiled module +scales them per run (`pipeline/recipes/template-lib.ts`, `canvasFor`): portrait SCALE = W/480 for sizes +and horizontal offsets, H/864 for vertical anchors; landscape SCALE = (H/864)×1.6. `em` values and ms +timings never scale; the manifest carries the run's real W/H. The reference 9:16 canvas 736×1312 is +SCALE 1.533. The shell (reset · body · .vid · cueWin/.cue · .pg with pgOn/pgOff/pgMid) is the shared +one in `template-lib.ts` `docShell`; the SKELETON lists what this recipe adds. + +## 1. IDENTITY + +A left-aligned all-caps Instrument Sans block low in the frame, wide tracking and generous leading; +each word fades and rises 0.3em over 280ms then holds dead still until the cut. The beat's strongest +word takes the mint accent colour and nothing else changes. + +## 2. SKELETON + +``` +fonts: Instrument+Sans:wght@500 +.cap { left:44px; right:44px; bottom:132px; left; 'Instrument Sans' 500; letter-spacing:.05em; line-height:1.38; color:#F5F1E6; + text-shadow:0 2px 14px rgba(20,17,14,.62), 0 1px 3px rgba(20,17,14,.48) } +.g { inline-block; opacity:0; animation:capG 280ms cubic-bezier(.2,.7,.3,1) forwards } @keyframes capG { 0%{opacity:0; translateY(.3em)} } +.ac { color:#7FD5AA } +``` + +## 3. PER-BEAT ASSEMBLY + +- 36px; advance 0.67em → maxChars = floor(392 / (0.67 × 36)) = 16 per line, 2 lines a page (cap + 2×maxChars − 2, 8 units); overflow → demote ×0.92. + +## 4. WORDS + TIMING + +One span per word (glyph spans where the recipe types), `animation-delay` = `delayMs` VERBATIM; the cue gets `{cueDelayMs}` and a window running to the next beat's start (the last beat to the video's end). Mid-beat pages switch at the successor's first word. + +## 5. EMPHASIS + +Accent = `accentIndex` → `.ac` on the unit (colour only). + +## BOUNDED VARIETY + +None. + +## 6. VERIFY LOOP + +``` +{repo}/.veed-engine/veed-engine-cli {repo}/runs//final --verify +``` +bounds on a `b{N}…` id → the runner demotes that beat one row (font × 0.92, capacities recomputed) and +regenerates; ≤ 2 cycles → `--record`. Manifest: `{"render":{"width":W,"height":H,"fps":FPS,"duration":DUR}}`. +Ids sit on the element that wraps the text; demotion is keyed per beat. + +## 7. DO NOT + +No fonts, colours or keyframes beyond this sheet; no invented timing; no reading the frames; no depth +compositing (layers the source composited behind the presenter are drawn on top here); never hand-edit +the output. diff --git a/refs/html/template-040/recipe.ts b/refs/html/template-040/recipe.ts new file mode 100644 index 0000000..eea6f79 --- /dev/null +++ b/refs/html/template-040/recipe.ts @@ -0,0 +1,78 @@ +/** + * template-040: a left-aligned all-caps Instrument Sans block low in the frame, wide tracking and + * generous leading; each word fades and rises 0.3em over 280ms then holds dead still until the cut. + * The beat's strongest word takes the mint accent colour, nothing else changes. + */ +import { accentIndex } from '../../../pipeline/recipes/lib.ts'; +import { + type Unit, + DEMOTE_STEP, + canvasFor, + cueDiv, + docShell, + eachBeat, + glyphSpans, + pageGateStyle, + paginate, + templateRecipe, + unitDelay, + wrapByChars, +} from '../../../pipeline/recipes/template-lib.ts'; + +const FONT = 36; +const AVG_EM = 0.67; // Instrument Sans 500 caps + 0.05em tracking +const MARGIN = 44; +const BOTTOM = 132; +const MAX_LINES = 2; + +export default templateRecipe('template-040', (meta, timings, opts) => { + const c = canvasFor(meta); + const measure = c.refWidth - 2 * MARGIN; + + const cues = eachBeat(meta, timings, opts, 'uppercase', (b) => { + const accent = accentIndex(b.units); + let rows = b.rows; + let f = 1; + let maxChars = 0; + let pages: Unit[][] = []; + let pageLines: Unit[][][] = []; + for (; rows <= b.rows + 4; rows++) { + f = Math.pow(DEMOTE_STEP, rows); + maxChars = Math.floor(measure / (AVG_EM * FONT * f)); + pages = paginate(b.units, maxChars * MAX_LINES - 2, 8); + pageLines = pages.map((p) => wrapByChars(p, maxChars)); + if (pageLines.every((ls) => ls.length <= MAX_LINES)) break; + } + const fs = Math.round(FONT * f * c.s); + const pageDivs = pages.map((page, pi) => { + const pageId = `b${b.n}p${pi + 1}`; + const firstDelay = unitDelay(page[0]); + const nextStart = pi + 1 < pages.length ? unitDelay(pages[pi + 1][0]) : null; + const lines = pageLines[pi].map((line, li) => { + const units = line.map((u) => `${u.spans.map((s) => glyphSpans(s.text, s.delayMs, 0, 'g')).join('')}`); + return `
${units.join(' ')}
`; + }).join('\n '); + return `
\n ${lines}\n
`; + }); + return cueDiv(b.n, b.cueDelayMs, b.winMs, pageDivs.join('\n')); + }); + + const css = ` + .cap { left:${c.px(MARGIN)}px; right:${c.px(MARGIN)}px; bottom:${c.py(BOTTOM)}px; text-align:left; z-index:2; + font-family:'Instrument Sans'; font-weight:500; letter-spacing:0.05em; line-height:1.38; color:#F5F1E6; + text-shadow:0 ${c.px(2)}px ${c.px(14)}px rgba(20,17,14,0.62), 0 ${c.px(1)}px ${c.px(3)}px rgba(20,17,14,0.48); } + .cl { display:block; } + .u { display:inline-block; white-space:pre; } + .sp { display:inline-block; white-space:pre; } + .g { display:inline-block; opacity:0; animation:capG 280ms cubic-bezier(.2,.7,.3,1) forwards; } + @keyframes capG { 0%{opacity:0; transform:translateY(0.3em)} 100%{opacity:1; transform:translateY(0)} } + .ac { color:#7FD5AA; }`; + + return docShell({ + canvas: c, + videoPath: meta.videoPath, + fonts: ['Instrument+Sans:wght@500'], + css, + body: cues.join('\n'), + }); +}); diff --git a/refs/html/template-040/template.wv b/refs/html/template-040/template.wv new file mode 100644 index 0000000..21680a9 --- /dev/null +++ b/refs/html/template-040/template.wv @@ -0,0 +1,72 @@ + + + + + + +
+
+
I BOUGHT THIS
+
JACKET FOR ONE
+
+
+
+
+
HIKE JUST TO
+
LOOK THE PART.
+
+
+
+
+
HONESTLY, TWO
+
MONTHS LATER, IT
+
+
+
HAS DONE RAIN,
+
+
+
+
+
WIND, THREE
+
SUMMITS AND ONE
+
+
+
VERY MUDDY
+
FESTIVAL.
+
+
+
+
+
STILL LOOKS NEW.
+
+
+
+
+
I GET WHY PEOPLE
+
WOULD NOT
+
+
+
SHUT UP
+
ABOUT IT.
+
+
diff --git a/refs/html/template-040/transcript.json b/refs/html/template-040/transcript.json new file mode 100644 index 0000000..debe149 --- /dev/null +++ b/refs/html/template-040/transcript.json @@ -0,0 +1,346 @@ +{ + "text": "I BOUGHT THIS JACKET FOR ONE HIKE JUST TO LOOK THE PART. HONESTLY, TWO MONTHS LATER, IT HAS DONE RAIN, WIND, THREE SUMMITS AND ONE VERY MUDDY FESTIVAL. STILL LOOKS NEW. I GET WHY PEOPLE WOULD NOT SHUT UP ABOUT IT.", + "chunks": [ + { + "text": "I BOUGHT THIS JACKET FOR ONE", + "timestamp": [ + 0.16, + 1.4 + ], + "words": [ + { + "text": "I", + "timestamp": [ + 0.16, + 0.24 + ] + }, + { + "text": "BOUGHT", + "timestamp": [ + 0.24, + 0.44 + ] + }, + { + "text": "THIS", + "timestamp": [ + 0.44, + 0.6 + ] + }, + { + "text": "JACKET", + "timestamp": [ + 0.6, + 1 + ] + }, + { + "text": "FOR", + "timestamp": [ + 1, + 1.16 + ] + }, + { + "text": "ONE", + "timestamp": [ + 1.16, + 1.4 + ] + } + ] + }, + { + "text": "HIKE JUST TO LOOK THE PART.", + "timestamp": [ + 1.4, + 3.12 + ], + "words": [ + { + "text": "HIKE", + "timestamp": [ + 1.4, + 1.92 + ] + }, + { + "text": "JUST", + "timestamp": [ + 1.92, + 2.2 + ] + }, + { + "text": "TO", + "timestamp": [ + 2.2, + 2.36 + ] + }, + { + "text": "LOOK", + "timestamp": [ + 2.36, + 2.56 + ] + }, + { + "text": "THE", + "timestamp": [ + 2.56, + 2.76 + ] + }, + { + "text": "PART.", + "timestamp": [ + 2.76, + 3.12 + ] + } + ] + }, + { + "text": "HONESTLY, TWO MONTHS LATER, IT HAS DONE RAIN,", + "timestamp": [ + 3.12, + 6.08 + ], + "words": [ + { + "text": "HONESTLY,", + "timestamp": [ + 3.12, + 4.08 + ] + }, + { + "text": "TWO", + "timestamp": [ + 4.08, + 4.36 + ] + }, + { + "text": "MONTHS", + "timestamp": [ + 4.36, + 4.6 + ] + }, + { + "text": "LATER,", + "timestamp": [ + 4.6, + 4.84 + ] + }, + { + "text": "IT", + "timestamp": [ + 4.84, + 5 + ] + }, + { + "text": "HAS", + "timestamp": [ + 5, + 5.2 + ] + }, + { + "text": "DONE", + "timestamp": [ + 5.2, + 5.44 + ] + }, + { + "text": "RAIN,", + "timestamp": [ + 5.44, + 6.08 + ] + } + ] + }, + { + "text": "WIND, THREE SUMMITS AND ONE VERY MUDDY FESTIVAL.", + "timestamp": [ + 6.08, + 9.92 + ], + "words": [ + { + "text": "WIND,", + "timestamp": [ + 6.08, + 6.8 + ] + }, + { + "text": "THREE", + "timestamp": [ + 6.8, + 7.16 + ] + }, + { + "text": "SUMMITS", + "timestamp": [ + 7.16, + 7.92 + ] + }, + { + "text": "AND", + "timestamp": [ + 7.92, + 8.2 + ] + }, + { + "text": "ONE", + "timestamp": [ + 8.2, + 8.44 + ] + }, + { + "text": "VERY", + "timestamp": [ + 8.44, + 8.72 + ] + }, + { + "text": "MUDDY", + "timestamp": [ + 8.72, + 9.16 + ] + }, + { + "text": "FESTIVAL.", + "timestamp": [ + 9.16, + 9.92 + ] + } + ] + }, + { + "text": "STILL LOOKS NEW.", + "timestamp": [ + 9.92, + 10.8 + ], + "words": [ + { + "text": "STILL", + "timestamp": [ + 9.92, + 10.24 + ] + }, + { + "text": "LOOKS", + "timestamp": [ + 10.24, + 10.52 + ] + }, + { + "text": "NEW.", + "timestamp": [ + 10.52, + 10.8 + ] + } + ] + }, + { + "text": "I GET WHY PEOPLE WOULD NOT SHUT UP ABOUT IT.", + "timestamp": [ + 10.8, + 13.067 + ], + "words": [ + { + "text": "I", + "timestamp": [ + 10.8, + 11.08 + ] + }, + { + "text": "GET", + "timestamp": [ + 11.08, + 11.24 + ] + }, + { + "text": "WHY", + "timestamp": [ + 11.24, + 11.4 + ] + }, + { + "text": "PEOPLE", + "timestamp": [ + 11.4, + 11.6 + ] + }, + { + "text": "WOULD", + "timestamp": [ + 11.6, + 11.8 + ] + }, + { + "text": "NOT", + "timestamp": [ + 11.8, + 12 + ] + }, + { + "text": "SHUT", + "timestamp": [ + 12, + 12.24 + ] + }, + { + "text": "UP", + "timestamp": [ + 12.24, + 12.36 + ] + }, + { + "text": "ABOUT", + "timestamp": [ + 12.36, + 12.52 + ] + }, + { + "text": "IT.", + "timestamp": [ + 12.52, + 13.067 + ] + } + ] + } + ] +} diff --git a/refs/html/template-040/word-timings.json b/refs/html/template-040/word-timings.json new file mode 100644 index 0000000..3070a96 --- /dev/null +++ b/refs/html/template-040/word-timings.json @@ -0,0 +1,222 @@ +{ + "beats": [ + { + "i": 1, + "startSec": 0.16, + "endSec": 1.4, + "cueDelayMs": 160, + "cueDurMs": 1240, + "words": [ + { + "w": "I", + "delayMs": 160 + }, + { + "w": "BOUGHT", + "delayMs": 240 + }, + { + "w": "THIS", + "delayMs": 440 + }, + { + "w": "JACKET", + "delayMs": 600 + }, + { + "w": "FOR", + "delayMs": 1000 + }, + { + "w": "ONE", + "delayMs": 1160 + } + ] + }, + { + "i": 2, + "startSec": 1.4, + "endSec": 3.12, + "cueDelayMs": 1400, + "cueDurMs": 1720, + "words": [ + { + "w": "HIKE", + "delayMs": 1400 + }, + { + "w": "JUST", + "delayMs": 1920 + }, + { + "w": "TO", + "delayMs": 2200 + }, + { + "w": "LOOK", + "delayMs": 2360 + }, + { + "w": "THE", + "delayMs": 2560 + }, + { + "w": "PART.", + "delayMs": 2760 + } + ] + }, + { + "i": 3, + "startSec": 3.12, + "endSec": 6.08, + "cueDelayMs": 3120, + "cueDurMs": 2960, + "words": [ + { + "w": "HONESTLY,", + "delayMs": 3120 + }, + { + "w": "TWO", + "delayMs": 4080 + }, + { + "w": "MONTHS", + "delayMs": 4360 + }, + { + "w": "LATER,", + "delayMs": 4600 + }, + { + "w": "IT", + "delayMs": 4840 + }, + { + "w": "HAS", + "delayMs": 5000 + }, + { + "w": "DONE", + "delayMs": 5200 + }, + { + "w": "RAIN,", + "delayMs": 5440 + } + ] + }, + { + "i": 4, + "startSec": 6.08, + "endSec": 9.92, + "cueDelayMs": 6080, + "cueDurMs": 3840, + "words": [ + { + "w": "WIND,", + "delayMs": 6080 + }, + { + "w": "THREE", + "delayMs": 6800 + }, + { + "w": "SUMMITS", + "delayMs": 7160 + }, + { + "w": "AND", + "delayMs": 7920 + }, + { + "w": "ONE", + "delayMs": 8200 + }, + { + "w": "VERY", + "delayMs": 8440 + }, + { + "w": "MUDDY", + "delayMs": 8720 + }, + { + "w": "FESTIVAL.", + "delayMs": 9160 + } + ] + }, + { + "i": 5, + "startSec": 9.92, + "endSec": 10.8, + "cueDelayMs": 9920, + "cueDurMs": 880, + "words": [ + { + "w": "STILL", + "delayMs": 9920 + }, + { + "w": "LOOKS", + "delayMs": 10240 + }, + { + "w": "NEW.", + "delayMs": 10520 + } + ] + }, + { + "i": 6, + "startSec": 10.8, + "endSec": 13.067, + "cueDelayMs": 10800, + "cueDurMs": 2267, + "words": [ + { + "w": "I", + "delayMs": 10800 + }, + { + "w": "GET", + "delayMs": 11080 + }, + { + "w": "WHY", + "delayMs": 11240 + }, + { + "w": "PEOPLE", + "delayMs": 11400 + }, + { + "w": "WOULD", + "delayMs": 11600 + }, + { + "w": "NOT", + "delayMs": 11800 + }, + { + "w": "SHUT", + "delayMs": 12000 + }, + { + "w": "UP", + "delayMs": 12240 + }, + { + "w": "ABOUT", + "delayMs": 12360 + }, + { + "w": "IT.", + "delayMs": 12520 + } + ] + } + ] +} diff --git a/refs/html/template-041/recipe.md b/refs/html/template-041/recipe.md new file mode 100644 index 0000000..375cd76 --- /dev/null +++ b/refs/html/template-041/recipe.md @@ -0,0 +1,56 @@ +# RECIPE — template-041 (9:16 · 736×1312 @ 30fps) — Standfirst + +RESOLUTION: px below are authored at the source template's own canvas, 480×864; the compiled module +scales them per run (`pipeline/recipes/template-lib.ts`, `canvasFor`): portrait SCALE = W/480 for sizes +and horizontal offsets, H/864 for vertical anchors; landscape SCALE = (H/864)×1.6. `em` values and ms +timings never scale; the manifest carries the run's real W/H. The reference 9:16 canvas 736×1312 is +SCALE 1.533. The shell (reset · body · .vid · cueWin/.cue · .pg with pgOn/pgOff/pgMid) is the shared +one in `template-lib.ts` `docShell`; the SKELETON lists what this recipe adds. + +## 1. IDENTITY + +A centred Fraunces serif block just below mid-frame, one position and one fixed 52px pitch for every +beat; words fade and rise 0.22em into place then hold; the beat's pivot word changes register into the +face's own italic in gold, same size, same baseline. + +## 2. SKELETON + +``` +fonts: Fraunces:ital,wght@0,600;1,600 +.cap { left:56px; top:518px; width:368px; center; 'Fraunces' 600; letter-spacing:.004em; color:#F7F1E6; + text-shadow:0 2px 14px rgba(36,28,21,.62), 0 1px 3px rgba(36,28,21,.5) } .cl { block; nowrap; line-height:52px } +.w { inline-block; pre; opacity:0; line-height:52px; animation:wordIn 260ms cubic-bezier(.2,.7,.3,1) both } @keyframes wordIn { 0%{opacity:0; translateY(.22em)} } +.a { italic; color:#FEC719 } +``` + +## 3. PER-BEAT ASSEMBLY + +- 34px; advance 0.5em → maxChars = floor(368 / (0.5 × 34)) = 21 per line, 2 lines a page (cap + 2×maxChars − 2, 9 units); overflow → demote ×0.92 (pitch scales with the font). + +## 4. WORDS + TIMING + +One span per word (glyph spans where the recipe types), `animation-delay` = `delayMs` VERBATIM; the cue gets `{cueDelayMs}` and a window running to the next beat's start (the last beat to the video's end). Mid-beat pages switch at the successor's first word. + +## 5. EMPHASIS + +Accent = `accentIndex` → `.a`. + +## BOUNDED VARIETY + +None. + +## 6. VERIFY LOOP + +``` +{repo}/.veed-engine/veed-engine-cli {repo}/runs//final --verify +``` +bounds on a `b{N}…` id → the runner demotes that beat one row (font × 0.92, capacities recomputed) and +regenerates; ≤ 2 cycles → `--record`. Manifest: `{"render":{"width":W,"height":H,"fps":FPS,"duration":DUR}}`. +Ids sit on the element that wraps the text; demotion is keyed per beat. + +## 7. DO NOT + +No fonts, colours or keyframes beyond this sheet; no invented timing; no reading the frames; no depth +compositing (layers the source composited behind the presenter are drawn on top here); never hand-edit +the output. diff --git a/refs/html/template-041/recipe.ts b/refs/html/template-041/recipe.ts new file mode 100644 index 0000000..f314d6a --- /dev/null +++ b/refs/html/template-041/recipe.ts @@ -0,0 +1,83 @@ +/** + * template-041 "Standfirst": a centred Fraunces serif block just below mid-frame, one position and one + * fixed line pitch for every beat; words fade and rise into place then hold; the beat's pivot word + * changes register into the face's own italic in gold, same size, same baseline. + */ +import { accentIndex } from '../../../pipeline/recipes/lib.ts'; +import { + type Unit, + DEMOTE_STEP, + canvasFor, + cueDiv, + docShell, + eachBeat, + escapeHtml, + pageGateStyle, + paginate, + templateRecipe, + unitDelay, + wrapByChars, +} from '../../../pipeline/recipes/template-lib.ts'; + +const FONT = 34; +const AVG_EM = 0.5; // Fraunces 600 mixed case +const LEFT = 56; +const TOP = 518; +const WIDTH = 368; +const PITCH = 52; +const MAX_LINES = 2; + +export default templateRecipe('template-041', (meta, timings, opts) => { + const c = canvasFor(meta); + let wordSeq = 0; + + const cues = eachBeat(meta, timings, opts, 'none', (b) => { + const accent = accentIndex(b.units); + let rows = b.rows; + let f = 1; + let maxChars = 0; + let pages: Unit[][] = []; + let pageLines: Unit[][][] = []; + for (; rows <= b.rows + 4; rows++) { + f = Math.pow(DEMOTE_STEP, rows); + maxChars = Math.floor(WIDTH / (AVG_EM * FONT * f)); + pages = paginate(b.units, maxChars * MAX_LINES - 2, 9); + pageLines = pages.map((p) => wrapByChars(p, maxChars)); + if (pageLines.every((ls) => ls.length <= MAX_LINES)) break; + } + const fs = Math.round(FONT * f * c.s); + const pitch = Math.round(PITCH * f * c.s); + const pageDivs = pages.map((page, pi) => { + const pageId = `b${b.n}p${pi + 1}`; + const firstDelay = unitDelay(page[0]); + const nextStart = pi + 1 < pages.length ? unitDelay(pages[pi + 1][0]) : null; + const lines = pageLines[pi].map((line, li) => { + const spans = line.map((u) => u.spans.map((s) => { + wordSeq++; + return `${escapeHtml(s.text)}`; + }).join('')); + return `
${spans.join(' ')}
`; + }).join('\n '); + return `
\n ${lines}\n
`; + }); + return cueDiv(b.n, b.cueDelayMs, b.winMs, pageDivs.join('\n')); + }); + + const css = ` + .cap { left:${c.px(LEFT)}px; top:${c.py(TOP)}px; width:${c.px(WIDTH)}px; text-align:center; z-index:2; + font-family:'Fraunces'; font-weight:600; letter-spacing:0.004em; color:#F7F1E6; + text-shadow:0 ${c.px(2)}px ${c.px(14)}px rgba(36,28,21,0.62), 0 ${c.px(1)}px ${c.px(3)}px rgba(36,28,21,0.5); } + .cl { display:block; white-space:nowrap; } + .sp { display:inline-block; white-space:pre; } + .w { display:inline-block; white-space:pre; opacity:0; animation-name:wordIn; animation-duration:260ms; animation-timing-function:cubic-bezier(.2,.7,.3,1); animation-fill-mode:both; } + @keyframes wordIn { 0%{opacity:0; transform:translateY(0.22em)} 100%{opacity:1; transform:translateY(0)} } + .a { font-style:italic; color:#FEC719; }`; + + return docShell({ + canvas: c, + videoPath: meta.videoPath, + fonts: ['Fraunces:ital,wght@0,600;1,600'], + css, + body: cues.join('\n'), + }); +}); diff --git a/refs/html/template-041/template.wv b/refs/html/template-041/template.wv new file mode 100644 index 0000000..41759d8 --- /dev/null +++ b/refs/html/template-041/template.wv @@ -0,0 +1,61 @@ + + + + + + +
+
+
Two kids, two jobs,
+
zero free evenings.
+
+
+
+
+
I was skeptical
+
about this app.
+
+
+
+
+
Now meal plans
+
take 10 minutes on
+
+
+
+
+
Sunday and the week
+
just runs itself.
+
+
+
+
+
My kids even eat
+
the vegetables.
+
+
+
+
+
Mostly I am
+
keeping it.
+
+
diff --git a/refs/html/template-041/transcript.json b/refs/html/template-041/transcript.json new file mode 100644 index 0000000..2bc0954 --- /dev/null +++ b/refs/html/template-041/transcript.json @@ -0,0 +1,325 @@ +{ + "text": "Two kids, two jobs, zero free evenings. I was skeptical about this app. Now meal plans take 10 minutes on Sunday and the week just runs itself. My kids even eat the vegetables. Mostly I am keeping it.", + "chunks": [ + { + "text": "Two kids, two jobs, zero free evenings.", + "timestamp": [ + 0.24, + 2.52 + ], + "words": [ + { + "text": "Two", + "timestamp": [ + 0.24, + 0.4 + ] + }, + { + "text": "kids,", + "timestamp": [ + 0.4, + 0.76 + ] + }, + { + "text": "two", + "timestamp": [ + 0.76, + 1.04 + ] + }, + { + "text": "jobs,", + "timestamp": [ + 1.04, + 1.48 + ] + }, + { + "text": "zero", + "timestamp": [ + 1.48, + 1.8 + ] + }, + { + "text": "free", + "timestamp": [ + 1.8, + 2.08 + ] + }, + { + "text": "evenings.", + "timestamp": [ + 2.08, + 2.52 + ] + } + ] + }, + { + "text": "I was skeptical about this app.", + "timestamp": [ + 2.52, + 4.16 + ], + "words": [ + { + "text": "I", + "timestamp": [ + 2.52, + 2.72 + ] + }, + { + "text": "was", + "timestamp": [ + 2.72, + 2.84 + ] + }, + { + "text": "skeptical", + "timestamp": [ + 2.84, + 3.24 + ] + }, + { + "text": "about", + "timestamp": [ + 3.24, + 3.44 + ] + }, + { + "text": "this", + "timestamp": [ + 3.44, + 3.64 + ] + }, + { + "text": "app.", + "timestamp": [ + 3.64, + 4.16 + ] + } + ] + }, + { + "text": "Now meal plans take 10 minutes on", + "timestamp": [ + 4.16, + 5.88 + ], + "words": [ + { + "text": "Now", + "timestamp": [ + 4.16, + 4.48 + ] + }, + { + "text": "meal", + "timestamp": [ + 4.48, + 4.76 + ] + }, + { + "text": "plans", + "timestamp": [ + 4.76, + 5.08 + ] + }, + { + "text": "take", + "timestamp": [ + 5.08, + 5.24 + ] + }, + { + "text": "10", + "timestamp": [ + 5.24, + 5.4 + ] + }, + { + "text": "minutes", + "timestamp": [ + 5.4, + 5.72 + ] + }, + { + "text": "on", + "timestamp": [ + 5.72, + 5.88 + ] + } + ] + }, + { + "text": "Sunday and the week just runs itself.", + "timestamp": [ + 5.88, + 8.16 + ], + "words": [ + { + "text": "Sunday", + "timestamp": [ + 5.88, + 6.12 + ] + }, + { + "text": "and", + "timestamp": [ + 6.12, + 6.36 + ] + }, + { + "text": "the", + "timestamp": [ + 6.36, + 6.52 + ] + }, + { + "text": "week", + "timestamp": [ + 6.52, + 6.72 + ] + }, + { + "text": "just", + "timestamp": [ + 6.72, + 6.96 + ] + }, + { + "text": "runs", + "timestamp": [ + 6.96, + 7.32 + ] + }, + { + "text": "itself.", + "timestamp": [ + 7.32, + 8.16 + ] + } + ] + }, + { + "text": "My kids even eat the vegetables.", + "timestamp": [ + 8.16, + 9.76 + ], + "words": [ + { + "text": "My", + "timestamp": [ + 8.16, + 8.44 + ] + }, + { + "text": "kids", + "timestamp": [ + 8.44, + 8.68 + ] + }, + { + "text": "even", + "timestamp": [ + 8.68, + 8.84 + ] + }, + { + "text": "eat", + "timestamp": [ + 8.84, + 9.04 + ] + }, + { + "text": "the", + "timestamp": [ + 9.04, + 9.12 + ] + }, + { + "text": "vegetables.", + "timestamp": [ + 9.12, + 9.76 + ] + } + ] + }, + { + "text": "Mostly I am keeping it.", + "timestamp": [ + 9.76, + 11.7 + ], + "words": [ + { + "text": "Mostly", + "timestamp": [ + 9.76, + 10.48 + ] + }, + { + "text": "I", + "timestamp": [ + 10.48, + 10.76 + ] + }, + { + "text": "am", + "timestamp": [ + 10.76, + 10.96 + ] + }, + { + "text": "keeping", + "timestamp": [ + 10.96, + 11.24 + ] + }, + { + "text": "it.", + "timestamp": [ + 11.24, + 11.7 + ] + } + ] + } + ] +} diff --git a/refs/html/template-041/word-timings.json b/refs/html/template-041/word-timings.json new file mode 100644 index 0000000..9a3098b --- /dev/null +++ b/refs/html/template-041/word-timings.json @@ -0,0 +1,210 @@ +{ + "beats": [ + { + "i": 1, + "startSec": 0.24, + "endSec": 2.52, + "cueDelayMs": 240, + "cueDurMs": 2280, + "words": [ + { + "w": "Two", + "delayMs": 240 + }, + { + "w": "kids,", + "delayMs": 400 + }, + { + "w": "two", + "delayMs": 760 + }, + { + "w": "jobs,", + "delayMs": 1040 + }, + { + "w": "zero", + "delayMs": 1480 + }, + { + "w": "free", + "delayMs": 1800 + }, + { + "w": "evenings.", + "delayMs": 2080 + } + ] + }, + { + "i": 2, + "startSec": 2.52, + "endSec": 4.16, + "cueDelayMs": 2520, + "cueDurMs": 1640, + "words": [ + { + "w": "I", + "delayMs": 2520 + }, + { + "w": "was", + "delayMs": 2720 + }, + { + "w": "skeptical", + "delayMs": 2840 + }, + { + "w": "about", + "delayMs": 3240 + }, + { + "w": "this", + "delayMs": 3440 + }, + { + "w": "app.", + "delayMs": 3640 + } + ] + }, + { + "i": 3, + "startSec": 4.16, + "endSec": 5.88, + "cueDelayMs": 4160, + "cueDurMs": 1720, + "words": [ + { + "w": "Now", + "delayMs": 4160 + }, + { + "w": "meal", + "delayMs": 4480 + }, + { + "w": "plans", + "delayMs": 4760 + }, + { + "w": "take", + "delayMs": 5080 + }, + { + "w": "10", + "delayMs": 5240 + }, + { + "w": "minutes", + "delayMs": 5400 + }, + { + "w": "on", + "delayMs": 5720 + } + ] + }, + { + "i": 4, + "startSec": 5.88, + "endSec": 8.16, + "cueDelayMs": 5880, + "cueDurMs": 2280, + "words": [ + { + "w": "Sunday", + "delayMs": 5880 + }, + { + "w": "and", + "delayMs": 6120 + }, + { + "w": "the", + "delayMs": 6360 + }, + { + "w": "week", + "delayMs": 6520 + }, + { + "w": "just", + "delayMs": 6720 + }, + { + "w": "runs", + "delayMs": 6960 + }, + { + "w": "itself.", + "delayMs": 7320 + } + ] + }, + { + "i": 5, + "startSec": 8.16, + "endSec": 9.76, + "cueDelayMs": 8160, + "cueDurMs": 1600, + "words": [ + { + "w": "My", + "delayMs": 8160 + }, + { + "w": "kids", + "delayMs": 8440 + }, + { + "w": "even", + "delayMs": 8680 + }, + { + "w": "eat", + "delayMs": 8840 + }, + { + "w": "the", + "delayMs": 9040 + }, + { + "w": "vegetables.", + "delayMs": 9120 + } + ] + }, + { + "i": 6, + "startSec": 9.76, + "endSec": 11.7, + "cueDelayMs": 9760, + "cueDurMs": 1940, + "words": [ + { + "w": "Mostly", + "delayMs": 9760 + }, + { + "w": "I", + "delayMs": 10480 + }, + { + "w": "am", + "delayMs": 10760 + }, + { + "w": "keeping", + "delayMs": 10960 + }, + { + "w": "it.", + "delayMs": 11240 + } + ] + } + ] +} diff --git a/refs/html/template-042/recipe.md b/refs/html/template-042/recipe.md new file mode 100644 index 0000000..13e2265 --- /dev/null +++ b/refs/html/template-042/recipe.md @@ -0,0 +1,61 @@ +# RECIPE — template-042 (9:16 · 736×1312 @ 30fps) — Driftwood + +RESOLUTION: px below are authored at the source template's own canvas, 480×864; the compiled module +scales them per run (`pipeline/recipes/template-lib.ts`, `canvasFor`): portrait SCALE = W/480 for sizes +and horizontal offsets, H/864 for vertical anchors; landscape SCALE = (H/864)×1.6. `em` values and ms +timings never scale; the manifest carries the run's real W/H. The reference 9:16 canvas 736×1312 is +SCALE 1.533. The shell (reset · body · .vid · cueWin/.cue · .pg with pgOn/pgOff/pgMid) is the shared +one in `template-lib.ts` `docShell`; the SKELETON lists what this recipe adds. + +## 1. IDENTITY + +Dark slate Instrument Sans typed glyph by glyph (36ms) in a fixed centred block below mid-frame, a +thin grey-blue caret trailing each glyph and parking after the word until the next one types; the +beat's strongest word switches to Instrument Serif italic on the same baseline. Type is grounded by +the preset's shadow stack: one soft dark drop shadow under three widening cream glows, never a plate. + +## 2. SKELETON + +``` +fonts: Instrument+Sans:wght@500 · Instrument+Serif:ital@1 +.cap { left:96px; top:646px; width:288px; center; 'Instrument Sans' 500; letter-spacing:.0081em; color:#1A2A2F; + text-shadow: 0 5px 6px rgba(26,42,47,.65), 0 0 7px rgba(251,244,222,.35), 0 0 12px rgba(251,244,222,.25), 0 0 17px rgba(251,244,222,.2) } +.cl { block; line-height:45px } .g { inline-block; opacity:0; animation:capG 100ms cubic-bezier(.2,.7,.3,1) forwards } +.acc { 'Instrument Serif'; italic } +.cur { inline-block; width:2px; height:.8em; margin:0 -3px 0 1px; background:#4C6274; opacity:0; animation:capCur linear forwards } +@keyframes capCur { 0%,99.9%{opacity:.82} 100%{opacity:0} } +``` + +## 3. PER-BEAT ASSEMBLY + +- 34px; advance 0.52em → maxChars = floor(288 / (0.52 × 34)) = 16 per line, 2 lines a page (cap + 2×maxChars − 2, 9 units); overflow → demote ×0.92 (pitch scales with the font). +- Shadow stack = the preset's four layers, blur and offset as fractions of the 480px canvas width + (0.012 / 0.015 / 0.025 / 0.035 blur, 0.01 offset). + +## 4. WORDS + TIMING + +One span per word (glyph spans where the recipe types), `animation-delay` = `delayMs` VERBATIM; the cue gets `{cueDelayMs}` and a window running to the next beat's start (the last beat to the video's end). Mid-beat pages switch at the successor's first word. + +## 5. EMPHASIS + +Glyph k of a word fires at `delayMs` + 36k; its caret runs to the next glyph, the word's last caret to the next word (or the page end). Accent = `accentIndex` → `.acc` on every glyph of the unit. + +## BOUNDED VARIETY + +None. + +## 6. VERIFY LOOP + +``` +{repo}/.veed-engine/veed-engine-cli {repo}/runs//final --verify +``` +bounds on a `b{N}…` id → the runner demotes that beat one row (font × 0.92, capacities recomputed) and +regenerates; ≤ 2 cycles → `--record`. Manifest: `{"render":{"width":W,"height":H,"fps":FPS,"duration":DUR}}`. +Ids sit on the element that wraps the text; demotion is keyed per beat. + +## 7. DO NOT + +No fonts, colours or keyframes beyond this sheet; no invented timing; no reading the frames; no depth +compositing (layers the source composited behind the presenter are drawn on top here); never hand-edit +the output. diff --git a/refs/html/template-042/recipe.ts b/refs/html/template-042/recipe.ts new file mode 100644 index 0000000..c0e1d4e --- /dev/null +++ b/refs/html/template-042/recipe.ts @@ -0,0 +1,109 @@ +/** + * template-042 "Driftwood": dark slate Instrument Sans typed glyph by glyph in a fixed centred block + * below mid-frame, a thin grey-blue caret trailing each glyph; the beat's strongest word switches to + * Instrument Serif italic on the same baseline. Type is grounded by a stacked cream glow over a soft + * dark drop shadow (the preset's updated shadow stack), never a plate. + */ +import { accentIndex } from '../../../pipeline/recipes/lib.ts'; +import { + type Unit, + DEMOTE_STEP, + REF_W, + canvasFor, + cueDiv, + docShell, + eachBeat, + escapeHtml, + pageGateStyle, + paginate, + templateRecipe, + unitDelay, + wrapByChars, +} from '../../../pipeline/recipes/template-lib.ts'; + +const FONT = 34; +const AVG_EM = 0.52; // Instrument Sans 500 mixed case +const LEFT = 96; +const TOP = 646; +const WIDTH = 288; +const PITCH = 45; +const MAX_LINES = 2; +const GLYPH_STEP = 36; +// The preset's shadow stack: blur/offset as fractions of the source canvas width. +const SHADOWS = [ + { blur: 0.012, dy: 0.01, color: 'rgba(26,42,47,0.65)' }, + { blur: 0.015, dy: 0, color: 'rgba(251,244,222,0.35)' }, + { blur: 0.025, dy: 0, color: 'rgba(251,244,222,0.25)' }, + { blur: 0.035, dy: 0, color: 'rgba(251,244,222,0.2)' }, +]; + +export default templateRecipe('template-042', (meta, timings, opts) => { + const c = canvasFor(meta); + + const cues = eachBeat(meta, timings, opts, 'none', (b) => { + const accent = accentIndex(b.units); + let rows = b.rows; + let f = 1; + let maxChars = 0; + let pages: Unit[][] = []; + let pageLines: Unit[][][] = []; + for (; rows <= b.rows + 4; rows++) { + f = Math.pow(DEMOTE_STEP, rows); + maxChars = Math.floor(WIDTH / (AVG_EM * FONT * f)); + pages = paginate(b.units, maxChars * MAX_LINES - 2, 9); + pageLines = pages.map((p) => wrapByChars(p, maxChars)); + if (pageLines.every((ls) => ls.length <= MAX_LINES)) break; + } + const fs = Math.round(FONT * f * c.s); + const pitch = Math.round(PITCH * f * c.s); + const pageDivs = pages.map((page, pi) => { + const pageId = `b${b.n}p${pi + 1}`; + const firstDelay = unitDelay(page[0]); + const nextStart = pi + 1 < pages.length ? unitDelay(pages[pi + 1][0]) : null; + const pageEnd = nextStart ?? b.cueEndMs; + const lines = pageLines[pi].map((line, li) => { + const parts = line.map((u) => { + const acc = b.units.indexOf(u) === accent ? ' acc' : ''; + const nextUnit = page[page.indexOf(u) + 1]; + const unitEnd = nextUnit ? unitDelay(nextUnit) : pageEnd; + const glyphs: string[] = []; + for (const s of u.spans) { + const chars = [...s.text]; + chars.forEach((ch, k) => { + const d = s.delayMs + k * GLYPH_STEP; + // the caret sits after this glyph until the next one types; the word's last caret waits for the next word + const caretEnd = k + 1 < chars.length ? d + GLYPH_STEP : Math.max(d + GLYPH_STEP, unitEnd); + glyphs.push(`${escapeHtml(ch)}`); + }); + } + return `${glyphs.join('')}`; + }); + return `
${parts.join(' ')}
`; + }).join('\n '); + return `
\n ${lines}\n
`; + }); + return cueDiv(b.n, b.cueDelayMs, b.winMs, pageDivs.join('\n')); + }); + + const shadow = SHADOWS.map((s) => `0 ${c.px(s.dy * REF_W)}px ${c.px(s.blur * REF_W)}px ${s.color}`).join(', '); + const css = ` + .cap { left:${c.px(LEFT)}px; top:${c.py(TOP)}px; width:${c.px(WIDTH)}px; text-align:center; z-index:2; + font-family:'Instrument Sans'; font-weight:500; letter-spacing:0.0081em; color:#1A2A2F; text-shadow:${shadow}; } + .cl { display:block; } + .u { display:inline-block; white-space:pre; } + .sp { display:inline-block; white-space:pre; } + .g { display:inline-block; opacity:0; animation:capG 100ms cubic-bezier(.2,.7,.3,1) forwards; } + .acc { font-family:'Instrument Serif'; font-style:italic; } + .cur { display:inline-block; width:${c.px(2)}px; height:0.8em; margin:0 -${c.px(3)}px 0 ${c.px(1)}px; background:#4C6274; opacity:0; + animation-name:capCur; animation-fill-mode:forwards; animation-timing-function:linear; } + @keyframes capG { from{opacity:0} to{opacity:1} } + @keyframes capCur { 0%{opacity:0.82} 99.9%{opacity:0.82} 100%{opacity:0} }`; + + return docShell({ + canvas: c, + videoPath: meta.videoPath, + fonts: ['Instrument+Sans:wght@500', 'Instrument+Serif:ital@1'], + css, + body: cues.join('\n'), + }); +}); diff --git a/refs/html/template-042/template.wv b/refs/html/template-042/template.wv new file mode 100644 index 0000000..7195d6b --- /dev/null +++ b/refs/html/template-042/template.wv @@ -0,0 +1,73 @@ + + + + + + +
+
+
Every piece we
+
make started on
+
+
+
+
+
a beach exactly
+
like this one.
+
+
+
+
+
5 tons of ocean
+
plastic so far
+
+
+
pulled
+
+
+
+
+
from the water
+
and spun into
+
+
+
thread.
+
+
+
+
+
The ocean gave
+
us the brand.
+
+
+
+
+
The least we
+
can do is give
+
+
+
something back.
+
+
diff --git a/refs/html/template-042/transcript.json b/refs/html/template-042/transcript.json new file mode 100644 index 0000000..22b6a03 --- /dev/null +++ b/refs/html/template-042/transcript.json @@ -0,0 +1,353 @@ +{ + "text": "Every piece we make started on a beach exactly like this one. 5 tons of ocean plastic so far pulled from the water and spun into thread. The ocean gave us the brand. The least we can do is give something back.", + "chunks": [ + { + "text": "Every piece we make started on", + "timestamp": [ + 0.16, + 1.64 + ], + "words": [ + { + "text": "Every", + "timestamp": [ + 0.16, + 0.32 + ] + }, + { + "text": "piece", + "timestamp": [ + 0.32, + 0.64 + ] + }, + { + "text": "we", + "timestamp": [ + 0.64, + 0.84 + ] + }, + { + "text": "make", + "timestamp": [ + 0.84, + 1.08 + ] + }, + { + "text": "started", + "timestamp": [ + 1.08, + 1.4 + ] + }, + { + "text": "on", + "timestamp": [ + 1.4, + 1.64 + ] + } + ] + }, + { + "text": "a beach exactly like this one.", + "timestamp": [ + 1.64, + 3.76 + ], + "words": [ + { + "text": "a", + "timestamp": [ + 1.64, + 1.76 + ] + }, + { + "text": "beach", + "timestamp": [ + 1.76, + 2.08 + ] + }, + { + "text": "exactly", + "timestamp": [ + 2.08, + 2.68 + ] + }, + { + "text": "like", + "timestamp": [ + 2.68, + 2.88 + ] + }, + { + "text": "this", + "timestamp": [ + 2.88, + 3.12 + ] + }, + { + "text": "one.", + "timestamp": [ + 3.12, + 3.76 + ] + } + ] + }, + { + "text": "5 tons of ocean plastic so far pulled", + "timestamp": [ + 3.76, + 6.68 + ], + "words": [ + { + "text": "5", + "timestamp": [ + 3.76, + 4.08 + ] + }, + { + "text": "tons", + "timestamp": [ + 4.08, + 4.44 + ] + }, + { + "text": "of", + "timestamp": [ + 4.44, + 4.6 + ] + }, + { + "text": "ocean", + "timestamp": [ + 4.6, + 5.04 + ] + }, + { + "text": "plastic", + "timestamp": [ + 5.04, + 5.68 + ] + }, + { + "text": "so", + "timestamp": [ + 5.68, + 5.96 + ] + }, + { + "text": "far", + "timestamp": [ + 5.96, + 6.24 + ] + }, + { + "text": "pulled", + "timestamp": [ + 6.24, + 6.68 + ] + } + ] + }, + { + "text": "from the water and spun into thread.", + "timestamp": [ + 6.68, + 8.88 + ], + "words": [ + { + "text": "from", + "timestamp": [ + 6.68, + 6.8 + ] + }, + { + "text": "the", + "timestamp": [ + 6.8, + 6.92 + ] + }, + { + "text": "water", + "timestamp": [ + 6.92, + 7.16 + ] + }, + { + "text": "and", + "timestamp": [ + 7.16, + 7.44 + ] + }, + { + "text": "spun", + "timestamp": [ + 7.44, + 7.8 + ] + }, + { + "text": "into", + "timestamp": [ + 7.8, + 8.16 + ] + }, + { + "text": "thread.", + "timestamp": [ + 8.16, + 8.88 + ] + } + ] + }, + { + "text": "The ocean gave us the brand.", + "timestamp": [ + 8.88, + 10.8 + ], + "words": [ + { + "text": "The", + "timestamp": [ + 8.88, + 9.2 + ] + }, + { + "text": "ocean", + "timestamp": [ + 9.2, + 9.64 + ] + }, + { + "text": "gave", + "timestamp": [ + 9.64, + 9.88 + ] + }, + { + "text": "us", + "timestamp": [ + 9.88, + 10.08 + ] + }, + { + "text": "the", + "timestamp": [ + 10.08, + 10.28 + ] + }, + { + "text": "brand.", + "timestamp": [ + 10.28, + 10.8 + ] + } + ] + }, + { + "text": "The least we can do is give something back.", + "timestamp": [ + 10.8, + 13.8 + ], + "words": [ + { + "text": "The", + "timestamp": [ + 10.8, + 11.08 + ] + }, + { + "text": "least", + "timestamp": [ + 11.08, + 11.32 + ] + }, + { + "text": "we", + "timestamp": [ + 11.32, + 11.56 + ] + }, + { + "text": "can", + "timestamp": [ + 11.56, + 11.76 + ] + }, + { + "text": "do", + "timestamp": [ + 11.76, + 12 + ] + }, + { + "text": "is", + "timestamp": [ + 12, + 12.28 + ] + }, + { + "text": "give", + "timestamp": [ + 12.28, + 12.64 + ] + }, + { + "text": "something", + "timestamp": [ + 12.64, + 13.12 + ] + }, + { + "text": "back.", + "timestamp": [ + 13.12, + 13.8 + ] + } + ] + } + ] +} diff --git a/refs/html/template-042/word-timings.json b/refs/html/template-042/word-timings.json new file mode 100644 index 0000000..c807b0a --- /dev/null +++ b/refs/html/template-042/word-timings.json @@ -0,0 +1,226 @@ +{ + "beats": [ + { + "i": 1, + "startSec": 0.16, + "endSec": 1.64, + "cueDelayMs": 160, + "cueDurMs": 1480, + "words": [ + { + "w": "Every", + "delayMs": 160 + }, + { + "w": "piece", + "delayMs": 320 + }, + { + "w": "we", + "delayMs": 640 + }, + { + "w": "make", + "delayMs": 840 + }, + { + "w": "started", + "delayMs": 1080 + }, + { + "w": "on", + "delayMs": 1400 + } + ] + }, + { + "i": 2, + "startSec": 1.64, + "endSec": 3.76, + "cueDelayMs": 1640, + "cueDurMs": 2120, + "words": [ + { + "w": "a", + "delayMs": 1640 + }, + { + "w": "beach", + "delayMs": 1760 + }, + { + "w": "exactly", + "delayMs": 2080 + }, + { + "w": "like", + "delayMs": 2680 + }, + { + "w": "this", + "delayMs": 2880 + }, + { + "w": "one.", + "delayMs": 3120 + } + ] + }, + { + "i": 3, + "startSec": 3.76, + "endSec": 6.68, + "cueDelayMs": 3760, + "cueDurMs": 2920, + "words": [ + { + "w": "5", + "delayMs": 3760 + }, + { + "w": "tons", + "delayMs": 4080 + }, + { + "w": "of", + "delayMs": 4440 + }, + { + "w": "ocean", + "delayMs": 4600 + }, + { + "w": "plastic", + "delayMs": 5040 + }, + { + "w": "so", + "delayMs": 5680 + }, + { + "w": "far", + "delayMs": 5960 + }, + { + "w": "pulled", + "delayMs": 6240 + } + ] + }, + { + "i": 4, + "startSec": 6.68, + "endSec": 8.88, + "cueDelayMs": 6680, + "cueDurMs": 2200, + "words": [ + { + "w": "from", + "delayMs": 6680 + }, + { + "w": "the", + "delayMs": 6800 + }, + { + "w": "water", + "delayMs": 6920 + }, + { + "w": "and", + "delayMs": 7160 + }, + { + "w": "spun", + "delayMs": 7440 + }, + { + "w": "into", + "delayMs": 7800 + }, + { + "w": "thread.", + "delayMs": 8160 + } + ] + }, + { + "i": 5, + "startSec": 8.88, + "endSec": 10.8, + "cueDelayMs": 8880, + "cueDurMs": 1920, + "words": [ + { + "w": "The", + "delayMs": 8880 + }, + { + "w": "ocean", + "delayMs": 9200 + }, + { + "w": "gave", + "delayMs": 9640 + }, + { + "w": "us", + "delayMs": 9880 + }, + { + "w": "the", + "delayMs": 10080 + }, + { + "w": "brand.", + "delayMs": 10280 + } + ] + }, + { + "i": 6, + "startSec": 10.8, + "endSec": 13.8, + "cueDelayMs": 10800, + "cueDurMs": 3000, + "words": [ + { + "w": "The", + "delayMs": 10800 + }, + { + "w": "least", + "delayMs": 11080 + }, + { + "w": "we", + "delayMs": 11320 + }, + { + "w": "can", + "delayMs": 11560 + }, + { + "w": "do", + "delayMs": 11760 + }, + { + "w": "is", + "delayMs": 12000 + }, + { + "w": "give", + "delayMs": 12280 + }, + { + "w": "something", + "delayMs": 12640 + }, + { + "w": "back.", + "delayMs": 13120 + } + ] + } + ] +} diff --git a/refs/html/template-043/recipe.md b/refs/html/template-043/recipe.md new file mode 100644 index 0000000..39a5860 --- /dev/null +++ b/refs/html/template-043/recipe.md @@ -0,0 +1,62 @@ +# RECIPE — template-043 (9:16 · 736×1312 @ 30fps) — Script hero + +RESOLUTION: px below are authored at the source template's own canvas, 480×864; the compiled module +scales them per run (`pipeline/recipes/template-lib.ts`, `canvasFor`): portrait SCALE = W/480 for sizes +and horizontal offsets, H/864 for vertical anchors; landscape SCALE = (H/864)×1.6. `em` values and ms +timings never scale; the manifest carries the run's real W/H. The reference 9:16 canvas 736×1312 is +SCALE 1.533. The shell (reset · body · .vid · cueWin/.cue · .pg with pgOn/pgOff/pgMid) is the shared +one in `template-lib.ts` `docShell`; the SKELETON lists what this recipe adds. + +## 1. IDENTITY + +Centred EB Garamond caps typed glyph by glyph (34ms) with a honey caret, in a lockup of two blocks: +the words before the beat's strongest word in the upper block, the words after it in the lower one, +and the word itself handwritten between them in large honey Pinyon Script, scaling in over 640ms. The +closing beat's hero is set larger. + +## 2. SKELETON + +``` +fonts: EB+Garamond:wght@600 · Pinyon+Script +.cap { left:53px; right:53px; center; 'EB Garamond' 600; letter-spacing:.06em; line-height:1.3; color:#F7F0E4; + text-shadow:0 2px 9px rgba(4,3,2,.38), 0 1px 2px rgba(4,3,2,.3) } upper block bottom:328px · lower block bottom:152px +.g { inline-block; opacity:0; animation:capG 120ms cubic-bezier(.2,.7,.3,1) forwards } +.cur { inline-block; width:2px; height:.85em; margin-left:2px; text-bottom; background:#FBF684; opacity:0; animation:capCur linear forwards } +.hrow { absolute; left:0; width:480px; top:540px; center; nowrap; z-index:3; 'Pinyon Script'; line-height:1.2; color:#FBF684; + text-shadow:0 3px 15px rgba(4,3,2,.38), 0 2px 3px rgba(4,3,2,.3) } +.hw { inline-block; opacity:0; transform-origin:50% 60%; animation:heroIn 640ms cubic-bezier(.2,.7,.3,1) both } @keyframes heroIn { 0%{opacity:0; scale(.94)} } +``` + +## 3. PER-BEAT ASSEMBLY + +- 26px; advance 0.56em → maxChars = floor(374 / (0.56 × 26)) = 25 per line; each block ≤ 2 lines; + overflow of either → demote ×0.92 (5 rows). Each block is its own gate from its first word to the + beat's end. +- Hero 72px (88px on the last beat) Pinyon, lowercase, advance 0.42em, shrunk to fit 432px. + +## 4. WORDS + TIMING + +One span per word (glyph spans where the recipe types), `animation-delay` = `delayMs` VERBATIM; the cue gets `{cueDelayMs}` and a window running to the next beat's start (the last beat to the video's end). Mid-beat pages switch at the successor's first word. + +## 5. EMPHASIS + +Hero = `accentIndex` of beats with ≥ 2 units, REMOVED from the caption. The caret after each word runs from the word's delay to the next word's (the block's last to the beat's end). + +## BOUNDED VARIETY + +None. + +## 6. VERIFY LOOP + +``` +{repo}/.veed-engine/veed-engine-cli {repo}/runs//final --verify +``` +bounds on a `b{N}…` id → the runner demotes that beat one row (font × 0.92, capacities recomputed) and +regenerates; ≤ 2 cycles → `--record`. Manifest: `{"render":{"width":W,"height":H,"fps":FPS,"duration":DUR}}`. +Ids sit on the element that wraps the text; demotion is keyed per beat. + +## 7. DO NOT + +No fonts, colours or keyframes beyond this sheet; no invented timing; no reading the frames; no depth +compositing (layers the source composited behind the presenter are drawn on top here); never hand-edit +the output. diff --git a/refs/html/template-043/recipe.ts b/refs/html/template-043/recipe.ts new file mode 100644 index 0000000..9dffd0e --- /dev/null +++ b/refs/html/template-043/recipe.ts @@ -0,0 +1,106 @@ +/** + * template-043 "Script hero": centred EB Garamond caps typed glyph by glyph with a honey caret, in a + * lockup of two blocks; the beat's strongest word leaves the caption and is handwritten between them + * in large honey Pinyon Script, scaling in. Words before the hero sit in the upper block, words after + * it in the lower one. + */ +import { accentIndex } from '../../../pipeline/recipes/lib.ts'; +import { + type Unit, + DEMOTE_STEP, + canvasFor, + cueDiv, + docShell, + eachBeat, + escapeHtml, + templateRecipe, + unitDelay, + unitText, + wrapByChars, +} from '../../../pipeline/recipes/template-lib.ts'; + +const FONT = 26; +const AVG_EM = 0.56; // EB Garamond 600 caps + 0.06em tracking +const MARGIN = 53; +const BOTTOM_K = 328; // the upper block (words before the hero) +const BOTTOM_C = 152; // the lower block (words after) +const HERO_TOP = 540; +const HERO_FONT = 72; +const HERO_FONT_LAST = 88; +const HERO_EM = 0.42; // Pinyon Script +const GLYPH_STEP = 34; +const MAX_LINES = 2; + +export default templateRecipe('template-043', (meta, timings, opts) => { + const c = canvasFor(meta); + const measure = c.refWidth - 2 * MARGIN; + + const cues = eachBeat(meta, timings, opts, 'uppercase', (b) => { + const heroIdx = b.units.length >= 2 ? accentIndex(b.units) : -1; + const before = heroIdx >= 0 ? b.units.slice(0, heroIdx) : b.units; + const after = heroIdx >= 0 ? b.units.slice(heroIdx + 1) : []; + let rows = b.rows; + let f = 1; + let maxChars = 0; + let kLines: Unit[][] = []; + let cLines: Unit[][] = []; + for (; rows <= b.rows + 5; rows++) { + f = Math.pow(DEMOTE_STEP, rows); + maxChars = Math.floor(measure / (AVG_EM * FONT * f)); + kLines = before.length ? wrapByChars(before, maxChars) : []; + cLines = after.length ? wrapByChars(after, maxChars) : []; + if (kLines.length <= MAX_LINES && cLines.length <= MAX_LINES) break; + } + const fs = Math.round(FONT * f * c.s); + + // typed glyphs with the caret parked after the last typed one until the next word starts + const block = (units: Unit[], lines: Unit[][], id: string, bottom: number) => { + if (!units.length) return ''; + const start = unitDelay(units[0]); + const lineDivs = lines.map((line, li) => { + const parts = line.map((u, ui) => { + const isLastInBlock = u === units[units.length - 1]; + const next = isLastInBlock ? b.cueEndMs : unitDelay(units[units.indexOf(u) + 1]); + const glyphs = u.spans.map((s) => [...s.text].map((ch, k) => `${escapeHtml(ch)}`).join('')).join(''); + const d = unitDelay(u); + return `${glyphs}`; + }); + return `
${parts.join(' ')}
`; + }).join('\n '); + return `
\n ${lineDivs}\n
\n`; + }; + let heroHtml = ''; + if (heroIdx >= 0) { + const hero = b.units[heroIdx]; + const text = unitText(hero).toLowerCase(); + const base = b.isLast ? HERO_FONT_LAST : HERO_FONT; + const font = Math.min(base, (c.refWidth - 2 * 24) / (HERO_EM * text.length)) * f; + heroHtml = `
${escapeHtml(text)}
\n`; + } + return cueDiv(b.n, b.cueDelayMs, b.winMs, block(before, kLines, `b${b.n}k`, BOTTOM_K) + heroHtml + block(after, cLines, `b${b.n}c`, BOTTOM_C)); + }); + + const css = ` + .cap { left:${c.px(MARGIN)}px; right:${c.px(MARGIN)}px; text-align:center; z-index:2; font-family:'EB Garamond'; font-weight:600; letter-spacing:0.06em; + line-height:1.3; color:#F7F0E4; text-shadow:0 ${c.px(2)}px ${c.px(9)}px rgba(4,3,2,0.38), 0 ${c.px(1)}px ${c.px(2)}px rgba(4,3,2,0.3); } + .cl { display:block; } + .u { display:inline-block; white-space:pre; } + .sp { display:inline-block; white-space:pre; } + .g { display:inline-block; opacity:0; animation:capG 120ms cubic-bezier(.2,.7,.3,1) forwards; } + .cur { display:inline-block; width:${c.px(2)}px; height:0.85em; margin-left:${c.px(2)}px; vertical-align:text-bottom; background:#FBF684; opacity:0; + animation-name:capCur; animation-fill-mode:forwards; animation-timing-function:linear; } + @keyframes capG { from{opacity:0} to{opacity:1} } + @keyframes capCur { 0%{opacity:1} 99.9%{opacity:1} 100%{opacity:0} } + .hrow { position:absolute; left:0; width:${c.W}px; text-align:center; white-space:nowrap; z-index:3; font-family:'Pinyon Script'; font-weight:400; line-height:1.2; + color:#FBF684; text-shadow:0 ${c.px(3)}px ${c.px(15)}px rgba(4,3,2,0.38), 0 ${c.px(2)}px ${c.px(3)}px rgba(4,3,2,0.3); } + .hw { display:inline-block; white-space:pre; opacity:0; transform-origin:50% 60%; animation:heroIn 640ms cubic-bezier(.2,.7,.3,1) both; } + @keyframes heroIn { 0%{opacity:0; transform:scale(0.94)} 100%{opacity:1; transform:scale(1)} }`; + + return docShell({ + canvas: c, + videoPath: meta.videoPath, + fonts: ['EB+Garamond:wght@600', 'Pinyon+Script'], + css, + body: cues.join('\n'), + }); +}); diff --git a/refs/html/template-043/template.wv b/refs/html/template-043/template.wv new file mode 100644 index 0000000..fa507fe --- /dev/null +++ b/refs/html/template-043/template.wv @@ -0,0 +1,84 @@ + + + + + + +
+
+
MY MOTHER WROTE
+
HER RECIPES ON
+
+
whatever
+ +
+
+
+
PAPER THE
+
+
kitchen
+
+
HAD THAT DAY.
+
+ +
+
+
60
+
+
YEARS LATER,
+
WE STILL BAKE
+
+ +
+
+
+
FROM THOSE PAGES,
+
+
stains
+
+
AND ALL.
+
+ +
+
+
+
WE COULD
+
+
modernize
+
+
IT. WE NEVER WILL.
+
+ +
+
+
+
THAT IS THE WHOLE
+
+
point.
+ +
diff --git a/refs/html/template-043/transcript.json b/refs/html/template-043/transcript.json new file mode 100644 index 0000000..d6655f7 --- /dev/null +++ b/refs/html/template-043/transcript.json @@ -0,0 +1,318 @@ +{ + "text": "MY MOTHER WROTE HER RECIPES ON WHATEVER PAPER THE KITCHEN HAD THAT DAY. 60 YEARS LATER, WE STILL BAKE FROM THOSE PAGES, STAINS AND ALL. WE COULD MODERNIZE IT. WE NEVER WILL. THAT IS THE WHOLE POINT.", + "chunks": [ + { + "text": "MY MOTHER WROTE HER RECIPES ON WHATEVER", + "timestamp": [ + 0.16, + 2.32 + ], + "words": [ + { + "text": "MY", + "timestamp": [ + 0.16, + 0.28 + ] + }, + { + "text": "MOTHER", + "timestamp": [ + 0.28, + 0.52 + ] + }, + { + "text": "WROTE", + "timestamp": [ + 0.52, + 0.76 + ] + }, + { + "text": "HER", + "timestamp": [ + 0.76, + 0.92 + ] + }, + { + "text": "RECIPES", + "timestamp": [ + 0.92, + 1.44 + ] + }, + { + "text": "ON", + "timestamp": [ + 1.44, + 1.84 + ] + }, + { + "text": "WHATEVER", + "timestamp": [ + 1.84, + 2.32 + ] + } + ] + }, + { + "text": "PAPER THE KITCHEN HAD THAT DAY.", + "timestamp": [ + 2.32, + 4.387 + ], + "words": [ + { + "text": "PAPER", + "timestamp": [ + 2.32, + 2.64 + ] + }, + { + "text": "THE", + "timestamp": [ + 2.64, + 2.84 + ] + }, + { + "text": "KITCHEN", + "timestamp": [ + 2.84, + 3.2 + ] + }, + { + "text": "HAD", + "timestamp": [ + 3.2, + 3.44 + ] + }, + { + "text": "THAT", + "timestamp": [ + 3.44, + 3.64 + ] + }, + { + "text": "DAY.", + "timestamp": [ + 3.64, + 4.387 + ] + } + ] + }, + { + "text": "60 YEARS LATER, WE STILL BAKE", + "timestamp": [ + 4.4, + 5.987 + ], + "words": [ + { + "text": "60", + "timestamp": [ + 4.4, + 4.76 + ] + }, + { + "text": "YEARS", + "timestamp": [ + 4.76, + 5.04 + ] + }, + { + "text": "LATER,", + "timestamp": [ + 5.04, + 5.28 + ] + }, + { + "text": "WE", + "timestamp": [ + 5.28, + 5.52 + ] + }, + { + "text": "STILL", + "timestamp": [ + 5.52, + 5.72 + ] + }, + { + "text": "BAKE", + "timestamp": [ + 5.72, + 5.987 + ] + } + ] + }, + { + "text": "FROM THOSE PAGES, STAINS AND ALL.", + "timestamp": [ + 6, + 8.16 + ], + "words": [ + { + "text": "FROM", + "timestamp": [ + 6, + 6.2 + ] + }, + { + "text": "THOSE", + "timestamp": [ + 6.2, + 6.44 + ] + }, + { + "text": "PAGES,", + "timestamp": [ + 6.44, + 7.04 + ] + }, + { + "text": "STAINS", + "timestamp": [ + 7.04, + 7.52 + ] + }, + { + "text": "AND", + "timestamp": [ + 7.52, + 7.72 + ] + }, + { + "text": "ALL.", + "timestamp": [ + 7.72, + 8.16 + ] + } + ] + }, + { + "text": "WE COULD MODERNIZE IT. WE NEVER WILL.", + "timestamp": [ + 8.16, + 10.48 + ], + "words": [ + { + "text": "WE", + "timestamp": [ + 8.16, + 8.44 + ] + }, + { + "text": "COULD", + "timestamp": [ + 8.44, + 8.6 + ] + }, + { + "text": "MODERNIZE", + "timestamp": [ + 8.6, + 9.16 + ] + }, + { + "text": "IT.", + "timestamp": [ + 9.16, + 9.44 + ] + }, + { + "text": "WE", + "timestamp": [ + 9.44, + 9.72 + ] + }, + { + "text": "NEVER", + "timestamp": [ + 9.72, + 9.96 + ] + }, + { + "text": "WILL.", + "timestamp": [ + 9.96, + 10.48 + ] + } + ] + }, + { + "text": "THAT IS THE WHOLE POINT.", + "timestamp": [ + 10.48, + 12.033 + ], + "words": [ + { + "text": "THAT", + "timestamp": [ + 10.48, + 10.76 + ] + }, + { + "text": "IS", + "timestamp": [ + 10.76, + 10.92 + ] + }, + { + "text": "THE", + "timestamp": [ + 10.92, + 11.08 + ] + }, + { + "text": "WHOLE", + "timestamp": [ + 11.08, + 11.36 + ] + }, + { + "text": "POINT.", + "timestamp": [ + 11.36, + 12.033 + ] + } + ] + } + ] +} diff --git a/refs/html/template-043/word-timings.json b/refs/html/template-043/word-timings.json new file mode 100644 index 0000000..2defb93 --- /dev/null +++ b/refs/html/template-043/word-timings.json @@ -0,0 +1,206 @@ +{ + "beats": [ + { + "i": 1, + "startSec": 0.16, + "endSec": 2.32, + "cueDelayMs": 160, + "cueDurMs": 2160, + "words": [ + { + "w": "MY", + "delayMs": 160 + }, + { + "w": "MOTHER", + "delayMs": 280 + }, + { + "w": "WROTE", + "delayMs": 520 + }, + { + "w": "HER", + "delayMs": 760 + }, + { + "w": "RECIPES", + "delayMs": 920 + }, + { + "w": "ON", + "delayMs": 1440 + }, + { + "w": "WHATEVER", + "delayMs": 1840 + } + ] + }, + { + "i": 2, + "startSec": 2.32, + "endSec": 4.387, + "cueDelayMs": 2320, + "cueDurMs": 2067, + "words": [ + { + "w": "PAPER", + "delayMs": 2320 + }, + { + "w": "THE", + "delayMs": 2640 + }, + { + "w": "KITCHEN", + "delayMs": 2840 + }, + { + "w": "HAD", + "delayMs": 3200 + }, + { + "w": "THAT", + "delayMs": 3440 + }, + { + "w": "DAY.", + "delayMs": 3640 + } + ] + }, + { + "i": 3, + "startSec": 4.4, + "endSec": 5.987, + "cueDelayMs": 4400, + "cueDurMs": 1587, + "words": [ + { + "w": "60", + "delayMs": 4400 + }, + { + "w": "YEARS", + "delayMs": 4760 + }, + { + "w": "LATER,", + "delayMs": 5040 + }, + { + "w": "WE", + "delayMs": 5280 + }, + { + "w": "STILL", + "delayMs": 5520 + }, + { + "w": "BAKE", + "delayMs": 5720 + } + ] + }, + { + "i": 4, + "startSec": 6, + "endSec": 8.16, + "cueDelayMs": 6000, + "cueDurMs": 2160, + "words": [ + { + "w": "FROM", + "delayMs": 6000 + }, + { + "w": "THOSE", + "delayMs": 6200 + }, + { + "w": "PAGES,", + "delayMs": 6440 + }, + { + "w": "STAINS", + "delayMs": 7040 + }, + { + "w": "AND", + "delayMs": 7520 + }, + { + "w": "ALL.", + "delayMs": 7720 + } + ] + }, + { + "i": 5, + "startSec": 8.16, + "endSec": 10.48, + "cueDelayMs": 8160, + "cueDurMs": 2320, + "words": [ + { + "w": "WE", + "delayMs": 8160 + }, + { + "w": "COULD", + "delayMs": 8440 + }, + { + "w": "MODERNIZE", + "delayMs": 8600 + }, + { + "w": "IT.", + "delayMs": 9160 + }, + { + "w": "WE", + "delayMs": 9440 + }, + { + "w": "NEVER", + "delayMs": 9720 + }, + { + "w": "WILL.", + "delayMs": 9960 + } + ] + }, + { + "i": 6, + "startSec": 10.48, + "endSec": 12.033, + "cueDelayMs": 10480, + "cueDurMs": 1553, + "words": [ + { + "w": "THAT", + "delayMs": 10480 + }, + { + "w": "IS", + "delayMs": 10760 + }, + { + "w": "THE", + "delayMs": 10920 + }, + { + "w": "WHOLE", + "delayMs": 11080 + }, + { + "w": "POINT.", + "delayMs": 11360 + } + ] + } + ] +} diff --git a/refs/tags.json b/refs/tags.json index f606532..7c997f7 100644 --- a/refs/tags.json +++ b/refs/tags.json @@ -21,10 +21,13 @@ "doodle-overlay", "echo-trail", "framed-window", + "ghost-ink", "glow-text", + "hero-title", "laurel-badge", "speech-bubble", - "torn-paper" + "torn-paper", + "word-plate" ], "layout": [ "flat-bg", @@ -46,6 +49,7 @@ "scattered-kinetic", "scattered-suspect", "stop-motion", + "word-light", "word-pop-active", "word-rise", "word-thru" @@ -807,6 +811,384 @@ "9:16" ] } + }, + { + "id": "template-030", + "wv": "refs/html/template-030/template.wv", + "facets": { + "type": [ + "two-font-combo" + ], + "device": [ + "word-plate" + ], + "layout": [ + "lower-third", + "scattered" + ], + "motion": [ + "word-rise" + ], + "energy": [ + "playful" + ], + "fit": [ + "9:16" + ] + } + }, + { + "id": "template-031", + "wv": "refs/html/template-031/template.wv", + "facets": { + "type": [ + "clean-sans", + "two-font-combo" + ], + "device": [ + "hero-title" + ], + "layout": [ + "lower-third", + "one-big-bold" + ], + "motion": [ + "char-rise" + ], + "energy": [ + "clean", + "premium" + ], + "fit": [ + "9:16" + ] + } + }, + { + "id": "template-032", + "wv": "refs/html/template-032/template.wv", + "facets": { + "type": [ + "clean-sans", + "two-font-combo" + ], + "device": [ + "hero-title" + ], + "layout": [ + "lower-third", + "one-big-bold" + ], + "motion": [ + "char-fade" + ], + "energy": [ + "calm", + "premium" + ], + "fit": [ + "9:16" + ] + } + }, + { + "id": "template-033", + "wv": "refs/html/template-033/template.wv", + "facets": { + "type": [ + "editorial-serif" + ], + "device": [ + "ghost-ink" + ], + "layout": [ + "lower-third", + "stacked-center" + ], + "motion": [ + "word-light" + ], + "energy": [ + "calm", + "elegant" + ], + "fit": [ + "9:16" + ] + } + }, + { + "id": "template-034", + "wv": "refs/html/template-034/template.wv", + "facets": { + "type": [ + "brutalist-caps" + ], + "device": [ + "word-plate", + "counter-accent" + ], + "layout": [ + "lower-third", + "one-big-bold" + ], + "motion": [ + "word-rise", + "word-pop-active" + ], + "energy": [ + "punchy", + "loud" + ], + "fit": [ + "9:16" + ] + } + }, + { + "id": "template-035", + "wv": "refs/html/template-035/template.wv", + "facets": { + "type": [ + "brutalist-caps", + "two-font-combo" + ], + "device": [ + "hero-title" + ], + "layout": [ + "lower-third", + "stacked-center" + ], + "motion": [ + "word-rise", + "scale-in" + ], + "energy": [ + "punchy", + "raw" + ], + "fit": [ + "9:16" + ] + } + }, + { + "id": "template-036", + "wv": "refs/html/template-036/template.wv", + "facets": { + "type": [ + "tech-mono", + "two-font-combo" + ], + "device": [ + "hero-title", + "word-plate" + ], + "layout": [ + "lower-third", + "one-big-bold" + ], + "motion": [ + "word-rise" + ], + "energy": [ + "clean", + "premium" + ], + "fit": [ + "9:16" + ] + } + }, + { + "id": "template-037", + "wv": "refs/html/template-037/template.wv", + "facets": { + "type": [ + "tech-mono", + "brutalist-caps" + ], + "device": [ + "hero-title", + "word-plate" + ], + "layout": [ + "stacked-center", + "one-big-bold" + ], + "motion": [ + "word-rise" + ], + "energy": [ + "loud", + "punchy" + ], + "fit": [ + "9:16" + ] + } + }, + { + "id": "template-038", + "wv": "refs/html/template-038/template.wv", + "facets": { + "type": [ + "tech-mono", + "brutalist-caps" + ], + "device": [ + "word-plate", + "hero-title" + ], + "layout": [ + "lower-third", + "one-big-bold" + ], + "motion": [ + "word-rise" + ], + "energy": [ + "raw", + "punchy" + ], + "fit": [ + "9:16" + ] + } + }, + { + "id": "template-039", + "wv": "refs/html/template-039/template.wv", + "facets": { + "type": [ + "brutalist-caps" + ], + "device": [ + "word-plate", + "counter-accent" + ], + "layout": [ + "stacked-center", + "one-big-bold" + ], + "motion": [ + "word-rise", + "word-pop-active" + ], + "energy": [ + "loud", + "hype" + ], + "fit": [ + "9:16" + ] + } + }, + { + "id": "template-040", + "wv": "refs/html/template-040/template.wv", + "facets": { + "type": [ + "clean-sans" + ], + "device": [ + "counter-accent" + ], + "layout": [ + "lower-third" + ], + "motion": [ + "word-rise" + ], + "energy": [ + "clean", + "calm" + ], + "fit": [ + "9:16" + ] + } + }, + { + "id": "template-041", + "wv": "refs/html/template-041/template.wv", + "facets": { + "type": [ + "editorial-serif", + "italic-serif" + ], + "device": [ + "counter-accent" + ], + "layout": [ + "stacked-center" + ], + "motion": [ + "word-rise" + ], + "energy": [ + "elegant", + "calm" + ], + "fit": [ + "9:16" + ] + } + }, + { + "id": "template-042", + "wv": "refs/html/template-042/template.wv", + "facets": { + "type": [ + "clean-sans", + "italic-serif" + ], + "device": [ + "counter-accent" + ], + "layout": [ + "stacked-center" + ], + "motion": [ + "char-fade" + ], + "energy": [ + "calm", + "premium" + ], + "fit": [ + "9:16" + ] + } + }, + { + "id": "template-043", + "wv": "refs/html/template-043/template.wv", + "facets": { + "type": [ + "editorial-serif", + "script-handwritten" + ], + "device": [ + "hero-title" + ], + "layout": [ + "stacked-center", + "lower-third" + ], + "motion": [ + "char-fade", + "scale-in" + ], + "energy": [ + "elegant", + "premium" + ], + "fit": [ + "9:16" + ] + } } ] }