/** * Pure motion-style registry + concept→animation metaphor map. * * Three styles, each a {accent, visualBlock, motionBlock} record: * - `apple-clean` — ported verbatim from the source skill (Apple-keynote * minimalism with tactile materiality). * - `editorial-dark` — genericized from the source skill's "CODA Dark" * (manifesto / punchline editorial poster aesthetic). CRITICAL: the prose * carries NO numeric specs, hex codes, or font-family names (CRITICAL * RULE 2 — those leak as on-screen text). The accent is a configurable * field, never baked into the prose. * - `knowledge-tool` — genericized from the source skill's "Brain Clone" * (dark-academia / knowledge-graph look). Same no-specs prose discipline. * * The accent default lives in `MOTION_STYLES[id].accent`; `resolveStyle` lets a * caller override it (`--accent`) or supply a brand profile (`--brand`, which * takes precedence over `--accent`). * * Pure: no I/O. Deterministic. */ import type { MotionStyleId, ResolvedStyle } from './types.js'; export interface MotionStyleDef { accent: string; // default accent hex for this style visualBlock: string; // prose — NO px/ms/hex/font-name/easing tokens motionBlock: string; // prose — NO px/ms/hex/font-name/easing tokens } /** * Style registry. The visual/motion prose is what gets sent to Omni, so it must * stay free of any technical metadata (CRITICAL RULE 2). The accent is held * separately as a hex value and is resolved into a HUMAN-NAMED colour at compose * time — it never appears as a hex string in the prompt. */ export const MOTION_STYLES: Record = { // ── apple-clean — ported verbatim from the source skill ────────────────── 'apple-clean': { accent: '#FF5000', visualBlock: 'Style: Apple-keynote minimalism with tactile materiality. Cards are dark frosted glass — they should feel like physical sheets of darkened glass with subtle refraction of the footage visible through them, not flat digital rectangles. Very soft rounded corners, a barely-visible hairline white edge that catches light like a real material seam, and a soft weighted drop shadow suggesting the card is floating slightly above the background. Text is clean modern white sans-serif, mostly sentence case, with category labels in uppercase. Typography itself behaves like an actor: each headline word feels deliberate, with tracking that breathes (slightly tighter on entry, settling into final spacing). The only accent colour is the configured accent — used SOLELY for the asterisk character (*) following key words, or as a thin underline below a single emphasized phrase. Never use the accent to fill or colour entire words; the accent is a beat marker, not decoration. Icons are pure white hairline outlines, never filled, never glowing. Backgrounded elements (cards no longer in focus) carry a subtle depth-of-field blur — the foreground is sharp, the background softens, like a cinematic lens.', motionBlock: 'Motion: Slow, confident, premium — like an Apple product reveal directed with a real camera. Cards and headlines enter with a soft glide that decelerates into rest, NEVER linear motion — always settling under their own weight. Active camera movement is essential: elements use push-in and pull-out as if a slow cinematic lens is approaching and retreating. While an element holds on screen, it carries a slow gentle push-in — scaling up subtly over a beat, as if the camera is moving closer. When an element exits, it pulls back and slips into soft depth-of-field blur, shrinking slightly as it recedes. On key beats (asterisks, numbers, anchor words), a brief emphasis pulse hits the element — a quick scale up and settle that punctuates the rhythm. Idle elements carry a barely-perceptible parallax drift, suggesting the camera is never fully locked, always breathing slightly. Text reveals word by word with a small upward offset; on headline-anchor words (large dominant text), the letter tracking is slightly tight on entry and breathes outward into final spacing as the word settles — letters exhaling into place. Crossfades are smooth, outgoing elements fading while pulling back and slipping out of focus. Icons appear via stroke-on draw animation — lines drawing themselves like a pen on paper, never popping in. Kinetic typography mixes with cards: the speaker\'s anchor words appear as giant headline text dominating the frame, sometimes alone, sometimes alongside an icon — never let a take be only cards with icon labels; intersperse with at least one giant headline-anchor moment that syncs with the speaker\'s emphasis. Critical pacing principle: when the speaker pauses or breathes between sentences, the visual STOPS too — hold the current state silent and still during that beat. Silence before a reveal is more powerful than continuous animation. No bouncy springs, no whip pans, no pop-in, no constant motion filling every second. Subtle motion blur on all transforms.', }, // ── editorial-dark — genericized from "CODA Dark" (no specs / no fonts) ─── 'editorial-dark': { accent: '#FF5000', visualBlock: 'Style: Editorial dark manifesto — high-contrast poster typography in motion. Cards are solid deep-black surfaces with sharp, squared corners, no rounding — an editorial, decisive feel rather than a soft product look. Typography is a bold, condensed grotesque sans-serif set in uppercase with tight tracking and a low, punchy line height, treated like a magazine cover headline. The only accent colour is the configured accent, used SOLELY for a small asterisk (*) after each headline and for a thin underline on key shapes — never to fill whole words. Icons are minimal hairline strokes in the accent on key shapes only. A weighted drop shadow gives each black card real heft so it reads as a stacked poster panel floating above the frame. The overall feeling is a printed manifesto poster brought to life: stark, confident, punchline-driven, generous negative space around a single dominant statement.', motionBlock: 'Motion: Snappy, decisive, high-contrast — an editorial poster animated. Cards and headlines enter fast and crisp, then settle without bounce, NEVER linear motion. Headlines snap in word by word in quick succession, each word landing with a confident stop. Exits are a quick fade with a small upward drift, the panel slipping away cleanly. There is still a real camera: a subtle push-in while a statement holds, a slight pull-back on exit, and a brief emphasis pulse on the asterisk or a key number to mark the beat. Idle panels carry a barely-perceptible parallax drift. Keep the rhythm decisive and poster-like rather than soft and floaty. Kinetic typography dominates: anchor words from the speech appear as giant uppercase headlines filling the frame, sometimes alone, sometimes with one hairline icon — never a take that is only icon-labelled cards. Critical pacing principle: when the speaker pauses, the visual STOPS too — hold the current panel silent and still during that beat. Silence before a reveal lands harder than constant motion. No springs, no whip pans, no pop-in clutter.', }, // ── knowledge-tool — genericized from "Brain Clone" (no specs / no fonts) ─ 'knowledge-tool': { accent: '#A78BFA', visualBlock: 'Style: Dark-academia digital meeting a knowledge-graph aesthetic — a knowledge-tool look, not generic motion graphics. Cards are deep near-black surfaces with the slightest cool blue undertone (never pure black), with very subtle hairline edges and a soft weighted shadow suggesting weight. Two typographic worlds intermix: headline anchors use a classical editorial serif — italic-capable, manuscript-like, the kind of letterforms on the cover of an old hardcover book; card labels and sublines use a clean modern sans-serif; small technical brackets and timestamps use a monospace typewriter-style face like an editor label. The only accent colour is the configured accent — a soft, calm lavender, never neon, never magenta — used SOLELY for the asterisk character (*) after key words and occasionally a thin underline. The asterisks have a slightly hand-drawn, brushy quality, like an ink stroke rather than a perfectly geometric character. The accent is a beat marker, never decoration. Icons are clean hairline outlines in soft, slightly cool off-white. Backgrounded elements carry depth-of-field blur. The composition feels like a knowledge tool: editorial magazine layout, generous negative space, considered hierarchy — not a slick product ad.', motionBlock: 'Motion: Slow, contemplative, scholarly — directed like a documentary about a thinker\'s notebook rather than a product reveal. Cards and headlines enter with a soft glide that decelerates into rest, NEVER linear motion. Active cinematic camera: a slow push-in during a hold, as if a lens is approaching a page; a pull-out on exit where the element shrinks slightly and slips into soft depth-of-field blur; an emphasis pulse on key beats (asterisks, anchor words) — a quick scale up and settle. Idle elements carry a barely-perceptible parallax drift. The serif headline letters enter with tracking slightly tight, then breathe outward into final spacing as the word settles — letters exhaling like the word is being read aloud and considered. The brushy accent asterisk lands distinct from the word, a beat marker punctuating thought. Icons appear via stroke-on draw animation, lines forming like ink on paper. Kinetic typography mixes with cards: anchor words appear as giant serif headlines dominating the frame, sometimes alone, sometimes alongside an icon — never a take that is only cards with icon labels. Critical pacing principle: when the speaker pauses, the visual STOPS too — hold silent and still. Silence before a reveal is more powerful than continuous motion. The overall feeling: a quiet, deliberate study session, premium and considered.', }, }; export interface ResolveStyleOptions { /** Override the style's default accent (e.g. `--accent #00AAFF`). */ accent?: string; /** Brand profile: its accent wins over both the style default AND `accent`. */ brand?: { accent?: string }; } /** * Resolve a style id + optional accent/brand overrides into a `ResolvedStyle`. * * Accent precedence (highest first): `brand.accent` → `accent` → style default. * Pure: returns a fresh object; never mutates the registry. */ export function resolveStyle(id: MotionStyleId, opts: ResolveStyleOptions = {}): ResolvedStyle { const def = MOTION_STYLES[id]; if (!def) { throw new Error(`resolveStyle: unknown style "${id}"`); } const accent = opts.brand?.accent ?? opts.accent ?? def.accent; return { id, accent, visualBlock: def.visualBlock, motionBlock: def.motionBlock, }; } /** * Concept → animation prose. * * Concrete concepts (real-world objects) map to minimalist hairline-icon prose; * abstract concepts (states/processes — AI, clones, intelligence, insight) map * to a MATERIAL METAPHOR (particles / grids / light), never a generic hairline * icon (which reads weak for an abstract idea). Pure — returns prose with no * numeric specs. */ const CONCRETE_ICONS: Record = { carousel: 'three staggered diagonal portrait rectangles, like a deck of slides fanned out', slides: 'three staggered diagonal portrait rectangles, like a deck of slides fanned out', website: 'a browser window outline with three dots in the top-left and a few horizontal content lines', 'web page': 'a browser window outline with three dots in the top-left and a few horizontal content lines', app: 'the hairline outline of a vertical phone with a small notch', application: 'the hairline outline of a vertical phone with a small notch', code: 'a window outline holding a few lines of code suggested as hairlines with varied indentation', programming: 'a window outline holding a few lines of code suggested as hairlines with varied indentation', chat: 'two stacked speech bubbles, one showing typing dots', conversation: 'two stacked speech bubbles, one showing typing dots', money: 'a currency mark inside a hairline circle', revenue: 'a currency mark inside a hairline circle', time: 'a clock outline with slim hands', fast: 'a clock outline with slim hands', data: 'a hairline bar chart with a few rising bars', analytics: 'a hairline bar chart with a few rising bars', vault: 'a hexagonal outline with a central dot and radial lines', obsidian: 'a hexagonal outline with a central dot and radial lines', }; const ABSTRACT_METAPHORS: Record = { ai: 'soft white particles drifting upward, slowly coalescing into a brain-like silhouette outlined in hairlines, with a subtle inner glow', intelligence: 'soft white particles drifting upward, slowly coalescing into a brain-like silhouette outlined in hairlines, with a subtle inner glow', thinking: 'soft white particles drifting upward, slowly coalescing into a brain-like silhouette outlined in hairlines, with a subtle inner glow', clone: 'thin hairline lines drawing themselves one by one between scattered white dots, forming a network constellation that settles into a stable graph', clones: 'thin hairline lines drawing themselves one by one between scattered white dots, forming a network constellation that settles into a stable graph', sources: 'thin hairline lines drawing themselves one by one between scattered white dots, forming a network constellation that settles into a stable graph', knowledge: 'thin hairline lines drawing themselves one by one between scattered white dots, forming a network constellation that settles into a stable graph', insight: 'a soft warm-white bloom of light expanding outward from a central point, then settling into a clean centred headline as the light fades', reveal: 'a soft warm-white bloom of light expanding outward from a central point, then settling into a clean centred headline as the light fades', answer: 'a soft warm-white bloom of light expanding outward from a central point, then settling into a clean centred headline as the light fades', agent: 'interconnected nodes with one node pulsing softly, suggesting a continuous process — gentle, not mechanical', automation: 'interconnected nodes with one node pulsing softly, suggesting a continuous process — gentle, not mechanical', processing: 'particles flowing into a structured grid that locks into place line by line, then settles', analysis: 'particles flowing into a structured grid that locks into place line by line, then settles', }; /** * Resolve a single concept word to animation prose. Concrete → hairline icon; * abstract → material metaphor; unknown abstract-ish words fall back to a * generic light metaphor and unknown concrete-ish words to a generic hairline * icon. Lookup is case-insensitive on the trimmed concept. */ export function conceptAnimation(concept: string): string { const key = concept.trim().toLowerCase(); if (key in ABSTRACT_METAPHORS) { return ABSTRACT_METAPHORS[key]; } if (key in CONCRETE_ICONS) { return CONCRETE_ICONS[key]; } // Unknown: keep the concrete default (a hairline icon). Abstract concepts the // map doesn't know about should be added rather than guessed at — a hairline // icon is the safer generic than an over-specific metaphor. return `a minimalist hairline icon representing "${concept.trim()}"`; }