@clipkit/cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +182 -0
- package/dist/commands/docs.d.ts +3 -0
- package/dist/commands/docs.d.ts.map +1 -0
- package/dist/commands/docs.js +31 -0
- package/dist/commands/docs.js.map +1 -0
- package/dist/commands/explain.d.ts +3 -0
- package/dist/commands/explain.d.ts.map +1 -0
- package/dist/commands/explain.js +25 -0
- package/dist/commands/explain.js.map +1 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +176 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +88 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/mcp.d.ts +3 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +26 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/new.d.ts +3 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +77 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/preview.d.ts +3 -0
- package/dist/commands/preview.d.ts.map +1 -0
- package/dist/commands/preview.js +69 -0
- package/dist/commands/preview.js.map +1 -0
- package/dist/commands/render.d.ts +3 -0
- package/dist/commands/render.d.ts.map +1 -0
- package/dist/commands/render.js +213 -0
- package/dist/commands/render.js.map +1 -0
- package/dist/commands/schema.d.ts +3 -0
- package/dist/commands/schema.d.ts.map +1 -0
- package/dist/commands/schema.js +20 -0
- package/dist/commands/schema.js.map +1 -0
- package/dist/commands/still.d.ts +3 -0
- package/dist/commands/still.d.ts.map +1 -0
- package/dist/commands/still.js +58 -0
- package/dist/commands/still.js.map +1 -0
- package/dist/commands/transcribe.d.ts +3 -0
- package/dist/commands/transcribe.d.ts.map +1 -0
- package/dist/commands/transcribe.js +52 -0
- package/dist/commands/transcribe.js.map +1 -0
- package/dist/commands/validate.d.ts +3 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +46 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +58 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/lint.d.ts +8 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +75 -0
- package/dist/lint.js.map +1 -0
- package/dist/load-source.d.ts +5 -0
- package/dist/load-source.d.ts.map +1 -0
- package/dist/load-source.js +63 -0
- package/dist/load-source.js.map +1 -0
- package/dist/templates/agents-content.d.ts +3 -0
- package/dist/templates/agents-content.d.ts.map +1 -0
- package/dist/templates/agents-content.js +3282 -0
- package/dist/templates/agents-content.js.map +1 -0
- package/dist/util.d.ts +17 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +40 -0
- package/dist/util.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare const AGENTS_MD_CONTENT = "# Clipkit \u2014 Authoring guide (AGENTS.md)\n\n> This file is the authoring-time context for AI tools (and humans)\n> writing Clipkit videos. Paste it into a system prompt or attach it\n> to a Claude project. For the **formal protocol spec** \u2014 element types,\n> field semantics, conformance levels, normative MUST/SHOULD/MAY \u2014 see\n> [`PROTOCOL.md`](./PROTOCOL.md).\n\nClipkit is a protocol for describing motion-graphics videos as JSON.\nA `Source` document is the list of `elements` (with timing and\nanimations) that a conforming runtime turns into frames or MP4.\n\nThere are two layers you work with as an author:\n\n1. **Primitives** \u2014 the element types defined by the Clipkit Protocol.\n The formal spec is in [PROTOCOL.md](./PROTOCOL.md). Section 1 below\n is a quick cheat sheet so you don't have to bounce out for common\n lookups, but PROTOCOL.md is the source of truth.\n2. **Patterns** \u2014 authoring-time TypeScript helpers in `@clipkit/patterns`\n that produce primitive elements. You never reference patterns in\n a Source JSON; you call them in TS/JS and inline their output.\n Patterns are documented in section 2 (Pattern catalog).\n\nSection 3 (Recipes) walks through complete example videos showing how\npatterns compose into a full piece.\n\n---\n\n## 0. Which surface to use\n\nBefore authoring anything, pick the right surface for your context.\nThere are two \u2014 they layer cleanly, no overlap.\n\n| You are running in\u2026 | Use\u2026 | How |\n|---|---|---|\n| **Claude Code, Cursor, Replit Agent, or any IDE-embedded AI with file + shell access** | `@clipkit/cli` | Write files (`video.ts` or a JSON source) using this doc as context. Run `npx @clipkit/cli render`, `validate`, or `preview` via Bash. Skip MCP entirely. |\n| **Claude.ai chat, Claude Desktop without local tools, or any chat-mode AI without filesystem access** | `@clipkit/mcp-server` | Call the MCP tools \u2014 `set_project` to build (the full creative canvas), `create_promo` to compose from prebuilt scenes, `add_element`/`edit_element`/`delete_element` to tweak, `preview_still`/`validate_project`/`describe_project` to check, `open_in_editor`/`render_video` to deliver. The server holds state for you. |\n| **A human developer** | `@clipkit/cli` | `npx @clipkit/cli init my-video`, edit files, `npx @clipkit/cli render`. Same path as IDE-embedded AI. |\n| **CI / a build script** | `@clipkit/cli` | One-shot `clipkit render input.json --out out.mp4`. |\n\nA few clarifications agents sometimes need:\n\n- **The CLI and the MCP server are not interchangeable.** They target\n different deployments. CLI assumes you have a shell. MCP assumes you\n don't.\n- **If you can run `Bash`, you're in CLI territory.** You don't need\n MCP at all \u2014 go straight to files + CLI.\n- **There is no \"Clipkit Skill.\"** The skill concept was considered and\n dropped because everything it would do (auto-load this doc, wrap CLI\n commands as slash commands) is already covered by the CLI and by MCP\n resource-loading. If a user asks about installing the skill, redirect\n them to the CLI.\n\nOnce you know your surface, continue to \u00A71 for the schema cheat sheet.\n\n---\n\n## 1. Schema cheat sheet\n\nAuthoritative spec: **[PROTOCOL.md](./PROTOCOL.md)** (CKP/1.0). What\nfollows is the at-a-glance reference for things you look up constantly.\n\n### Source root\n\n```json\n{\n \"clipkit_version\": \"1.0\",\n \"output_format\": \"mp4\",\n \"width\": 1920, \"height\": 1080,\n \"duration\": 30, \"frame_rate\": 30,\n \"background_color\": \"#000000\",\n \"elements\": [ /* ... */ ]\n}\n```\n\nOptional root field `motion_blur: { \"samples\": 8, \"shutter\": 0.5 }`\nadds whole-frame motion blur at render time \u2014 the frame becomes the\nexact average of `samples` sub-frame renders across a shutter window\n(`shutter` = fraction of the frame interval; 0.5 \u2248 a film camera's\n180\u00B0 shutter). Use it when fast motion strobes: whip-pans, fast\ntravel, spinning elements. Renders cost `samples`\u00D7 the frames, so keep\nsamples at 8 (default) unless trails visibly stair-step. Previews play\nunblurred; the blur appears in the exported file.\n\n### Element types\n\n| `type` | Renders | Key fields |\n|---|---|---|\n| `shape` | rect / ellipse / rounded rect, **or** vector paths | primitive: `shape`, `fill_color`, `gradient`, `border_radius`; path form: `paths[]`, `view_box`, `gradients` |\n| `text` | one-line text | `text`, `spans`, `font_family`, `font_size`, `background_color`, `text_shadow`, `mask` |\n| `image` | bitmap | `source`, `fit`, `crop_*` |\n| `video` | video frame at element time | `source`, `volume`, `trim_start`, `loop`, `fit`, `crop_*` |\n| `audio` | non-visual; mixed in export | `source`, `volume` |\n| `caption` | word-timed captions | `words`, `style`, `highlight_color` |\n| `particles` | ballistic or convergence | `rate`/`burst`/`target_points`, `lifetime`, `velocity`, `z_velocity` |\n| `group` | container; transforms/animates children as one | `elements[]`, `clip`, `mask`, `time_remap` |\n\nA `shape` is **either** a primitive (`shape: \"rectangle\" | \"ellipse\"`, default\nrectangle) **or** vector geometry (`paths`) \u2014 never both on one element. When\n`paths` is present it wins and the primitive fields (`shape`, `fill_color`,\n`gradient`, `border_radius`, `stroke_*`) are ignored; geometry and fill come\nfrom inside each path. There is no `triangle`/`polygon` primitive \u2014 use `paths`\n(e.g. `\"M ... L ... L ... Z\"`) for triangles and any arbitrary/morphing shape.\n\n`text` and `caption` take `background_color` (+ `background_border_radius`,\n`background_padding` as a number or `[x, y]`) for a solid bg drawn as **one\nband per line**, each shrink-wrapped to that line's glyphs (the social-\ncaption look) \u2014 don't put a separate shape behind text; the integral bg\nauto-sizes to the wrapped / auto-fit text and moves with it.\n\nFor text shadows: `text_shadow` ({`color`, `offset_x`, `offset_y`, `blur`,\n`opacity`}, or an **array** for stacked / 3D-extrusion shadows) is\n**per-glyph** \u2014 each letter casts its own, tracking per-letter animation.\nUse the `drop_shadow` *effect* instead for one soft shadow of the whole\ntext silhouette.\n\n`image` and `video` take `fit` (CSS `object-fit`: `cover` default,\n`contain`, `fill`, `none`) and a source **crop** \u2014 `crop_x` / `crop_y` /\n`crop_width` / `crop_height`, normalized `0..1` of the source, origin\ntop-left, default `0,0,1,1` (whole source). Crop selects a sub-rectangle\nof the media BEFORE `fit` maps it into the box; the element box is\nunchanged. Each crop component is keyframeable \u2014 animate the origin to\npan and the size to zoom (Ken Burns) without touching layout.\n\nThere is deliberately NO nested-composition (\"pre-comp\") element. Reuse\nis an authoring-time concern \u2014 component patterns in `@clipkit/patterns`\n(section 2) expand into plain elements before serialization. Nested\ntiming (speed-ramp / freeze / reverse a whole scene) is `time_remap` on\na plain `group`.\n\n### Shared transform fields (every element)\n\n`x`, `y`, `x_anchor` (0..1), `y_anchor` (0..1), `width`, `height`,\n`rotation` (degrees), `scale`, `opacity` (0..1 \u2014 CSS convention),\nand the CKP/1.0 3D fields `x_rotation` / `y_rotation` / `z_rotation` /\n`z` (see \"3D transforms\" below).\n\nAll of `x` / `y` / `width` / `height` accept numbers (pixels) or\nlength strings \u2014 `\"50%\"`, `\"10vw\"`, `\"15vh\"`, `\"5vmin\"`, `\"5vmax\"`.\n\n> \u26A0\uFE0F **`%` is relative to the CANVAS, not the parent** (unlike CSS).\n> `\"50%\"` on `x` means half the canvas width regardless of nesting.\n> `vw`/`vh`/`vmin`/`vmax` likewise resolve against the canvas. There is\n> no parent-relative percentage; use pixels inside groups when you need\n> child-relative sizing.\n\n### Coming from CSS/HTML\n\nClipkit deliberately tracks CSS where an agent's CSS prior would\notherwise misfire, and deliberately diverges where the protocol earns\nit. Aligned (write them like CSS): **`opacity`** is `0..1` (default 1);\n**gradient `angle`** is the CSS convention (`0deg` = to top, clockwise,\nso `90` = to right, `180`/default = to bottom); **colors** accept hex,\n`rgb()/rgba()`, `hsl()/hsla()`, the 148 CSS named colors, and\n`transparent`. Field name map:\n\n| CSS | Clipkit | Note |\n|---|---|---|\n| `left` / `top` | `x` / `y` | top-left corner, like CSS |\n| `width` / `height` | `width` / `height` | px or canvas-% / vw / vh |\n| `opacity` | `opacity` | `0..1`, default 1 |\n| `border-radius` | `border_radius` | px |\n| `color` / `background` | `fill_color` | SVG-flavored name |\n| `border` color | `stroke_color` / `stroke_width` | |\n| `z-index` | `z` (+ `layer`) | see below |\n| `transform: rotate()` | `rotation` (= `z_rotation`) | degrees |\n| `transform: scale()` | `scale` / `x_scale` / `y_scale` | |\n| `linear-gradient(\u03B8)` | `gradient.angle` | same \u03B8 convention |\n| `transition` / `@keyframes` | `animations` / `keyframe_animations` | declarative timeline |\n\n**Positioning is the CSS model:** `x`/`y` place the element's **top-left\ncorner** (`x_anchor`/`y_anchor` default `0`), exactly like `left`/`top` /\nSVG / Canvas. Set `x_anchor: 0.5, y_anchor: 0.5` to position by center\n(or `1` for the far edge). Rotation and scale always pivot the element's\ncenter \u2014 like CSS `transform-origin` \u2014 independent of the anchor. So a\nfull-frame layer is just `x:0, y:0, width:W, height:H`.\n\n**Layers (required).** Every element gets its own integer `layer` (1..1000),\nlike an After Effects layer: **layer 1 is on top**, higher numbers go toward\nthe back. Give each element a UNIQUE layer \u2014 number them 1..N within a container\n(the top-level `elements`, each group's `elements`, each group mask's\n`elements`). Duplicate layers are a validation error. `z` depth still wins over\n`layer`; `layer` orders elements within equal depth.\n\nKept divergences (intentional \u2014 don't \"fix\" them):\n- **`z` not `z_index`.** `z` is the single depth axis (layering +\n perspective foreshortening); `layer` orders within equal depth (lower = front). See \"3D\n transforms\".\n- **snake_case keys** and SVG-flavored `fill_color` / `stroke_color`.\n- **Easing names are a superset of CSS** (`ease-out-back`, springs, \u2026).\n\n### Expressions (procedural motion)\n\nA numeric property can be a **formula** instead of a number or a keyframe\ntable \u2014 a pure function of the element's own clock. Reach for it on the\nmotions that are one line as math and a nightmare as keyframes: bobs,\norbits, spins, handheld shake, staggered reveals.\n\n```jsonc\n{ \"y\": { \"expr\": \"540 + sin(t * PI) * 30\" } } // bob \u00B130px at 0.5 Hz\n{ \"rotation\": { \"expr\": \"t * 90\" } } // 90\u00B0/s spin\n{ \"x\": { \"expr\": \"960 + wiggle(3, 12)\" } } // handheld shake, \u00B112px\n{ \"opacity\": { \"expr\": \"clamp(t / 0.4, 0, 1)\" } } // 0.4s linear fade-in\n```\n\nIn scope: `t` (element-local seconds), `dur`, `i` (index in a generated\nset), `n` (sibling count), `value` (the property's base default); the\nconstants `PI`/`TAU`/`E`; and a fixed function set (`sin cos \u2026 clamp lerp\nsmoothstep linear ease noise wiggle random`). Operators `+ - * / % ^`,\ncomparisons, `&& || !`, and `cond ? a : b`. The full normative grammar is\nPROTOCOL.md \u00A73.6.\n\nUse `i` for staggered fleets \u2014 give every generated element the **same**\nexpression and they self-offset by index:\n\n```jsonc\n{ \"y\": { \"expr\": \"300 + i * 80\" }, // stack by index\n \"opacity\": { \"expr\": \"clamp((t - i * 0.1) / 0.3, 0, 1)\" } } // ripple-in\n```\n\nThe rules that keep it safe and portable (all NORMATIVE):\n- **No cross-element references, no runtime inputs.** An `expr` sees only\n its own clock \u2014 never another element, the mouse, or audio. (Those are\n \"Tier-B\" and permanently unsupported. If you're tempted, bake the\n relationship into keyframes, or parent the elements.)\n- **Deterministic.** `noise`/`wiggle`/`random` are seeded hashes, not\n wall-clock RNG, so every render is identical; `seed` defaults to `0`.\n- **Bakeable.** An expression and a `Keyframe[]` are interchangeable \u2014 the\n importer/editor can sample one into the other.\n- A typo (unknown name, member access, a string) does **not** stop the\n render \u2014 the property silently falls back to its base value. So if a\n move \"does nothing,\" check the formula.\n\n### 3D transforms (CKP/1.0)\n\nEvery element also takes `x_rotation` (tip top edge away), `y_rotation`\n(turn right edge away), `z_rotation` (alias of `rotation` \u2014 author one\nor the other, both is a validation error), and `z` \u2014 **the one depth\naxis** (px toward the viewer). `z` is also the stacking control: it\norders elements always (higher `z` = in front), and *additionally*\nforeshortens once a camera is present. There is no separate `z_index` \u2014\nuse `z` for layering, `layer` orders within equal depth (lower = front, unique per container). Add a\nSource-level camera for true perspective:\n\n```jsonc\n{ \"camera\": { \"perspective\": 1200 }, // px; smaller = stronger\n \"elements\": [{ \"type\": \"group\", \"clip\": true, \"y_rotation\": -24,\n \"width\": 620, \"height\": 420, \"elements\": [ /* UI mock */ ] }] }\n```\n\nThe tilted-UI promo recipe: put the screenshot/mock in a `clip: true`\ngroup and rotate THE GROUP \u2014 the flattened layer projects as one card.\n\nThe camera also MOVES (CKP/1.0): `x`/`y`/`z` position and `x_rotation`/\n`y_rotation`/`z_rotation` Euler orientation (all keyframeable) orbit,\npan, tilt, and dolly the viewpoint \u2014 not just push/pull on `perspective`.\nA \"look at the logo\" move is authored as resolved orientation angles\n(there is no `target` field). Identity pose = the plain perspective lens.\n\nGOTCHAS:\n- **Paint order is by `z` depth \u2014 always.** Sibling cards paint\n back-to-front by `z` (nearer = in front), whether or not there's a\n camera; a camera just adds perspective on top. So adding/removing a\n camera never reorders \u2014 only the foreshortening appears. With every\n `z = 0` it collapses to `layer` order (layer 1 on top, like a plain 2D doc).\n Set `camera.sort: \"paint\"` to force fixed `layer` order under a camera.\n The sort is whole-card (no per-pixel depth buffer): interpenetrating or\n depth-ambiguous cards can mis-order \u2014 keep cards from crossing, or use\n `sort: \"paint\"`.\n- **Animate 3D via `keyframe_animations`** (`property: \"y_rotation\"`),\n not a keyframe array in the field itself (same rule as `rotation`).\n- **No camera = affine + flat.** 3D rotations still foreshorten (a\n y-rotated card narrows) but edges stay parallel, and `z` orders the\n stack without adding perspective; add the camera for converging\n perspective and parallax.\n- **Glass works in 3D** \u2014 tilt a glass pane (`y_rotation` + camera)\n and the whole optical model rides the plane: footprint, refraction,\n highlights and shadow all project correctly. Signature move: a glass\n panel swinging over a UI card. (Edge-on with no camera = invisible.)\n- **Plain groups nest 3D for free** (children live in the parent's 3D\n space); `clip`/`mask` groups flatten, then the layer tilts as a unit.\n Filter fields and `effects` evaluate on the PROJECTED pixels \u2014 glow\n radius / stroke width stay uniform in screen px on a tilted card.\n\n3D text reveals \u2014 `{ \"type\": \"text-flip\", \"axis\": \"x\" }` flips each\nletter in from 90\u00B0 to flat about its own center (AE's classic 3D\nper-character entrance). `axis: \"y\"` swings units in sideways, `\"z\"`\nspins them in-plane; `split: \"word\"` flips whole words as rigid slabs;\n`rotation` sets the start angle (try 180 for a full tumble). Composes\nwith `text-slide` (units slide AND flip). Pair with a `camera` for true\nperspective on the mid-flip letters; without one the flip still reads\n(pure foreshortening). Captions don't take text-* animations.\n\n### Lighting + materials (CKP/1.0)\n\nSurfaces can respond to light \u2014 PBR. **Opt-in:** with no `lights` and no\n`material`, everything renders unlit exactly as before. Add scene\n`lights` + a `material` on an element and the runtime shades it (the\nelement's own pixels are the albedo).\n\n```jsonc\n{ \"lights\": [ { \"type\": \"ambient\", \"intensity\": 0.4 },\n { \"type\": \"directional\", \"azimuth\": 45, \"elevation\": 30, \"intensity\": 1.2 } ],\n \"environment\": { \"type\": \"gradient\",\n \"stops\": [ { \"offset\": 0, \"color\": \"#0b0f1a\" },\n { \"offset\": 1, \"color\": \"#7da2ff\" } ] },\n \"camera\": { \"perspective\": 1400 },\n \"elements\": [ { \"type\": \"group\", \"clip\": true, \"y_rotation\": -20,\n \"material\": { \"roughness\": 0.25, \"metalness\": 0.6, \"reflectivity\": 1 },\n \"elements\": [ /* dark UI */ ] } ] }\n```\n\n- **`material`**: `roughness` (0 glossy/tight .. 1 matte), `metalness`\n (0 dielectric .. 1 metal), `reflectivity` (env-reflection strength),\n `emissive` (self-lit), `normal_map` (tangent-space normal map URL for\n surface detail/bumps \u2014 flat = `#8080ff`) + `normal_scale`. Scalars\n animatable.\n- **The reflection sweeps with the camera.** Specular + environment\n reflection are view-dependent, so orbiting/dollying the camera slides\n the highlight across a glossy dark UI \u2014 the premium look. In 2D (no\n camera) animate the **lights** instead for the sweep.\n- **`environment`** is what reflective surfaces mirror: either a gradient\n \"sky\" (`{ type: 'gradient', stops }`, offset 0 = down, 1 = up) or an\n **equirectangular image** (`{ type: 'image', src }`) for real\n photographic reflections. Roughness blurs the reflection toward the\n environment average. It's what makes glossy screens/metal look\n expensive.\n- **Works on shapes AND textured surfaces** \u2014 images, video, and\n flattened `clip` groups light from their own pixels, so a whole UI card\n wrapped in a clip group shades as one lit plane. `@clipkit/patterns`\n ships `litSurface()` for exactly this.\n- **Bloom** (`source.bloom { threshold, knee, intensity, radius }`): a\n whole-frame post that makes BRIGHT things bleed light (specular hits,\n emissive surfaces, bright media) \u2014 driven by each pixel's brightness,\n not a per-element knob. For a deliberate per-element halo (even on a\n dark element), use the `glow` effect instead.\n- **Editor authoring:** Material is a per-element inspector block; Lights,\n Environment, and Bloom live under Video settings (source scope).\n- Local illumination only (no cast shadows yet). Flat 2.5D surfaces.\n\n### Blend modes + filters (every element)\n\n`blend_mode`: `\"multiply\"` (darken; white neutral), `\"screen\"`\n(lighten; black neutral), `\"add\"` (glow). Element-local. On a group it\nneeds `clip: true` or a `mask` (the group must flatten to a layer).\n\nFilters, all animatable, CSS semantics: `blur_radius` (Gaussian \u03C3 px),\n`brightness` / `contrast` / `saturation` (multipliers, 1 = unchanged,\n`saturation: 0` = grayscale). Work on any element including groups \u2014\na group filters as one flattened image.\n\nThere is no `color_overlay` field \u2014 it decomposes: put a same-sized\nshape of the overlay color on a lower layer, in front of the element (use\n`opacity` or `blend_mode: \"multiply\"` for tint strength). For a\ngrayscale-then-tint look, combine `saturation: 0` with an overlay.\n\n### Stylize effects (every element)\n\n`effects` is an ordered array of passes, applied after the filters:\n\n```json\n\"effects\": [{ \"type\": \"pixelate\", \"cell_size\": 12 }]\n```\n\nTypes: `pixelate` (`cell_size`), `dither` (`levels`, 2 = 1-bit retro),\n`halftone` (`cell_size`, `angle`), `ascii` (`cell_size`). Every param\ntakes a number or keyframes (element-local time) \u2014 animating\n`cell_size` from 1 upward makes a great \"depixelate\" reveal. Effects\nchain in array order and work on groups (the subtree flattens first).\nLayer styles \u2014 `glow` (`radius`, `intensity`, `color`), `drop_shadow`\n(`offset_x/y`, `blur`, `color`, `opacity`), `stroke` (`width`,\n`color`) \u2014 composite beneath the element and work on text, images,\nand groups (not just shapes).\n\nGroup time remapping \u2014 `time_remap` on a GROUP warps the clock for\neverything inside it: slow a whole composed scene to 20% at the\ndramatic beat, freeze it, or run it backwards \u2014 children's animations\nand nested videos all follow \u2014 and so does AUDIO, varispeed-style\n(ramps pitch with speed, freezes silent, reverse plays backwards).\nThe group's own position/opacity still animate on real time, so you\ncan move a frozen scene around. Same keyframe shape as video\ntime_remap below.\n\nTime remapping \u2014 `time_remap` on a video element maps element-local\ntime \u2192 media seconds with plain keyframes: `[{time: 0, value: 0},\n{time: 1, value: 3}]` is a 3\u00D7 speed ramp, a flat segment is a freeze\nframe, decreasing values play in reverse, easings make the ramps\ncinematic. It replaces `trim_start`/`playback_rate`/`loop`. Audio\nfollows the warp VARISPEED, like tape: ramps pitch up/down with\nspeed, freezes go silent, reverse plays the sound backwards \u2014 usually\nexactly the cinematic speed-ramp sound you want.\n\nGOTCHA: the runtime keys ONE decoder + texture per unique video URL.\nTwo video elements with the same URL but different playheads\n(different `time_remap`, trims, or rates) will fight over it \u2014 all\ncopies display the same frames and seeking thrashes. Give each its\nown URL; a query suffix on the same file (`clip.mp4?copy=2`) is\nenough.\n\nTrim paths \u2014 on any shape path: `trim_start`/`trim_end` draw only that\nslice of the stroke (animate `trim_end` 0\\u21921 for the logo-draws-itself\nreveal), `trim_offset` rotates the window with wrap (animate it for\nthe traveling-dash \"snake\"). `stroke_progress` is the simple sugar.\nPath morphing: keyframe the `d` itself \u2014 values with the SAME command\nstructure (e.g. two cubics with equal point counts) tween smoothly;\nmismatched structures snap. Author morph targets with matching\ncommands, like every motion tool expects.\n\nLiving motion, one line each \u2014 `drift` (seeded organic float; the\n\"alive logo\" look: `{type: \"drift\", distance: 20}`), `breathe` (gentle\nscale pulse), `orbit` (circular motion: `distance` radius,\n`direction: \"left\"` for counter-clockwise). All run the element's full\nlife by default and are exactly seeded \u2014 same seed, same motion. And\nany keyframe_animation can now `loop: true` (repeat) or\n`loop: \"ping-pong\"` (back-and-forth) \u2014 build one cycle, loop forever.\n\nMotion paths \u2014 `keyframe_animations` with `property: \"position\"` and\n`[x, y]` values moves an element along a bezier path: `out_tangent` /\n`in_tangent` on the keyframes shape the curve (omit them for straight\npolyline hops), `auto_orient: true` turns the element to face its\ntravel direction. Speed is arc-length-true: linear easing = constant\nspeed; easings shape acceleration along the path. Use for swooping\nentrances, orbit flybys, anything that should move like it has\nmomentum instead of lerping x and y separately.\n\n3D paths \u2014 use `[x, y, z]` values (and optionally `[dx, dy, dz]`\ntangents) to carve the path through depth; the path then drives `z`\ntoo, and arc-length speed is true in 3D. Don't mix `[x, y]` and\n`[x, y, z]` keyframes in one path (validation error). Two gotchas:\npath z is invisible without a `camera` (same as the `z` field), and\n`auto_orient` stays in-plane \u2014 it spins `rotation` from the xy\ndirection but never tilts the element's plane (add `x_rotation` /\n`y_rotation` yourself if you want banking). Signature move: fly an\nelement from deep background to right up against the camera along a\ncurve.\n\nProcedural noise \u2014 `fractal_noise` (`scale`, `evolution`, `offset_x/y`,\n`octaves`, `seed`) fills the element's footprint with seeded grayscale\nfBM: animate `evolution` for churning fog/plasma, chain `levels` to\nshape contrast and a duotone `lut` for color (grayscale has no hue, so\n`hue_rotate` alone can't tint it), or put it on TEXT for noise-filled\ntype. `turbulent_displace` (`amount`, `scale`, `evolution`, `octaves`,\n`seed`) warps the element's own pixels \u2014 wavy text, heat shimmer.\nSame seed = same pixels, always (the noise function is normative).\n\nGrading \u2014 the `hue_rotate` filter field (degrees, joins\nblur/brightness/contrast/saturation on every element) plus two\neffects: `levels` (`in_black`, `in_white`, `gamma`, `out_black`,\n`out_white` \u2014 punchy shadows = `in_black: 0.08, gamma: 1.15`) and\n`lut` (`source` = a .cube file URL or data: URI, `intensity` to dial\nit back). A LUT is the one-liner for a whole color story\n(teal-orange, film stocks); levels is the surgical tool.\n\nKeying \u2014 `chroma_key` (`color`, `tolerance`, `softness`, `spill`)\nremoves a screen color by chroma distance: put it on a green-screen\n`video` element with `color` set to the screen's ACTUAL green (sample\nit \u2014 real screens are darker than `#00FF00`), then place the video\nover any backdrop. `spill` (default 0.5) desaturates green bounce on\nthe subject. `luma_key` (`threshold`, `softness`, `invert`) drops dark\npixels \u2014 the way to lift white-on-black mattes, flares, and smoke\nstock onto a scene. On a group, the composited children key as one\nlayer.\n\n`glass` is the one effect that reads the BACKDROP (everything drawn\nbeneath the element). It applies to SHAPE elements only (the pane\ngeometry must be exact; other element types skip it with a warning).\nDefaults give the classic clear liquid-glass button \u2014 most uses need\nNO params at all:\n\n```json\n{ \"type\": \"shape\", \"border_radius\": 65, \"width\": 380, \"height\": 300,\n \"fill_color\": \"#FFFFFF\",\n \"effects\": [{ \"type\": \"glass\" }] }\n```\n\nTwo materials, one dial: `blur_radius: 0` (default) = CLEAR glass,\n`blur_radius > 0` = FROSTED. `mode: \"dome\"` + `edge_width` = the\nshape's radius = a half-sphere magnifier:\n\n```json\n{ \"id\": \"magnifier\", \"type\": \"shape\", \"shape\": \"ellipse\",\n \"width\": 320, \"height\": 320, \"fill_color\": \"#FFFFFF\",\n \"effects\": [{ \"type\": \"glass\", \"mode\": \"dome\",\n \"edge_width\": 160, \"refraction\": 25 }] }\n```\n\nOther dials (reference-tuned defaults \u2014 change sparingly):\n`refraction` (bend strength, \u2248px), `edge_width` (lens depth; small =\nflat card with curl at the rim), `edge_highlight` (the whole light\nrig), `dispersion` (color fringing), `shadow` (built-in, outside-only\n\u2014 never add a shadow sibling under glass), `backdrop_saturation`,\n`tint`. The pane's fill color is ignored \u2014 only its geometry matters.\nA crisp ring is a stroked transparent sibling above; wrap pane + ring\nin an UNLAYERED group (no clip/mask) to move them as one element.\n\n### Easings (the 36)\n\n`linear`, `ease`, `ease-in`, `ease-out`, `ease-in-out`, plus\nease-{in,out,in-out}-{cubic, quad, quart, quint, sine, expo, circ,\nback}, plus `spring` (damped harmonic oscillator \u2014 overshoots ~5% then\nsettles; Remotion's signature feel), `elastic-{in,out,in-out}` (decaying\nsinusoidal wobble), and `bounce-{in,out,in-out}` (ball-drop bounces \u2014 the\neasing curves, distinct from the bounce-in/out animation PRESETS, which\nare scale tweens). Two parametric forms also count: `cubic-bezier(x1, y1,\nx2, y2)` and `steps(n)`.\n\nQuick guide:\n- **`spring`** \u2014 hero text reveals, signature scale-ins\n- **`ease-out-cubic`** \u2014 smooth, well-behaved ramps; default choice\n- **`ease-out-quart`** \u2014 fast start, long slow tail; great for measure bars\n- **`ease-out-back`** \u2014 slight overshoot at the end (UI bounce)\n- **`linear`** \u2014 when you specifically want no easing\n\n### Particles \u2014 two modes\n\nBallistic (emit + physics):\n\n```json\n{\n \"type\": \"particles\",\n \"x\": 540, \"y\": 960,\n \"burst\": true, \"burst_count\": 140,\n \"lifetime\": 2.5, \"velocity\": 900, \"spread\": 360, \"gravity\": 700,\n \"color\": [\"#22d3ee\", \"#ec4899\", \"#22c55e\", \"#fbbf24\"],\n \"particle_shape\": \"square\", \"size\": 18, \"rotation_speed\": 540,\n \"fade_at\": 0.8\n}\n```\n\nConvergence (assemble into a target shape):\n\n```json\n{\n \"type\": \"particles\",\n \"x\": 960, \"y\": 540,\n \"burst\": true, \"burst_count\": 500,\n \"target_points\": [[100, 200], [110, 200], /* ... */ ],\n \"scatter_radius\": 1100,\n \"convergence_easing\": \"ease-out-quart\",\n \"lifetime\": 1.6, \"size\": 10, \"particle_shape\": \"circle\"\n}\n```\n\nDepth \u2014 add `z_velocity` (px/s toward the viewer, can be negative) and\n`z_spread` (random vz range) to blow particles out of the screen:\nnearer ones grow, receding ones shrink. Needs a Source `camera` to be\nvisible (no perspective = no depth cue), and pairs beautifully with a\ntilted emitter (`x_rotation` on the particles element): confetti\nerupting off a card's surface. Paint order stays spawn order \u2014 depth\nnever re-sorts.\n\n### Shape paths \u2014 vector graphics\n\n```json\n{\n \"type\": \"shape\",\n \"view_box\": [0, 0, 180, 180],\n \"gradients\": [\n { \"id\": \"g1\", \"type\": \"linear\", \"x1\": 0, \"y1\": 0, \"x2\": 180, \"y2\": 0,\n \"stops\": [{ \"offset\": 0, \"color\": \"#FF4E00\" }, { \"offset\": 1, \"color\": \"#FF1791\" }] }\n ],\n \"paths\": [\n { \"d\": \"M 10 10 L 90 90\", \"stroke\": \"url(#g1)\", \"stroke_width\": 5,\n \"stroke_progress\": [{ \"time\": 0, \"value\": 0 }, { \"time\": 1, \"value\": 1 }] }\n ]\n}\n```\n\n`stroke_progress` from 0\u21921 makes the path draw in via dashoffset. Use a\nthick stroke on a small circle for pie/donut charts.\n\n### Diagonal text reveal\n\n```json\n{\n \"type\": \"text\", \"text\": \"Vercel and Clipkit\", \"font_size\": 70,\n \"mask\": {\n \"type\": \"linear-wipe\", \"angle\": -45,\n \"progress\": [{ \"time\": 0, \"value\": 0 }, { \"time\": 2.7, \"value\": 1, \"easing\": \"ease-out-cubic\" }],\n \"softness\": 0.3\n }\n}\n```\n\nThat's the at-a-glance set. For full normative semantics \u2014 units, edge\ncases, conformance \u2014 go to [PROTOCOL.md](./PROTOCOL.md).\n\n## 2. Pattern catalog\n\nPatterns live in `@clipkit/patterns`. Each is a TS function that emits\nplain elements \u2014 most return `Element[]` you spread into your `elements`\narray; *component* patterns (IntroCard, LowerThird) return a single\n`group` element you push, so the unit moves / animates / time-remaps as\none. They're opinionated: pick a variant by passing a `color` slot\n(\"pink\" | \"green\" | \"blue\" | \"lavender\" | \"purple\" | \"yellow\" | \"gray\")\nand a `theme` (`'mux'` is the default; `'minimal'` exists too).\n\nCommon params shared by most patterns:\n\n- `id`: string \u2014 prefix for produced element ids\n- `theme?: 'mux' | 'minimal'` \u2014 default `'mux'`\n- `color`: ColorName \u2014 picks the accent palette\n- `time`, `duration`: number \u2014 scene-local seconds\n- `layerBase`: number \u2014 pattern uses a small range starting here\n\n### HeaderBar\n\nThe scene framing for dashboard-style scenes: white header strip with\nleft logo + middle title + right date, colored body fill below.\n\n```ts\nheaderBar({\n id: 'overall',\n title: 'Overall stats',\n dateRange: 'Nov. 17 - Dec. 16 2021',\n bodyColor: 'pink',\n canvasWidth: 1920,\n canvasHeight: 1080,\n logo: { viewBox: [0, 0, 215, 70], paths: MUX_LOGO_PATHS, gradients: [MUX_GRADIENT] },\n time: 4.33,\n duration: 6.0,\n layerBase: 100,\n})\n```\n\nUse as the first elements of every scene that follows the \"logo + title\"\nconvention.\n\n### StatBlock\n\nThemed top border, big spring-counted number, label, optional trend pill.\n\n```ts\nstatBlock({\n id: 'views',\n current: 195654112,\n previous: 159560036, // optional \u2014 adds the trend pill\n label: 'Total views',\n color: 'pink',\n x: 120, y: 380,\n width: 1680,\n time: 4.33, duration: 6.0,\n layerBase: 200,\n})\n```\n\nStack two with `y` 340 px apart for a \"Total views / Total minutes watched\"\nlayout.\n\n### BarChartRow\n\nRow with an animated white background bar, top border, value count-up,\noptional icon, label, optional trend pill. Used for \"Top 5 by X\" lists.\n\n```ts\nbarChartRow({\n id: 'phone',\n value: 130733672,\n max: 130733672, // the leading value in the dataset\n previous: 133478441, // optional \u2014 adds trend pill\n label: 'Phone',\n icon: { viewBox: [0, 0, 32, 52], paths: PHONE_ICON_PATHS },\n color: 'green',\n x: 120, y: 320,\n width: 1680,\n time: 10.33, duration: 6.0,\n staggerIndex: 0, // 0..n for cascading entry\n layerBase: 400,\n})\n```\n\nLoop over your dataset, increment `y` by `168` and `staggerIndex` by 1\nper row.\n\n### RankedList\n\nNumbered list of items with rank, name, value, animated measure bar.\n1 or 2 columns. Used for \"Top 10\" style scenes.\n\n```ts\nrankedList({\n id: 'titles',\n items: [\n { label: 'Burgandy Alert', value: 2733413 },\n { label: 'Tough Affection', value: 1694421 },\n // ...\n ],\n color: 'lavender',\n x: 120, y: 320,\n width: 1680,\n columns: 2,\n time: 16.33, duration: 6.0,\n layerBase: 600,\n})\n```\n\n### PieCard\n\nAnimated pie chart (white slice growing over the colored body), big\npercentage label, count-up view count, optional trend pill, optional\nlogo on a rounded white square, bottom label. One card per data point;\nposition multiple horizontally for a \"Top 4 X\" scene.\n\n```ts\npieCard({\n id: 'chrome',\n value: 2233793,\n total: 3396911, // dataset total \u2014 drives the percentage\n previous: 2337628, // optional \u2014 adds trend pill\n label: 'Chrome',\n logoUrl: '/chrome.png', // optional \u2014 image over the white square\n color: 'blue',\n cx: 300, // center x of the card\n time: 28.33, duration: 6.0,\n staggerIndex: 0,\n layerBase: 1000,\n})\n```\n\n### IntroCard (component)\n\nFull-frame opening title: themed backdrop, optional all-caps kicker,\nbig headline, accent rule wipe, optional subtitle. Entrance + exit\nanimation built in. Returns ONE `group` element \u2014 position, animate, or\n`time_remap` the whole card as a unit.\n\n```ts\nelements.push(introCard({\n id: 'open',\n kicker: 'Mux Data',\n headline: '2021 in review',\n subtitle: 'Your year of video, in numbers',\n color: 'pink',\n canvasWidth: 1920, canvasHeight: 1080,\n time: 0, duration: 4,\n layer: 100,\n}))\n```\n\nNote components return a single `Element` \u2014 `push`, don't spread.\n\n### LowerThird (component)\n\nBroadcast-style name strip: accent bar + white panel + bold name +\noptional role line. Slides in from the left, fades out at the end of\nits window. Returns ONE `group` element.\n\n```ts\nelements.push(lowerThird({\n id: 'speaker',\n name: 'Dana Cruz',\n role: 'Head of Video',\n color: 'blue',\n x: 80, y: 900, // left edge / vertical center\n time: 12, duration: 5,\n layer: 300,\n}))\n```\n\n### TiltedShowcase (component)\n\nThe promo signature shot: a screenshot in a browser-chrome frame,\ntilted in 3D (CKP/1.0) and gently swinging. Returns ONE `clip: true`\ngroup, so the framed card projects as a unit. Pair with a Source-level\n`camera: { perspective: 1200 }` for converging perspective.\n\n```ts\nelements.push(tiltedShowcase({\n id: 'app-shot',\n source: '/screens/dashboard.png',\n color: 'blue',\n x: 960, y: 540, // card center\n width: 760, height: 500,\n tilt: 22, // swings \u00B122\u00B0; 0 = static\n z: 40,\n time: 2, duration: 6,\n layer: 200,\n}))\n```\n\n### cameraOrbit (scene camera)\n\nUnlike the others, this returns a `Camera` for `source.camera` (not an\nelement): a ready-made keyframed move \u2014 orbit (`yaw`), constant `pitch`\ntilt, `dolly` (z), `truck` (x). Place content at varied `z` for parallax;\ndepth-correct occlusion is automatic (\u00A74.4.3).\n\n```ts\nconst source = {\n camera: cameraOrbit({ perspective: 1500, yaw: 40, pitch: 6, dolly: 120, duration: 5 }),\n elements: [ /* cards at different z */ ],\n};\n```\n\n### TrendPill (helper)\n\nSmall pill rendering a signed percentage delta. Used internally by\nStatBlock, BarChartRow, PieCard, but exported so you can place one\nstandalone.\n\n```ts\ntrendPill({\n id: 'standalone-trend',\n cx: 1500, cy: 540,\n delta: trendPct(2233793, 2337628), // \u2192 -4.4\n color: 'pink',\n time: 4.33, duration: 6.0,\n layerBase: 250,\n})\n```\n\n---\n\n## 3. Recipe gallery\n\nExample videos demonstrating patterns in context. Source files live in\n`apps/playground/src/`:\n\n- **`mux-demo.ts`** \u2014 `MUX_DEMO`. 1920\u00D71080, 39 s, 7 scenes (intro, big\n stats, devices, top-10 titles, US heatmap, browsers, outro). The\n full-fidelity reference; every pattern in section 2 shows up here.\n- **`examples.ts`** \u2014 `VERCEL_TEMPLATE`. 1280\u00D7720, 6.7 s. Showcases the\n SVG stroke-evolution primitive (Next.js logo paths drawing in), the\n text linear-wipe mask, and keyframe-animated rings.\n- **`examples.ts`** \u2014 `CODE_WRAPPED`. 9:16, 15 s. Particle confetti\n bursts at celebration moments, gradient backgrounds, spring stat\n reveals \u2014 the github-unwrapped-style template.\n\nWhen you author a new video, lean on these as references. If a scene\n\"looks like X scene in mux-demo,\" call the same pattern with the same\nshape of args.\n\n### Captions from media (transcription)\n\nTo caption a video or audio clip, you don't hand-write `words` \u2014 transcribe\nthe media into a `caption` element. Runs Whisper **locally** (no API key, no\nupload) via `@clipkit/speech-to-text`; needs `ffmpeg` on the host.\n\n- **CLI** (local files): `clipkit transcribe <file>` prints the caption\n `words[]` JSON to stdout (progress on stderr). Add `--element` for a full\n `caption` element ready to drop into a Source, `--model Xenova/whisper-small`\n for accuracy or `\u2026-tiny.en` for speed, `--out cap.json` to write a file.\n- **MCP** (chat-mode agents): the `transcribe_media` tool takes a local\n `path`, transcribes, and adds a `caption` element to the current project\n (`add: false` to only return it).\n\nBoth produce the same thing: a `caption` element with `words: [{ text, start,\nend }]` (times relative to the caption's `time`). The protocol/runtime are\nunchanged \u2014 this only *fills* the caption the renderer already supports. Style\nit with the usual `caption` fields (`style`, `highlight_color`, \u2026).\n\n---\n\n## 4. Authoring guidance\n\nA few rules of thumb that go beyond the schema:\n\n- **Lead with patterns; reach for primitives only for one-offs.** If a\n scene fits HeaderBar + BarChartRow, do that. Don't reinvent.\n- **Stagger entrances.** For lists, use `staggerIndex` (or compute a\n `time: time + i*0.07` offset for primitives). All-at-once entrances\n look amateurish.\n- **Sound matters.** Set up an `audio` element early. The runtime mixes\n it into the export and the playground plays it during preview.\n- **Pace by frames at 30 fps.** Most scenes are 180 frames = 6 s. Title\n intros are 100\u2013130 frames. Outros 120\u2013150. Big stat reveals 180.\n- **Trend pills are optional.** Only add `previous` to a pattern if the\n comparison is genuinely interesting; otherwise let the number land\n on its own.\n\n---\n\n## 5. Output format\n\nWhatever you generate, the final output the user runs is a Clipkit\n`Source` JSON object. Patterns are TypeScript-only \u2014 they help you\nconstruct that JSON but never appear in it.\n\nIf you're emitting code for the user:\n\n```ts\nimport type { Source } from '@clipkit/protocol';\nimport { headerBar, statBlock /* ... */ } from '@clipkit/patterns';\n\nexport const MY_VIDEO: Source = {\n width: 1920, height: 1080, duration: 30, frame_rate: 30,\n elements: [\n ...headerBar({ ... }),\n ...statBlock({ ... }),\n // ...\n ],\n};\n```\n\nIf you're emitting raw JSON (no TS), inline the patterns' output\ndirectly. There's no `{ \"type\": \"stat_block\" }` \u2014 only the primitive\nelements the patterns produce.\n\n---\n\n## Appendix \u2014 Protocol change checklist (maintainers)\n\n> **This section is for editing the protocol itself, not for authoring\n> videos.** If you're writing a Source, ignore it. If you're adding or\n> changing an element type, field, effect, animation preset, output\n> format, etc., work this list \u2014 a protocol change \"bleeds down\" into\n> more places than it looks.\n\n**The schema source of truth (`packages/protocol/src/`) \u2014 all manual:**\n\n- `types.ts` \u2014 the interface / const enum (`ELEMENT_TYPES`,\n `ANIMATION_TYPES`, `OUTPUT_FORMATS`, the effect union, \u2026).\n- `zod.ts` \u2014 the matching validator. Const-driven enums sync from the\n arrays; a new interface needs a new schema object. **Put the field's\n default in its `.describe()` text** \u2014 that string is the only place a\n default is declared protocol-side. It's surfaced to agents via\n `get_schema` \u2192 `zodToJsonSchema`, so the wording an agent reads (\"\u2026\n (default 1)\") comes straight from here. Keep it honest against the\n runtime.\n- `CLIPKIT_PROTOCOL_VERSION` in `types.ts` \u2014 bump only on a real spec\n change.\n\nThere is **no central defaults table.** (A former `defaults.ts` /\n`applyDefaults()` was dead code \u2014 never imported or called \u2014 and has\nbeen deleted; don't re-add it.) The *effective* default for a field is\nthe inline `?? fallback` in whichever runtime renderer reads it\n(`packages/runtime/src/compositor/element-renderers/*.ts`, e.g.\n`fillColor = span.fill_color ?? defaults.color` in `text.ts`) \u2014 one\nfallback per field, at its point of use. So a new field with a default\nmeans **two** edits that must agree: the runtime's `??` fallback, and\nthe `.describe()` text in `zod.ts` that documents it.\n\n**Runtime (`packages/runtime/src/`):** a new field on an existing\nelement auto-works if the renderer already reads it. A new **element\ntype** needs a renderer in `compositor/element-renderers/`; a new\n**effect type** needs handling in the effect chain; a new **animation\npreset** needs a handler in `animation/presets.ts`.\n\n**Editor inspector \u2014 mostly automatic.** The registry deriver\n(`editor-core/src/registry/derive.ts`) reads the zod schema by\nstructure, so a new field gets a working control with no edits. Only\ntouch `registry/overrides.ts` when the auto-derived control is wrong\n(range, label, section, or it fell back to a raw JSON chip). Verify\nwith `node probes/probe-editor-registry.mjs` \u2014 it asserts every field\nhas exactly one knob and lists anything that fell through.\n\n**Docs (all hand-written, no codegen):**\n\n- `PROTOCOL.md` \u2014 the formal spec.\n- This file (`AGENTS.md`) \u2014 authoring patterns, if it's a preset/effect.\n- `apps/web/content/docs/fields/*.md` \u2014 per-element field reference\n (separate from PROTOCOL.md; easy to miss).\n- `packages/mcp-server/src/embedded-docs.ts` \u2014 a COPY of the agent\n guidance baked into the MCP server; it does **not** auto-sync with\n this file, so it drifts silently.\n\n**Agent / MCP surface:** `packages/mcp-server/src/tools.ts` \u2014\ntool input schemas are hand-written. A new top-level Source field or a\nnew `output_format` needs a parameter added here. Element-field CRUD\nvalidates against zod automatically.\n\n**Situational:** `apps/web/lib/playground/examples.ts` (showcase the\nnew feature) and `packages/snapshot-importer/` (if it should map from\nimported HTML/CSS).\n\nThe genuinely forgettable manual ones: **the runtime `??` fallback that\nsets the field's default, the matching `.describe()` default text in\n`zod.ts`, the `content/docs/fields/*.md` reference, and the MCP\n`embedded-docs.ts` copy** \u2014 then run the registry probe.\n";
|
|
2
|
+
export declare const PROTOCOL_MD_CONTENT = "# The Clipkit Protocol\n\n**Version:** 1.0 (CKP/1.0)\n**Status:** Draft\n**License:** Apache-2.0\n\nThis document specifies the Clipkit Protocol \u2014 the JSON-based interchange\nformat for describing motion-graphics videos. Documents that conform to\nthis protocol can be rendered by any conforming runtime: the reference\nimplementation `@clipkit/runtime`, future server-side renderers, or\nthird-party implementations.\n\nThe key words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, **MAY**,\nand **REQUIRED** in this document are to be interpreted as described in\n[RFC 2119](https://www.rfc-editor.org/rfc/rfc2119).\n\nThe reference implementation lives in [`packages/protocol`](./packages/protocol)\nof this repository.\n\n---\n\n## Table of contents\n\n1. [Introduction](#1-introduction)\n2. [Document structure](#2-document-structure)\n3. [Coordinate system, units, and types](#3-coordinate-system-units-and-types)\n4. [The element model](#4-the-element-model)\n5. [Element types](#5-element-types)\n6. [Animation](#6-animation)\n7. [Time, duration, sequencing](#7-time-duration-sequencing)\n8. [Asset references](#8-asset-references)\n9. [Output and rendering](#9-output-and-rendering)\n10. [Conformance levels](#10-conformance-levels)\n11. [Versioning and extensions](#11-versioning-and-extensions)\n12. [Implementation notes](#12-implementation-notes-non-normative)\n\n---\n\n## 1. Introduction\n\n### 1.1. Goals\n\nThe Clipkit Protocol aims to be:\n\n- **JSON-native.** A Clipkit document is plain JSON. Any tool that can\n write JSON can produce one. AI agents in particular benefit from this\n \u2014 they emit structured data better than they emit code.\n- **Renderer-agnostic.** The protocol describes *what* a video looks\n like, not *how* to render it. A conforming runtime may use WebGPU,\n WebGL2, software rasterization, server-side headless browsers, or any\n other mechanism.\n- **Composable.** Documents are built from a small set of primitive\n element types. Higher-level patterns are authoring concerns \u2014 they\n produce primitive elements; the protocol itself has no notion of\n \"pattern.\"\n- **Deterministic.** Given the same document and the same time, every\n conforming runtime MUST produce the same frame composition. Exact\n pixel output may differ between rasterization backends, but the\n scene description is unambiguous.\n\n### 1.2. Non-goals\n\n- **Visual editing format.** Editors MAY store additional state alongside\n a Source (selection, undo history, asset binaries) but that state is\n outside this protocol.\n- **Media container.** Clipkit references external media (images, video,\n audio) by URL or path. The protocol does not embed binary media.\n- **Audio mixing graph.** Audio elements are positioned in time; the\n exact mix algorithm (gains, codecs, bitrates) is implementation-defined.\n\n### 1.3. Reference implementation\n\n`@clipkit/protocol` is the canonical TypeScript + Zod implementation of\nthis protocol. Other implementations SHOULD treat it as authoritative on\nambiguous points until those points are resolved here.\n\n---\n\n## 2. Document structure\n\n### 2.1. The Source object\n\nA Clipkit document is a JSON object with the following shape:\n\n```json\n{\n \"clipkit_version\": \"1.0\",\n \"output_format\": \"mp4\",\n \"width\": 1920,\n \"height\": 1080,\n \"duration\": 30,\n \"frame_rate\": 30,\n \"background_color\": \"#000000\",\n \"elements\": [ /* ... */ ]\n}\n```\n\n#### Fields\n\n| Field | Type | Required? | Default | Meaning |\n|---|---|---|---|---|\n| `clipkit_version` | string | SHOULD | `\"1.0\"` | The protocol version this document targets. See \u00A711. |\n| `output_format` | string | MAY | `\"mp4\"` | One of `\"mp4\"` (video) or `\"gif\"` (animated). Clipkit is video-only. |\n| `width` | integer | SHOULD | `1920` | Composition width in pixels. MUST be positive. |\n| `height` | integer | SHOULD | `1080` | Composition height in pixels. MUST be positive. |\n| `duration` | number \\| `\"auto\"` | SHOULD | `\"auto\"` | Total composition duration in seconds. `\"auto\"` MUST be interpreted as the maximum end-time of any active element. |\n| `frame_rate` | number | SHOULD | `30` | Frames per second. MUST be positive. |\n| `background_color` | string | MAY | `\"#000000\"` | Color (\u00A73.4) the frame is cleared to before any element draws. Absent \u2192 opaque black. |\n| `motion_blur` | object | MAY | \u2014 | Whole-frame motion blur by exact sub-frame supersampling. See \"Motion blur\" below. |\n| `camera` | object | MAY | \u2014 | Scene camera (CKP/1.0): perspective lens + movable pose (position/orientation) + `sort`. Absent = identity = exact 2D. See \u00A74.4.2, \u00A74.4.3. |\n| `fonts` | array | MAY | \u2014 | Font faces the renderer MUST register before rendering. See \"Fonts\" below. |\n| `elements` | array | REQUIRED | \u2014 | At least one element. See \u00A74\u20135. |\n\n#### Motion blur\n\n`motion_blur` is an object with two optional fields:\n\n| Field | Type | Default | Meaning |\n|---|---|---|---|\n| `samples` | integer 1\u201332 | `8` | Sub-frame samples rendered per output frame. `1` disables blur. |\n| `shutter` | number (0, 1] | `0.5` | Fraction of the frame interval the shutter is open. `0.5` corresponds to a 180\u00B0 film shutter. |\n\nWhen present with `samples` \u2265 2, the renderer MUST produce each output\nframe as the arithmetic mean of `samples` full-scene renders taken at\nsub-frame times centered on the frame time. For the output frame at\ntime `t` with frame rate `f` and composition duration `D`:\n\n```\nt_k = clamp(t + ((k + 0.5) / samples \u2212 0.5) \u00D7 shutter / f, 0, D)\n for k = 0 \u2026 samples\u22121\n```\n\nEach sample is a complete render of the composition at `t_k` under the\nnormal rendering model (every animated value, transition, and effect is\nevaluated at `t_k`). The output pixel is the per-channel arithmetic\nmean of the `samples` sample pixels in the output color space (8-bit\nsRGB, after compositing over the opaque background), rounded once,\nhalf away from zero. Accumulation MUST be carried at a precision that\nmakes the result exact before that single rounding (e.g. float\naccumulation of 8-bit samples).\n\nThis is deterministic: the same document produces the same pixels.\nMedia elements evaluate `t_k` through their own media-time mapping\n(\u00A75.3.2); a video whose decoder quantizes to its own frame times\ncontributes the same decoded frame to several samples, which is\nconformant.\n\nMotion blur applies at export/render time. Interactive previews MAY\nrender the unblurred scene (a single sample at `t`) for speed and MUST\nNOT be treated as the reference output when `motion_blur` is present.\n\n#### Fonts\n\n`fonts` is an array of font-face objects. The renderer MUST register\nevery face before the first frame renders, so a document carrying its\nfonts is self-sufficient \u2014 text metrics don't depend on what the host\nenvironment happens to have installed.\n\n| Field | Type | Required? | Default | Meaning |\n|---|---|---|---|---|\n| `family` | string | REQUIRED | \u2014 | The `font_family` name text elements reference. |\n| `weight` | number \\| string | MAY | `\"normal\"` | CSS font-weight. Variable fonts MAY declare a range (e.g. `\"100 900\"`). |\n| `style` | string | MAY | `\"normal\"` | `\"normal\"` or `\"italic\"`. |\n| `src` | string | REQUIRED | \u2014 | URL of the font bytes: absolute, relative (resolved against the document hosting the Source), or a `data:` URI. |\n| `unicode_range` | string | MAY | \u2014 | CSS unicode-range (e.g. `\"U+0000-00FF, U+0131\"`). Subsetted webfonts ship one file per script under identical `family`/`weight`/`style`; without the range every subset matches every codepoint, and the winning file may lack the glyphs being rendered. Renderers MUST honor it when matching glyphs to faces. |\n\nMultiple entries MAY share a `family` (different weights, styles, or\nunicode-range subsets). Renderers MUST treat each entry as a distinct\nface, exactly as CSS treats multiple `@font-face` rules.\n\n### 2.2. Forward compatibility\n\nDocuments MAY contain additional top-level fields not listed here.\nConforming runtimes MUST ignore unknown top-level fields (passthrough).\nThis applies to all objects in this protocol unless otherwise stated.\n\n---\n\n## 3. Coordinate system, units, and types\n\n### 3.1. Coordinate system\n\n- The origin `(0, 0)` is at the **top-left** of the composition.\n- The positive **x** axis runs **right**.\n- The positive **y** axis runs **down**.\n- Positions, sizes, and offsets are in pixels relative to the composition\n width / height unless an explicit unit suffix is used (\u00A73.3).\n\n### 3.2. Anchors\n\nMost elements support `x_anchor` and `y_anchor` values in `[0, 1]`.\nThese define which point inside the element's bounding box is positioned\nat `(x, y)`:\n\n- `0` = left edge (or top edge) \u2014 **the default**\n- `0.5` = center\n- `1` = right edge (or bottom edge)\n\nBy default both anchors are `0`, so `(x, y)` is the element's **top-left\ncorner** \u2014 the CSS `left`/`top` / SVG / Canvas convention. For example,\n`x: 960` (anchor `0`) places the element's left edge at x=960;\n`x: 960, x_anchor: 0.5` places its center at x=960. The anchor only\nmoves where the box sits \u2014 rotation and scale always pivot the element's\ngeometric center (\u00A74.4), independent of the anchor.\n\n### 3.3. Length values\n\nWherever a position or dimension is accepted, a string with one of the\nfollowing unit suffixes MAY be used:\n\n| Unit | Meaning |\n|---|---|\n| `\"100px\"` | pixels (same as bare number `100`) |\n| `\"50%\"` | percentage of the property's natural reference (width \u2192 composition width; etc.) |\n| `\"10vw\"` | percentage of composition width |\n| `\"15vh\"` | percentage of composition height |\n| `\"5vmin\"` | percentage of the smaller composition dimension |\n| `\"5vmax\"` | percentage of the larger composition dimension |\n\nA bare number is interpreted as pixels.\n\n> **Divergence from CSS:** `%` and the viewport units resolve against the\n> **composition (canvas)**, never the parent element. `\"50%\"` is always\n> half the canvas, even on a child nested inside a group. There is no\n> parent-relative percentage; use pixels for child-relative sizing inside\n> groups.\n\n### 3.4. Color values\n\nColors are CSS-style strings. The reference runtime accepts:\n- hex \u2014 `\"#rgb\"`, `\"#rgba\"`, `\"#rrggbb\"`, `\"#rrggbbaa\"`\n- `rgb()` / `rgba()` \u2014 comma or space separated, alpha as 0..1 or `%`\n- `hsl()` / `hsla()` \u2014 same separators, optional `deg` on the hue\n- the 148 CSS named colors (`\"red\"`, `\"rebeccapurple\"`, \u2026) and\n `\"transparent\"`\n\nUnrecognized strings fall back to white. (Conformance: only hex support\nis REQUIRED of a runtime; the rest are RECOMMENDED for CSS parity.)\n\nInternally, all colors flow through rendering as straight-alpha sRGB,\npremultiplied at the final composition step. Authors do not need to\nthink about this; it is mentioned to document the reference runtime's\nbehavior.\n\n### 3.5. Keyframe values\n\nMany properties accept either a static value or a `Keyframe[]` array\nfor animation. The keyframe form is:\n\n```json\n[\n { \"time\": 0, \"value\": 0, \"easing\": \"ease-out-cubic\" },\n { \"time\": 1.0, \"value\": 100, \"easing\": \"ease-out-cubic\" }\n]\n```\n\n`time` is in seconds, relative to the element's `time` (see \u00A77).\n`value` can be a number, a string, or `[x, y]` for position-like\nproperties. `easing` is OPTIONAL; see \u00A76.4.\n\n### 3.6. Expression values\n\nAny **numeric** property (transform, `opacity`, blur/`brightness`/\n`contrast`/`saturation`, effect params, camera/light numbers \u2014 every\nproperty whose type admits a number) MAY instead be given as an\n**expression object**: a closed-form, deterministic function of the\nelement's own clock.\n\n```json\n{ \"y\": { \"expr\": \"540 + sin(t * PI) * 30\" } }\n{ \"rotation\": { \"expr\": \"t * 90\" } }\n{ \"x\": { \"expr\": \"960 + wiggle(3, 12)\" } }\n```\n\nAn expression is a **pure function of element-local time and the\nelement's own index** \u2014 nothing else. This is the entire safety\nboundary, and it is NORMATIVE:\n\n- It MUST NOT reference any other element (`ref()`, `valueAtTime`) or\n read any runtime input (mouse, audio). Those are Tier-B and are\n permanently unsupported.\n- It MUST be deterministic: every conforming runtime MUST produce\n identical results for the same expression and clock. `noise`,\n `wiggle`, and `random` derive from the protocol's normative\n value-noise hash (bit-stable across runtimes; seed defaults to `0`,\n never to wall-clock).\n- Because it is a function of `t`/`i`, it is **bakeable**: a runtime or\n tool MAY sample it to a `Keyframe[]` at any frame rate. A keyframe\n table and an expression are two encodings of the same value.\n\n**Scope** \u2014 the only identifiers in scope:\n\n| Variable | Meaning |\n|---|---|\n| `t` | element-local time, seconds (`0` at the element's `time`) |\n| `dur` | element duration, seconds |\n| `i` | element index within its generated/particle set (default `0`) |\n| `n` | sibling count (default `1`) |\n| `value` | the property's base value (its documented default) |\n\nConstants: `PI`, `TAU`, `E`. Functions (the ONLY callable identifiers):\n\n```\nsin cos tan asin acos atan atan2 sinh cosh tanh\nabs sign sqrt cbrt pow exp log log2 floor ceil round trunc fract\nmin max mod hypot\nclamp(x,lo,hi) lerp(a,b,u) mix(=lerp) step(edge,x) smoothstep(e0,e1,x)\nlinear(x,x0,x1,y0,y1) // map x\u2208[x0,x1] \u2192 [y0,y1], clamped\nease(x,x0,x1,y0,y1) // same, cubic in-out\nnoise(x[,seed]) // value noise, \u2208[-1,1]\nwiggle(freq,amp[,seed]) // amp \u00B7 fractal noise(t\u00B7freq)\nrandom(seed) // deterministic [0,1) hash, time-independent\n```\n\nOperators: `+ - * / % ^` (`^` = exponent, right-assoc), unary `-`,\ncomparisons `< > <= >= == !=`, logical `&& || !`, and the ternary\n`cond ? a : b`. Nothing else \u2014 no member access, assignment, indexing,\nstatements, or string values.\n\n**Evaluation (NORMATIVE).** A conforming runtime MUST evaluate\nexpressions with a restricted parser/evaluator, NOT a general\ncode-execution facility (`eval` / `Function`). Any unknown identifier or\nfunction, member access, assignment, or string literal is a parse error;\na parse error or a non-finite (`NaN` / `\u00B1\u221E`) result MUST fall back to the\nproperty's base value. Expressions are numeric-only in this version;\nstring/text expressions are reserved.\n\n---\n\n## 4. The element model\n\nEvery element extends a common base:\n\n```ts\ninterface BaseElement {\n id?: string;\n name?: string;\n type: ElementType; // discriminator\n layer: number; // REQUIRED, unique per container; 1..1000; LOWER draws in front (layer 1 = on top)\n time?: number | string; // seconds\n duration?: number | string | \"auto\" | \"end\";\n\n // Transform (numbers OR length strings OR Keyframe[])\n x?: number | string | Keyframe[];\n y?: number | string | Keyframe[];\n x_anchor?: number | string;\n y_anchor?: number | string;\n width?: number | string | Keyframe[];\n height?: number | string | Keyframe[];\n rotation?: number | Keyframe[]; // degrees, around center\n scale?: number | Keyframe[];\n\n // Visual\n opacity?: number | Keyframe[]; // 0..1 (CSS convention), default 1\n visible?: boolean; // false skips rendering (\u00A74.2)\n blend_mode?: 'normal' | 'multiply' | 'screen' | 'add' | 'overlay' | 'hard-light' | 'soft-light'; // \u00A74.5\n blur_radius?: number | Keyframe[]; // Gaussian \u03C3 in px (\u00A74.6)\n brightness?: number | Keyframe[]; // multiplier, 1 = unchanged (\u00A74.6)\n contrast?: number | Keyframe[]; // multiplier, 1 = unchanged (\u00A74.6)\n saturation?: number | Keyframe[]; // multiplier, 1 = unchanged (\u00A74.6)\n hue_rotate?: number | Keyframe[]; // degrees, default 0 (\u00A74.6)\n effects?: Effect[]; // ordered stylize passes (\u00A74.7)\n material?: Material; // PBR material; only shaded under scene lights (\u00A74.8)\n\n // Animation\n animations?: Animation[];\n keyframe_animations?: KeyframeAnimation[];\n}\n```\n\n### 4.1. The `type` discriminator\n\n`type` is REQUIRED and identifies which variant the object is. Conforming\nruntimes MUST recognize the values defined in \u00A75. Documents containing\nunknown `type` values are valid documents; runtimes MAY skip such\nelements with a warning rather than failing the whole document.\n\n### 4.2. Draw order\n\nEvery element owns a **`layer`** \u2014 a unique integer 1..1000 within its\ncontainer (the top-level `elements`, each group's `elements`, each group\nmask's `elements`), like an After Effects layer. **Lower `layer` draws\nin front; layer 1 is on top.** Elements draw back-to-front by **depth**\n(`z`, \u00A74.4), then by `layer`:\n\n```\ndraw_key = (depth descending, i.e. farther first), then layer descending\n (highest layer drawn first/behind, layer 1 drawn last/on top)\n```\n\n`z` is the single depth axis (\u00A74.4.2): with no camera it is pure\nstacking order (no perspective), with a camera it additionally\nforeshortens. `layer` orders elements *within* equal depth \u2014 when depths\nare equal (e.g. a 2D document where every `z` is 0), draw order is\nexactly layer descending, so layer 1 ends up on top. `layer` is\n**required and unique per container** (duplicate layers are a validation\nerror), so no two elements tie; the sort MUST nonetheless be stable. The\nsame rule applies to a group's children, locally within the group.\n(There is no separate `z_index` field \u2014 depth ordering is unified onto\n`z`. `camera.sort: 'paint'` opts a camera composition back into fixed\n`layer` order; see \u00A74.4.3.)\n\nElements with `visible: false` MUST NOT be rendered. Inactive elements\n(\u00A77.1) MUST NOT be rendered.\n\n### 4.3. Anchors and bounding boxes\n\nThe element's bounding box is `width \u00D7 height` pixels, with its\nanchor point at `(x, y)`. The visual center MUST be computed\nfrom the anchor:\n\n```\ncenter_x = x + (0.5 - x_anchor) * width\ncenter_y = y + (0.5 - y_anchor) * height\n```\n\nRotation rotates the element around its center.\n\n### 4.4. Transform composition\n\nEvery element supports, in addition to position and rotation:\n\n| Field | Type | Default | Meaning |\n|---|---|---|---|\n| `scale` | number | `1` | Uniform scale factor. |\n| `x_scale`, `y_scale` | number \\| `\"N%\"` | `1` | Per-axis scale factors; `\"150%\"` \u2261 `1.5`. |\n| `x_skew`, `y_skew` | number (degrees) | `0` | Shear, CSS `skewX`/`skewY` semantics: positive `x_skew` moves the bottom edge right; positive `y_skew` moves the right edge down. |\n\nSince CKP/1.0, every element also supports a 3D transform:\n\n| Field | Type | Default | Meaning |\n|---|---|---|---|\n| `z_rotation` | number (degrees) \\| Keyframe[] | `0` | Rotation in the element's plane. Exact alias for `rotation` \u2014 authoring BOTH on one element MUST be rejected by validators. |\n| `x_rotation` | number (degrees) \\| Keyframe[] | `0` | Rotation around the element's local x axis; positive tips the top edge away from the viewer. |\n| `y_rotation` | number (degrees) \\| Keyframe[] | `0` | Rotation around the element's local y axis; positive turns the right edge away from the viewer. |\n| `z` | number (px) \\| Keyframe[] | `0` | Depth toward (+) / away from (\u2212) the viewer. The depth axis for \u00A74.2 draw order: higher `z` draws nearer the viewer (on top), with `layer` ordering elements within equal depth (lower layer in front). Under a camera it additionally drives perspective foreshortening; with no camera it is pure stacking. |\n\nAll are animatable. The effective axis scales are\n`sx = scale \u00D7 x_scale`, `sy = scale \u00D7 y_scale`.\n\n#### 4.4.1. The local matrix (normative)\n\nAn element's local transform is the 4\u00D74 matrix, in pixels, with the\nanchor-derived center `(cx, cy)` (\u00A74.3) as the pivot:\n\n```\nM_local = T(cx, cy, z) \u00B7 Rz(z_rotation) \u00B7 Ry(y_rotation) \u00B7 Rx(x_rotation)\n \u00B7 K(x_skew, y_skew) \u00B7 S(sx, sy, 1) \u00B7 T(\u2212cx, \u2212cy, 0)\n```\n\nwhere `Rx/Ry/Rz` are the standard right-handed rotation matrices in\nthe protocol's coordinate system (x right, y DOWN, z toward the\nviewer; positive `z_rotation` is therefore clockwise on screen, as in\nCKP/1.0), and `K` embeds the 2D shear `[[1, tan(x_skew)],\n[tan(y_skew), 1]]` (CSS `skew(x, y)` semantics) in the xy plane. With\n`x_rotation = y_rotation = z = 0` this reduces exactly to the CKP/1.0\ncomposition **scale \u2192 shear \u2192 rotate** around the anchor-derived\ncenter; documents without 3D fields MUST render identically under\nboth models.\n\nGroup transforms stack multiplicatively onto children:\n`M = M_parent \u00B7 M_local`, evaluated in the group's local space, and\nopacities multiply down the group chain. Children of a group that\ndoes NOT rasterize to a layer (no `clip`, `mask`, filter fields, or\n`effects` \u2014 see \u00A74.4.3) therefore live in the parent's 3D space:\nnested 3D rotations compose, with no opt-in flag.\n\n#### 4.4.2. The camera\n\nA Source MAY declare one scene camera \u2014 a perspective lens (`perspective`,\n`origin_x`, `origin_y`) plus an optional rigid **pose** (a position and a\nlook orientation) that moves the viewpoint through the scene:\n\n```ts\ncamera?: {\n perspective: number | Keyframe[]; // focal distance in px, > 0\n origin_x?: number | string; // default \"50%\" (canvas center)\n origin_y?: number | string; // default \"50%\"\n\n // Eye position offset from the default eye, px, about the origin. Default 0.\n x?: number | Keyframe[]; // +x right\n y?: number | Keyframe[]; // +y down\n z?: number | Keyframe[]; // +z = eye toward the scene (dolly in)\n\n // Eye orientation, degrees, Euler (applied Rz\u00B7Ry\u00B7Rx). Default 0.\n x_rotation?: number | Keyframe[]; // pitch (tilt)\n y_rotation?: number | Keyframe[]; // yaw (pan)\n z_rotation?: number | Keyframe[]; // roll\n\n sort?: 'depth' | 'paint'; // compositing order, \u00A74.4.3. Default 'depth'.\n}\n```\n\nThe lens follows CSS Transforms Module Level 2 `perspective()`\nsemantics: with `d = perspective` and origin `(ox, oy)`,\n\n```\nP = T(ox, oy, 0) \u00B7 [ 1 0 0 0 ; 0 1 0 0 ; 0 0 1 0 ; 0 0 \u22121/d 1 ] \u00B7 T(\u2212ox, \u2212oy, 0)\n```\n\nThe pose is the **inverse of the camera's rigid world transform**, taken\nabout the origin so it composes with the lens. With eye position\n`(x, y, z)` and rotation `R = Rz\u00B7Ry\u00B7Rx`,\n\n```\nV = T(ox, oy, 0) \u00B7 R\u207B\u00B9 \u00B7 T(\u2212x, \u2212y, \u2212z) \u00B7 T(\u2212ox, \u2212oy, 0)\n```\n\nand the camera matrix applied once at the root is\n\n```\ncamera = P \u00B7 V\n```\n\nso every element renders through `P \u00B7 V \u00B7 M_chain \u00B7 M_local`, followed\nby the perspective divide. Orientation is given as explicit Euler angles\nrather than a look-at target: a target would derive orientation from\ngeometry (hidden runtime math); a \"look at this point\" gesture is an\nauthoring convenience that resolves to these angles, not a schema field.\n\n**Identity reduction (normative).** With the pose at its defaults\n(`x=y=z=0`, all rotations `0`), `V = I` and `camera = P` **bit-for-bit**:\na document that uses only `perspective`/origin renders identically to a\npre-pose runtime. Smaller `d` = stronger foreshortening; `perspective`\nis animatable (camera push/pull), as are all pose fields (orbit, pan,\ntilt, dolly).\n\n**No camera \u21D2 camera = I.** 3D rotations still render (affine\nforeshortening \u2014 a y-rotated card narrows but its edges stay parallel).\n`z` has no *perspective* effect without a camera, but it still **orders**\n(\u00A74.2, \u00A74.4.3): `z` is the single depth axis, so without a camera it acts\nas pure stacking order. A document with no 3D fields (all `z = 0`) and no\ncamera renders as pure layer stacking \u2014 equal depth collapses to `layer`\norder (descending, so layer 1 is on top).\n\n#### 4.4.3. Compositing under 3D (normative)\n\n- **Paint order \u2014 depth (2.5D).** `z` is the single depth axis and it\n orders **always**: each sibling draw list (the top-level elements, and\n the children of each non-flattened group) is painted **back-to-front\n by depth** \u2014 the eye-space `z` of the sibling's anchor (its `z` with no\n camera; `z` after `P \u00B7 V` under a camera), far cards first. With **no\n camera** this is pure stacking with no perspective; with a **camera**\n it is the same ordering plus foreshortening. This is whole-card\n (per-element) 2.5D sorting \u2014 flat cards ordered by distance \u2014 NOT a\n per-pixel depth buffer. The sort is **stable**: equal depths break by\n `layer` order (descending \u2014 layer 1 drawn last/on top), so a document\n where every `z = 0` collapses to exact `layer` order, and the same\n Source always yields the same pixels. A flattened group (clip / mask / filters /\n effects) is a single card and sorts by its own anchor depth as a\n unit; the \u00A74.4.3 flattening rule means its children are already\n coplanar inside the flat layer and keep their in-layer order. An\n element's own internal quads (a particle system's particles, a text\n element's glyphs) are likewise NOT reached by this sort \u2014 they follow\n their own documented order (e.g. particles in spawn order, \u00A75.13).\n **Limitation (normative, not a bug):** because the unit of sorting is\n the whole card, cards that interpenetrate, or whose anchor-depth order\n disagrees with their true per-pixel order, can be ordered \"wrong\" at\n some camera angles. There is no per-pixel resolution in the 2.5D\n model. `sort: 'paint'` opts a camera composition back into fixed\n `layer` order for authors who want explicit, camera-stable\n layering. There is no depth buffer in the rendering model.\n- **Flattening at layer boundaries.** A group with `clip: true` or\n `mask` renders its children in its own flat 2D layer space exactly\n as in CKP/1.0, and the finished layer's quad is then transformed by\n the full matrix chain. 3D declared INSIDE such a subtree composes\n only within that flat layer's plane; 3D declared ON or ABOVE it\n projects the layer as a unit. (This is how a clipped UI-mock group\n tilts as one card.)\n- **Effect surfaces are screen-space.** Filter fields (`blur_radius`,\n `brightness`, `contrast`, `saturation`, `hue_rotate`) and `effects`\n entries evaluate on the element's PROJECTED rendering: the element\n (or group subtree) draws with its full transform \u2014 including 3D and\n the camera \u2014 into a surface-sized layer, and the filter/effect chain\n runs on those screen-space pixels. This matches CKP/1.0, where the\n element's own transform is likewise baked into its effect surface\n before filtering, and means e.g. a glow's radius or a stroke's width\n is uniform in screen pixels on a tilted card. A shape's native\n `shadow` foreshortens with the element's plane, but its\n `offset_x`/`offset_y` translate in the PARENT plane \u2014 consistent\n with CKP/1.0, where a rotated shape's shadow offset stays\n screen-aligned.\n- **Glass.** Glass is legal under 3D. The pane is a true plane in the\n scene: pane-local coordinates come from the inverse of the pane's\n plane homography (the restriction of the full \u00A74.4 matrix chain to\n the pane's plane \u2014 equivalent to exact per-fragment ray/plane\n intersection), the \u00A74.7 optical model runs unchanged in that local\n frame, and refracted sample points map FORWARD through the\n homography onto the screen-space backdrop snapshot. See \u00A74.7 \"Glass\n under 3D\" for the normative model and degenerate cases. With no 3D\n on the element or its un-flattened chain, the orthographic CKP/1.0\n path applies bit-for-bit.\n- **Anti-aliasing.** SDF edge anti-aliasing remains derivative-based\n and scales naturally under projection; the \u00A71 cross-backend\n tolerance language applies unchanged.\n\n### 4.5. Blend modes\n\n`blend_mode` selects how the element's pixels combine with the pixels\nalready drawn beneath it. It is **element-local**: it changes only this\nelement's compositing math and MUST NOT alter how any other element\nrenders. With premultiplied sources:\n\n| Value | Color math | Character |\n|---|---|---|\n| `normal` (default) | `out = src + dst\u00B7(1 \u2212 src.a)` | Standard over. |\n| `multiply` | `out = src\u00B7dst + dst\u00B7(1 \u2212 src.a)` | Darkens; white is neutral; uncovered pixels leave the destination unchanged. |\n| `screen` | `out = src + dst\u00B7(1 \u2212 src)` | Lightens; black is neutral. |\n| `add` | `out = src + dst` | Linear dodge; overlaps sum toward white (glow). |\n| `overlay` | `B(cb,cs)` = `2\u00B7cb\u00B7cs` if `cb \u2264 0.5` else `1 \u2212 2\u00B7(1\u2212cb)\u00B7(1\u2212cs)` | Multiply in dark backdrop areas, screen in light ones; boosts contrast. |\n| `hard-light` | `B(cb,cs)` = overlay with source and backdrop swapped | Like shining a harsh light through the source. |\n| `soft-light` | `B(cb,cs)` per W3C soft-light | A gentler `hard-light` (soft dodge/burn). |\n\nThe blend function `B(cb, cs)` operates on **straight-alpha** (un-\npremultiplied) backdrop `cb` and source `cs` per channel; the result\ncomposites via the general separable formula\n`co = \u03B1s\u00B7(1\u2212\u03B1b)\u00B7cs + \u03B1s\u00B7\u03B1b\u00B7B(cb,cs) + (1\u2212\u03B1s)\u00B7\u03B1b\u00B7cb`, with\n`\u03B1o = \u03B1s + \u03B1b\u00B7(1 \u2212 \u03B1s)`. For `normal`/`multiply`/`screen`/`add` this\nreduces to the closed forms above, expressible with fixed-function\nblending. `overlay`/`hard-light`/`soft-light` are **piecewise on the\nbackdrop (or source)** and cannot be; a conforming runtime isolates\nthe element to its own layer and composites it against a snapshot of\nthe backdrop. The alpha channel always composites normally, so\ncoverage is unaffected by the mode.\n\nOn a `group`, `blend_mode` applies when the group's flattened layer is\ncomposited \u2014 which only exists when the group is layered via\n`clip: true` or `mask` (\u00A75.8). On an unlayered group the field MUST be\nignored (children draw directly to the parent surface with their own\nmodes); runtimes SHOULD warn. Children inside a layered group\ncomposite against each other inside the layer, isolated from the\nbackdrop \u2014 matching CSS `isolation: isolate` semantics.\n\n### 4.6. Filters\n\nFour element-local filter fields, all animatable, all following CSS\n`filter` function semantics:\n\n| Field | Default | Meaning |\n|---|---|---|\n| `blur_radius` | `0` | Gaussian blur; the value is the standard deviation \u03C3 in canvas pixels (CSS `blur(\u03C3)`). |\n| `brightness` | `1` | Color multiplier: `c' = c \u00D7 v` (CSS `brightness(v)`). |\n| `contrast` | `1` | Scale around mid-gray: `c' = (c \u2212 0.5) \u00D7 v + 0.5` (CSS `contrast(v)`). |\n| `saturation` | `1` | Lerp against Rec. 709 luma: `c' = mix(luma(c), c, v)`; `0` = grayscale (CSS `saturate(v)`). |\n| `hue_rotate` | `0` | Hue rotation by `v` DEGREES: `c' = M(v) \u00D7 c` with the SVG `feColorMatrix type=\"hueRotate\"` matrix below (CSS `hue-rotate(v)`). |\n\nThe `hue_rotate` matrix, NORMATIVE, with `cos\u03B8`/`sin\u03B8` of the angle:\n\n```\nM = [ 0.213+0.787cos\u03B8\u22120.213sin\u03B8 0.715\u22120.715cos\u03B8\u22120.715sin\u03B8 0.072\u22120.072cos\u03B8+0.928sin\u03B8 ]\n [ 0.213\u22120.213cos\u03B8+0.143sin\u03B8 0.715+0.285cos\u03B8+0.140sin\u03B8 0.072\u22120.072cos\u03B8\u22120.283sin\u03B8 ]\n [ 0.213\u22120.213cos\u03B8\u22120.787sin\u03B8 0.715\u22120.715cos\u03B8+0.715sin\u03B8 0.072+0.928cos\u03B8+0.072sin\u03B8 ]\n```\n\nBlur evaluation is a NORMATIVE downsample ladder (so identical sources\nproduce identical pixels everywhere): bilinearly halve the image until\nthe residual `\u03C3 / f \u2264 4` (`f` a power of two, max 16), apply a\n25-tap Gaussian (taps at `\u03C3/f \u00F7 4` spacing over \u00B13\u03C3, weights\n`exp(\u2212d\u00B2/2\u03C3\u00B2)` normalized by their sum) horizontally then vertically\nat the reduced size, and bilinearly upsample at the consuming draw.\nSparse full-resolution taps are not an acceptable substitute \u2014 they\nleave a visible \u03C3/4-pixel grid on hard edges.\n\nA filtered element \u2014 any type, `group` included, layered or not \u2014 is\nrendered with its normal transform into a transparent offscreen layer,\nthen that layer is composited back through the filter. Filters MUST\napply in the order **blur \u2192 brightness \u2192 contrast \u2192 saturation \u2192\nhue_rotate**, and\nthe color ops MUST operate on straight (unpremultiplied) color so\ntranslucent pixels don't skew toward the contrast midpoint. Channel\nresults clamp to [0, 1] before re-premultiplying; the alpha channel is\nnever changed by color ops.\n\nFilters are element-local: the blur may bleed past the element's box\n(like CSS `filter: blur()`) but never reads or alters other elements'\npixels. The element's `opacity` applies inside the layer and its\n`blend_mode` applies at the filter composite, so all three features\ncompose. On a group, filtering flattens the subtree first \u2014 children\nare filtered as one image, not individually.\n\n### 4.7. Stylize effects\n\n`effects` is an ordered array of stylize passes over the element's\nrendered pixels, applied AFTER the filter fields:\n\n```\nlayer \u2192 blur \u2192 brightness \u2192 contrast \u2192 saturation \u2192 hue_rotate \u2192 effects[0] \u2192 \u2026 \u2192 effects[n]\n```\n\n```json\n{ \"effects\": [{ \"type\": \"pixelate\", \"cell_size\": 12 }] }\n```\n\nEach effect is an object discriminated by `type`. Effect params accept\n`number | Keyframe[]`; keyframes evaluate against element-local time.\n(Effect params are NOT addressable from `keyframe_animations` \u2014 there\nis no property-path syntax into the array.) Like filters, effects are\nelement-local, work on every element type (a `group` flattens its\nsubtree first), and the element's `blend_mode` applies at the final\ncomposite. Runtimes encountering an effect `type` they don't implement\nMUST skip that effect (rendering the element without it) and SHOULD\nwarn.\n\n**Effects read only the element's own rendered pixels \u2014 with ONE\nexclusion: `glass`.** Glass additionally reads the element's\n*backdrop*: the current surface's pixels at the element's position in\ndraw order (\u00A74.2), i.e. everything drawn before it on the same\nsurface. It gets this carve-out because the effect is widely known and\nin high demand, and there is no proper decomposition \u2014 refraction\nneeds the pixels behind the pane \u2014 and the alternative, a first-class\nglass element type, is deliberately not part of this protocol. No\nother effect type reads the backdrop, and backdrop-sampling blend\nmodes (overlay, soft-light) remain excluded (\u00A74.5). Note the\nrelationship stays one-way: glass READS what is beneath it but never\nalters how any other element renders.\n\nPixel-grid coordinates below are the element's layer pixels at output\nresolution; cells are aligned to the layer's origin. Color math runs\non straight (unpremultiplied) color; \"ink\" factors scale color and\nalpha together (premultiplied output).\n\n#### `pixelate`\n\n| Param | Default | Meaning |\n|---|---|---|\n| `cell_size` | `8` (min 1) | Cell size in canvas pixels. |\n\nEvery pixel takes the color sampled at its cell's center:\n`out(p) = src((floor(p / cell) + 0.5) \u00D7 cell)`.\n\n#### `dither`\n\n| Param | Default | Meaning |\n|---|---|---|\n| `levels` | `4` (min 2) | Quantization levels per color channel. |\n\nOrdered dithering with the 4\u00D74 Bayer matrix\n`[0 8 2 10; 12 4 14 6; 3 11 1 9; 15 7 13 5]`:\n`t = (B[y mod 4][x mod 4] + 0.5) / 16`, then per channel\n`c' = clamp(floor(c \u00D7 (levels\u22121) + t) / (levels\u22121), 0, 1)`.\nAlpha (coverage) is not dithered.\n\n#### `halftone`\n\n| Param | Default | Meaning |\n|---|---|---|\n| `cell_size` | `8` (min 2) | Dot-grid cell size in canvas pixels. |\n| `angle` | `45` | Grid rotation in degrees. |\n\nIn grid space (pixel coords rotated by `angle`), each cell draws a dot\nat its center, colored with the source sample at that center and sized\nby its luminance: `luma = Rec709(c) \u00D7 \u03B1`, `r = 0.5 \u00D7 cell \u00D7 \u221Aluma`\n(area-proportional ink). Pixel ink is\n`(1 \u2212 smoothstep(r\u22121, r+1, d)) \u00D7 clamp(r, 0, 1)` where `d` is the\ngrid-space distance to the dot center; outside dots the output is\ntransparent.\n\n#### `ascii`\n\n| Param | Default | Meaning |\n|---|---|---|\n| `cell_size` | `12` (min 4) | Glyph cell size in canvas pixels. |\n\nEach cell samples its center color; `luma = Rec709(c) \u00D7 \u03B1` selects a\nglyph by `i = clamp(floor(luma \u00D7 10), 0, 9)` from the ten-step density\nramp `space . - : = + % * @ #`. The glyph tints with the cell's color;\nuninked pixels are transparent. Glyph shapes are NORMATIVE \u2014 the\nembedded 8\u00D78 bitmap font below (one byte per row, MSB = leftmost\npixel), upscaled nearest-neighbor to the cell \u2014 so the effect never\ndepends on platform fonts:\n\n| Glyph | Rows (hex) |\n|---|---|\n| space | `00 00 00 00 00 00 00 00` |\n| `.` | `00 00 00 00 00 18 18 00` |\n| `-` | `00 00 00 7E 00 00 00 00` |\n| `:` | `00 18 18 00 00 18 18 00` |\n| `=` | `00 00 7E 00 7E 00 00 00` |\n| `+` | `00 18 18 7E 18 18 00 00` |\n| `%` | `00 C6 CC 18 30 66 C6 00` |\n| `*` | `00 66 3C FF 3C 66 00 00` |\n| `@` | `7C C6 DE DE DE C0 78 00` |\n| `#` | `6C 6C FE 6C FE 6C 6C 00` |\n\n#### `glow`, `drop_shadow`, `stroke` (layer styles)\n\nLayer styles composite BENEATH the element's own pixels (premultiplied\nunder-operator: `out = content + style \u00D7 (1 \u2212 content.\u03B1)`), on any\nelement type.\n\n| Effect | Params (defaults) | Math |\n|---|---|---|\n| `glow` | `radius` 20, `intensity` 1, `color` `\"#FFFFFF\"` | silhouette alpha blurred by \u03C3 = radius (\u00A74.6 ladder), \u00D7 intensity (clamped to 1), \u00D7 color. |\n| `drop_shadow` | `offset_x` 0, `offset_y` 12, `blur` 18, `color` `\"#000000\"`, `opacity` 0.6 | silhouette alpha blurred by \u03C3 = blur, sampled at p \u2212 offset, \u00D7 color \u00D7 opacity. |\n| `stroke` | `width` 4, `color` `\"#FFFFFF\"` | outline band outside the silhouette: max alpha over a 16-tap ring of radius = width, \u00D7 color. |\n\nNumeric params are animatable; they chain in array order like all\neffects (a drop_shadow listed before a glow renders beneath it).\n\n#### `chroma_key`, `luma_key` (keying)\n\nKeying makes pixels of the element's own rendered layer transparent\nbased on their color. All math operates on the STRAIGHT-alpha color\n`c = premultiplied.rgb / \u03B1` (with `c = 0` where `\u03B1 = 0`); the resulting\ncoverage factor `a` scales the pixel's alpha (and its premultiplied\ncolor with it).\n\n**`chroma_key`** \u2014 params `color` (default `\"#00FF00\"`), `tolerance`\n(default `0.18`), `softness` (default `0.1`), `spill` (default `0.5`).\nWith `k` the key color and BT.709 luma `Y(x) = 0.2126\u00B7x.r + 0.7152\u00B7x.g\n+ 0.0722\u00B7x.b`:\n\n```\nCbCr(x) = ( (x.b \u2212 Y(x)) / 1.8556 , (x.r \u2212 Y(x)) / 1.5748 )\nd = | CbCr(c) \u2212 CbCr(k) | \u2014 Euclidean distance\na = softness > 0 ? clamp((d \u2212 tolerance) / softness, 0, 1)\n : (d > tolerance ? 1 : 0)\n```\n\nSpill suppression caps the key color's dominant channel (ties resolve\ngreen \u2192 red \u2192 blue): with `i` that channel and `j, k` the others,\n`c.i \u2212= spill \u00D7 max(0, c.i \u2212 max(c.j, c.k))`, applied to every pixel\nregardless of `d`. Output: `\u03B1' = \u03B1 \u00D7 a`, color `c \u00D7 \u03B1'`.\n\n**`luma_key`** \u2014 params `threshold` (default `0.5`), `softness`\n(default `0.1`), `invert` (default `false`):\n\n```\na = softness > 0 ? clamp((Y(c) \u2212 threshold) / softness, 0, 1)\n : (Y(c) > threshold ? 1 : 0)\ninvert \u2192 a = 1 \u2212 a\n```\n\nPixels darker than `threshold` are removed (with `invert`, brighter).\n`tolerance` / `softness` / `threshold` / `spill` are animatable;\n`color` and `invert` are static. Keying reads only the element's own\npixels \u2014 to key a green-screen video, put the effect on the `video`\nelement (`color` set to the screen's actual green); a group keys its\ncomposited children as one layer.\n\n#### `levels`, `lut` (grading)\n\nBoth operate per pixel on the straight-alpha color; alpha is never\nchanged.\n\n**`levels`** \u2014 params `in_black` 0, `in_white` 1, `gamma` 1,\n`out_black` 0, `out_white` 1 (all animatable, points clamped to\n[0, 1], gamma > 0). Per channel:\n\n```\nx = clamp((c \u2212 in_black) / (in_white \u2212 in_black), 0, 1)\ny = x^(1/gamma) \u2014 gamma > 1 brightens mid-tones\nout = clamp(out_black + y \u00D7 (out_white \u2212 out_black), 0, 1)\n```\n\n**`lut`** \u2014 params `source` (URL of a `.cube` file \u2014 http(s), relative,\nor `data:` URI), `intensity` 1 (animatable, 0..1). The file MUST\ndeclare `LUT_3D_SIZE N` (2 \u2264 N \u2264 256) with the default 0..1 domain;\ndata lines are `r g b` triples ordered red-fastest, then green, then\nblue. Values clamp to [0, 1]. The graded color is the TRILINEAR\ninterpolation of the lattice at `c \u00D7 (N\u22121)` per axis, and the output\nis `mix(c, graded, intensity)`. A LUT that fails to load or parse MUST\nskip the pass with a warning (the element still renders). The lattice\nis sampled at the pipeline's working precision (8-bit per channel in\nthe reference runtime).\n\n#### `fractal_noise`, `turbulent_displace` (procedural noise)\n\nBoth build on one NORMATIVE noise function so identical documents\nproduce identical pixels on every runtime. All integer math is 32-bit\nunsigned (wrapping); lattice coordinates convert int\u2192uint by two's\ncomplement.\n\n```\npcg(v) : s = v \u00D7 747796405 + 2891336453 (mod 2\u00B3\u00B2)\n w = ((s >> ((s >> 28) + 4)) XOR s) \u00D7 277803737 (mod 2\u00B3\u00B2)\n \u2192 (w >> 22) XOR w\nh(c, seed) = pcg(c.x XOR pcg(c.y XOR pcg(c.z XOR pcg(seed)))) / (2\u00B3\u00B2\u22121)\nnoise(p, seed): value noise \u2014 trilinear blend of h at the 8 corners of\n floor(p)'s lattice cell, weights faded per axis by\n u = f\u00B3(f(6f \u2212 15) + 10)\nfbm(p, octaves, seed) = \u03A3\u2092 0.5\u00B0 \u00D7 noise(p \u00D7 2\u00B0, seed + o) / \u03A3\u2092 0.5\u00B0\n for o = 0 \u2026 octaves\u22121 (lacunarity 2, gain 0.5)\n```\n\n**`fractal_noise`** \u2014 params `scale` 100 (canvas px per lattice cell),\n`evolution` 0, `offset_x`/`offset_y` 0 (canvas px), `octaves` 4\n(integer 1\u20138, static), `seed` 0 (integer, static; use values < 2\u00B2\u2074).\nThe element's pixels are replaced by grayscale noise, keeping its\nalpha footprint:\n\n```\nv = fbm((p + offset) / scale \u2295 evolution as the 3rd axis, octaves, seed)\nout = (v, v, v) \u00D7 \u03B1, \u03B1 unchanged\n```\n\nGrayscale by design \u2014 chain `levels` to shape contrast and a `lut` to\ncolor it (gray has no chroma for `hue_rotate` to act on). Animate\n`evolution` for in-place churn, the offsets to scroll. On a text\nelement the glyphs become a noise-filled matte.\n\n**`turbulent_displace`** \u2014 params `amount` 16 (max displacement,\ncanvas px), `scale` 120, `evolution` 0, `octaves` 2 (1\u20138, static),\n`seed` 0 (static). Each output pixel samples the element's layer at a\nnoise-displaced position (bilinear, clamped to the layer):\n\n```\nd = ( fbm(p/scale \u2295 evolution, octaves, seed) \u2212 0.5 ,\n fbm(p/scale \u2295 evolution, octaves, seed + 7919) \u2212 0.5 ) \u00D7 2 \u00D7 amount\nout(p) = layer(p + d)\n```\n\n`scale`, `evolution`, offsets, and `amount` are animatable; `octaves`\nand `seed` are static.\n\n#### `glass`\n\nLiquid glass \u2014 a refractive pane over the backdrop, following the\nwidely-adopted liquidglass optical model (reference:\ngithub.com/ybouane/liquidglass). Glass applies to `shape` elements\n(primitive rectangle / ellipse and path shapes) and `text` elements,\nwhere the pane geometry is known from the element's own geometry rather\nthan rasterized alpha.\n\nGlass is **legal under 3D**: 3D transforms (`x_rotation` /\n`y_rotation` / `z`) and a camera on a glass-carrying element or its\nun-flattened ancestor chain are valid. The pane is a true plane in the\nscene and the runtime projects the \u00A74.7 optical model through the\npane's plane homography (see \"Glass under 3D\" below). With no 3D in\nplay the orthographic path applies bit-for-bit.\n\n| Param | Default | Meaning |\n|---|---|---|\n| `blur_radius` | `0` | Backdrop Gaussian blur \u03C3 in px. `0` = CLEAR glass (pure refraction, the default material); `> 0` = FROSTED. |\n| `refraction` | `21` | Lens bend strength (\u2248 px of displacement; internally the reference dial = `refraction / 30`). The magnitude is used. |\n| `edge_width` | `40` | Bevel z-radius \u2014 how deep the lens curvature reaches. With `mode: \"dome\"` and `edge_width` = the shape's radius, the pane is a half-sphere magnifier. |\n| `mode` | `\"pill\"` | Lens cross-section: `\"pill\"` (biconvex \u2014 entry + exit refraction + depth-scaled centre magnification) or `\"dome\"` (flat bottom, curved top \u2014 uniform magnification toward the centre). |\n| `edge_highlight` | `0.35` | Scales the stock light rig (rim 0.22\u00D7, inner glow 0.15\u00D7, 1.5px top-biased inner stroke 0.55\u00D7, Fresnel). `0.35` reproduces the reference defaults exactly. |\n| `dispersion` | `0.05` | Chromatic aberration along the surface normal (\u00D718 px, edge-weighted). |\n| `shadow` | `0.3` | Drop-shadow opacity, painted ONLY outside the pane's SDF (spread 10 px, vertical offset 1 px \u2014 glass never frosts its own shadow). |\n| `backdrop_saturation` | `1` | Saturation of the sampled backdrop (`1` = unchanged). |\n| `tint` | none | Color drawn over the glass; alpha = strength. Not keyframable in v1. |\n\nAll numeric params are animatable. The pane's `fill_color` is unused\nunder glass; its `opacity` scales the pane. An `ellipse` evaluates as\nthe rounded-rect SDF with `r = min(half)` \u2014 a circle when square, a\nstadium otherwise. Pane content (labels, icons) goes on lower layers (nearer the front).\n\nThe model, NORMATIVE, evaluated per pixel in pane-local rotated\ncoordinates `p` with half-size `half`, corner radius `r`, z-radius\n`zR = edge_width`, and the reference dial `dial = |refraction| / 30`:\n\n```\nsdf = roundedRectSDF(p, half, r)\ninside = \u2212sdf\nh(d) = \u221A(d \u00D7 (2\u00B7zR \u2212 d)) clamped to [0, zR] \u2014 half-circle bevel\n\u2207h = central differences of h at step 2 px (the sdf is ANALYTIC:\n no field facets, no measured-divergence cap, no rim \"lip\")\nN = normalize(\u2212\u2207h, 1)\ndepth = smoothstep(0, zR, inside)\nedge = smoothstep(0.35 \u00D7 min(half), 0, inside)\n\npill: refr = (2\u00B7\u2207h + \u2207h\u00B7(h/zR)\u00B70.5) \u00D7 (1 \u2212 1/1.5) \u00D7 dial \u00D7 30\n + (\u2212p / half) \u00D7 dial \u00D7 4 \u00D7 depth\ndome: refr = \u2212p \u00D7 dial \u00D7 depth \u00D7 0.35\n\nchroma = N.xy \u00D7 dispersion \u00D7 18 \u00D7 (edge\u00D70.7 + 0.3) \u00D7 2;\n R/G/B sample at refr + chroma / refr / refr \u2212 chroma\ncol = mix(sharp, frosted, 1 \u2212 edge \u00D7 0.15)\n \u2014 frosted everywhere, 15% sharp at the rim; with\n blur_radius 0 both textures are identical (clear glass)\ncol = saturate(col, backdrop_saturation); mix toward tint;\n \u00D7 (1 + 0.06 \u00D7 depth)\nfresnel = (1 \u2212 |N.z|)\u2074 \u00D7 (edge_highlight / 0.35)\nspec = Blinn-Phong on N: L(0.4,0.7,1)^90 + L(\u22120.3,\u22120.5,1)^50\u00D70.3\n + diffuse L(0.1,0.3,1)^6\u00D70.1 + L(0,0.9,0.4)^120\u00D70.6\n (specular dial; reference default 0)\nstroke = 1.5px inner band \u00D7 (0.4 + 0.6\u00B7topBias) \u00D7 edgeHL \u00D7 0.55\nrim/glow = edge \u00D7 edgeHL \u00D7 0.22 / smoothstep(5,0,inside) \u00D7 edgeHL \u00D7 0.15\nenv = (N.y\u00D70.5 + 0.5) \u00D7 fresnel \u00D7 0.08\nfin = col + spec + rim + glow + stroke + env, then\n mix(fin, white, fresnel \u00D7 0.2)\nout = premultiply(fin, aaMask \u00D7 opacity)\n\nshadow (sdf > 0 only, offset down by offY):\nd = max(sdfShadow \u2212 1, 0); s = spread\n\u03B1 = (e^(\u2212d\u00B2/s\u00B2) \u00D7 0.65\n + e^(\u22120.08\u00B7d / max(0.04\u00B7s, 0.01)) \u00D7 0.35) \u00D7 shadow\n```\n\n**Glass under 3D (CKP/1.0, NORMATIVE).** When the pane carries 3D\nfields or sits under an un-flattened non-affine chain (\u00A74.4), the\nmodel above runs unchanged in the pane's LOCAL frame; only the\ncoordinate hand-off changes:\n\n- Let `H` be the pane's plane homography \u2014 the 3\u00D73 restriction of the\n full \u00A74.4 matrix chain (camera included) to the pane's plane,\n origin at the pane's center, y down. Per fragment, pane-local\n `p = (H\u207B\u00B9 \u00B7 (px, 1)).xy / w`; a non-positive `w` lies past the\n plane's horizon and outputs nothing. This is exactly the camera-ray\n / pane-plane intersection, in projective form.\n- Refracted and dispersed sample points (`p + refr \u00B1 chroma`) map\n FORWARD through `H` to surface px (clamped to the surface) and\n sample the same screen-space backdrop snapshot. Glass refracts the\n COMPOSITED IMAGE at the screen plane \u2014 it does not re-render the\n scene from the refracted direction.\n- The light rig, bevel field and SDF are defined in the pane's local\n frame and tilt WITH the pane (highlights track the surface, not the\n screen).\n- Degenerate case: a singular `H` (an edge-on pane with no\n perspective anywhere in its chain) is invisible \u2014 runtimes MUST\n draw nothing. Under perspective an off-axis edge-on pane projects\n to a thin wedge; that wedge is correct rendering, not an error.\n- The shadow term evaluates in pane-local coordinates like everything\n else, so the shadow projects with the pane.\n\nWith no 3D in play the orthographic path applies unchanged \u2014\ndocuments valid in CKP/1.0 render bit-identically.\n\n### 4.8. Lighting and materials (CKP/1.0, NORMATIVE)\n\nA Source MAY declare `lights` and an `environment`; an element MAY carry\na `material`. Lighting is **opt-in and additive**: a document with no\n`lights` and no element `material` renders **bit-identically** to one\nwithout these fields. Only elements with a `material` are shaded.\n\n**Shading model (PBR).** When an element has a `material` and the Source\nhas `lights`, the element renders its content normally \u2014 those pixels are\nthe **albedo** \u2014 and the runtime shades each fragment with one model,\nidentical in 2D and 3D:\n\n```\nout = albedo \u00B7 (ambient + \u03A3_dir diffuse(N,L)\u00B7Lc) // Lambert\n + \u03A3_dir specular_GGX(N,L,V,roughness)\u00B7F(V,H,F0)\u00B7Lc // Cook-Torrance\n + (kd\u00B7albedo\u00B7envAvg + envc\u00B7Fr) \u00B7 reflectivity // environment (IBL)\n + emissive\u00B7albedo\n```\n\nwhere `envc = mix(env(R), envAvg, roughness)` is the environment sampled\nalong the reflection ray `R = reflect(\u2212V,N)` and blurred toward its\naverage by roughness; `Fr` is the roughness-aware Schlick\u2013Fresnel at the\nview angle; `kd = (1\u2212Fr)\u00B7(1\u2212metalness)` is the dielectric diffuse weight;\nand `envAvg` is the environment's mean color (its diffuse irradiance).\n\n- **N** is the element's world-space face normal (from its \u00A74.4 3D\n orientation), perturbed per-fragment by `material.normal_map` when\n present (a tangent-space map sampled in the element's UV space, scaled\n by `normal_scale`; the tangent/bitangent come from the quad's U/V axes).\n- **V** is the view vector: per-fragment `normalize(eye \u2212 fragmentWorld)`\n under a camera, or `(0,0,1)` with no camera. This is the only term the\n camera affects \u2014 so specular and reflections **sweep as the camera\n moves** (3D); with no camera you animate the lights instead (2D). The\n math is the same.\n- **F0** (Fresnel base reflectance) is `0.04` for dielectric, the albedo\n for metal, interpolated by `metalness`. Fresnel uses Schlick.\n- **roughness** widens the GGX specular lobe and blurs the environment\n reflection; **reflectivity** is an art dial over the environment term.\n\n**Lights** (`source.lights`):\n- `{ type: 'ambient', color?, intensity? }` \u2014 uniform fill.\n- `{ type: 'directional', azimuth?, elevation?, color?, intensity? }` \u2014\n a parallel light; `azimuth`/`elevation` (degrees) give its direction.\n All scalar fields animatable.\n\n**Environment** (`source.environment`) is what reflective surfaces\nsample along the reflection vector. Phase 1: `{ type: 'gradient', stops }`\n\u2014 a gradient \"sky\" indexed by the reflection ray's vertical component\n(offset 0 = looking down, 1 = up), so a surface mirrors the sky when it\ntilts up and the ground when it tilts down, and the reflection shifts as\nthe surface (or camera) moves. Up to 4 stops. The environment also\ncontributes a diffuse irradiance (its average color) to dielectric\nsurfaces. The other type is `{ type: 'image', src }` \u2014 an\n**equirectangular** (2:1 lat-long) image the surface mirrors along the\nreflection vector (real photographic reflections); roughness blurs it\ntoward the image's average color. Both share one IBL path.\n\n**Bloom** (`source.bloom`, Phase 2) is a whole-frame post-process:\npixels brighter than `threshold` (luma, soft `knee`) are blurred by\n`radius` and added back \u00D7 `intensity`, so bright regions \u2014 specular\nhighlights, emissive surfaces, bright media \u2014 bleed light across element\nboundaries. It is **brightness-driven**: the amount each region blooms\ncomes from its own brightness, not a per-element knob (use the\nper-element `glow` effect for deliberate, art-directed halos). Opt-in;\nabsent \u21D2 no bloom (byte-identical). All fields animatable.\n\n**Material** (`element.material`): `roughness` (0 mirror\u2011tight .. 1\nmatte), `metalness` (0 dielectric .. 1 metal), `reflectivity` (env\nstrength), `emissive` (self-illumination toward the unlit pixels),\n`normal_map` (tangent-space normal map URL \u2014 flat texel = `#8080ff`) and\n`normal_scale` (perturbation strength). Scalars animatable. Absent \u21D2\nunlit.\n\n**Determinism / cost.** No `material` \u21D2 the element takes the exact\nunlit path (no shading pass), so unlit content is byte-identical and\npays nothing. The model is local illumination (no shadows, no global\nillumination) \u2014 flat 2.5D surfaces lit per-fragment.\n\n---\n\n## 5. Element types\n\nCKP/1.0 defines eight element types \u2014 `video`, `image`, `text`,\n`shape`, `audio`, `group`, `caption`, and `particles`. (The former\n`svg` element was absorbed into `shape`, which carries vector `paths`.)\nEach section below specifies the fields, semantics, and required\nbehavior for one type.\n\n### 5.1. `shape`\n\nA `shape` draws geometry in one of two representations, selected by whether\nit carries `paths`:\n\n- **Primitive** \u2014 `shape: \"rectangle\" | \"ellipse\"` with optional rounded\n corners, gradient fills, and stroke. Rendered as a resolution-independent\n SDF. `fill_color`, `stroke_color`, `stroke_width`, and `border_radius` are\n animatable via `keyframe_animations` (e.g. `property: \"border_radius\"`).\n- **Path** \u2014 arbitrary vector geometry via `paths`: keyframeable `d`\n morphing, per-sub-path fill/stroke, and stroke trim/draw-on. Rasterized,\n so resolution is bound by `view_box`. Specified in \u00A75.6.\n\nWhen `paths` is present the primitive fields are ignored.\n\n```ts\ninterface ShapeElement extends BaseElement {\n type: \"shape\";\n // Primitive form (ignored when `paths` is present):\n shape?: \"rectangle\" | \"ellipse\"; // default \"rectangle\"\n fill_color?: string; // hex, default \"#ffffff\"\n gradient?: LinearGradient | RadialGradient; // overrides fill_color\n stroke_color?: string;\n stroke_width?: number;\n border_radius?: number; // PIXELS \u2014 see \u00A75.1.2\n shadow?: BoxShadow; // drop shadow cast by the shape\n // Path form (\u00A75.6):\n view_box?: [number, number, number, number]; // default [0, 0, 100, 100]\n gradients?: PathGradient[];\n paths?: PathDef[]; // \u22651 when present\n}\n\ninterface BoxShadow {\n color: string; // \u00A73.4\n offset_x?: number; // px. Default 0\n offset_y?: number; // px. Default 12\n blur?: number; // Gaussian \u03C3 px (0 = crisp). Default 18\n}\n```\n\n`shadow` casts a soft drop shadow beneath the shape. Under 3D the shadow\nforeshortens with the element's plane, while `offset_x`/`offset_y`\ntranslate in the parent plane (\u00A74.4.3).\n\n#### 5.1.1. Gradients\n\n```ts\ninterface LinearGradient {\n type: \"linear\";\n angle?: number; // degrees, CSS linear-gradient() convention:\n // 0 = to top, clockwise. 90 = to right,\n // 180 = to bottom (default), 270 = to left.\n stops: GradientStop[]; // 2..4 stops\n}\ninterface RadialGradient {\n type: \"radial\";\n cx?: number; // 0..1 of bounding box, default 0.5\n cy?: number;\n radius?: number; // 0..1, default 0.5\n stops: GradientStop[];\n}\ninterface GradientStop {\n offset: number; // 0..1\n color: string; // hex\n}\n```\n\nImplementations MUST support at least 4 stops. The gradient direction\nfor linear gradients uses CSS-style angle conventions.\n\n#### 5.1.2. Corner radius\n\n`border_radius` is in PIXELS, not a normalized 0..1 value. Runtimes MUST\nclamp the value to half the shorter of `width`/`height` so that values\nexceeding the shape produce a pill or circle rather than visual artifacts.\n\nConforming runtimes MUST render corner arcs as **true quarter-circles**,\nnot stretched ellipses. This means SDF-based renderers MUST perform\ncorner math in pixel space, not in normalized UV space.\n\n### 5.2. `text`\n\nMulti-line text. Text soft-wraps within the box (`text_wrap`), honors\nexplicit `\\n` breaks, `line_height`, and per-line backgrounds. The\ndefault font is **Inter** (not a platform stack); `font_family` selects\nanother registered family (\u00A72.1 `fonts`).\n\n```ts\ninterface TextElement extends BaseElement {\n type: \"text\";\n text?: string; // static text\n spans?: TextSpan[]; // inline-styled runs (\u00A75.2.4)\n\n font_family?: string; // registered family name; default Inter\n font_size?: number | string; // \"auto\" fits to width\n font_weight?: number | string; // \"400\", \"bold\", etc.\n font_style?: \"normal\" | \"italic\";\n fill_color?: string;\n stroke_color?: string;\n stroke_width?: number;\n text_align?: \"left\" | \"center\" | \"right\";\n letter_spacing?: number;\n\n background_color?: string; // solid bg, SHRINK-WRAPPED to glyphs\n background_border_radius?: number; // corner radius (px) for the bg\n background_padding?: number | [number, number]; // bg padding px (or [x,y])\n text_shadow?: TextShadow | TextShadow[]; // per-glyph shadow(s)\n\n mask?: TextMask; // reveal mask\n}\n\ninterface TextShadow {\n color: string; // \u00A73.4\n offset_x?: number; // px, text-local frame. Default 0\n offset_y?: number;\n blur?: number; // Gaussian softness px (0 = crisp). Default 0\n opacity?: number; // 0..1, multiplies color alpha. Default 1\n}\n```\n\n`text_shadow` is a **per-glyph** drop shadow (CSS `text-shadow`): each glyph\ncasts its own, so it tracks per-letter animation and overlapping glyphs \u2014\nunlike the silhouette `drop_shadow` effect (\u00A74.7), which shadows the\nflattened text as one shape. Pass an **array** for stacked shadows, painted\nback-to-front (list farthest \u2192 nearest for a clean 3D extrusion). Shadows\nare drawn behind every glyph (a two-pass render) so they never get clipped\nby neighbouring letters. Works the same on `caption`. (Reach for the\n`drop_shadow` effect instead when you want one soft shadow of the whole\ntext silhouette.)\n\n`background_color` draws a solid background behind the text as **one band\nper line**, each shrink-wrapped to that line's glyphs (line width \u00D7 the\nfont ascent/descent box) \u2014 NOT the element's `width`/`height` box \u2014 so\ncentered or ragged multi-line text gets per-line pills, and it tracks\nwrapping and `font_size: \"auto\"`. `background_border_radius` rounds each\nband; `background_padding` (a number, or `[x, y]`) insets it outward. It\nrotates, scales, and skews with the element. For a per-run highlight band\nuse a span `background` (\u00A75.2.4); for a drop shadow use a `drop_shadow`\neffect (\u00A74.7). The same fields work on `caption` (one band around the\ncaption phrase).\n\n#### 5.2.1. `text` content\n\n`text` (or `spans`, \u00A75.2.4) provides the content. If `spans` is present it\ntakes precedence over `text`. If neither is present the element renders\nnothing (and runtimes MAY skip the element entirely).\n\n#### 5.2.2. `font_size: \"auto\"`\n\nWhen `font_size` is the string `\"auto\"`, the runtime MUST compute a\nfont size such that the rendered text fits inside the element's `width`.\nThe exact algorithm is implementation-defined but MUST be deterministic\nfor the same text + font + width inputs.\n\n#### 5.2.3. Text mask\n\n```ts\ninterface TextMask {\n type: \"linear-wipe\";\n angle?: number; // degrees, default -45\n progress?: number | Keyframe[]; // 0..1\n softness?: number; // 0..1, default 0.3\n}\n```\n\nWhen present, the text is rendered into an offscreen surface and\nmultiplied by a linear-gradient alpha mask. `progress` controls the\nreveal position; `softness` controls the wipe edge width.\n\n#### 5.2.4. Spans\n\n`spans` carries inline-styled runs. When present it takes precedence\nover `text`. Runs lay out left-to-right; a span whose\n`text` is exactly `\"\\n\"` is a hard line break. Each span inherits the\nelement's `font_family` / `font_size` / `font_weight` / `fill_color` /\n`letter_spacing` unless it overrides them.\n\n```ts\ninterface TextSpan {\n text: string;\n font_family?: string;\n font_size?: number | string;\n font_weight?: number | string;\n font_style?: \"normal\" | \"italic\";\n fill_color?: string;\n letter_spacing?: number; // px tracking; inherits element's\n background_color?: string; // flat full-line-box band\n background?: TextSpanBackground; // stylized band; overrides above\n nowrap?: boolean; // atomic for word-wrap\n}\n```\n\n`letter_spacing` (element and span level) is pixels of tracking added\nafter EVERY character, including the last \u2014 Chrome's model, so boxes\nmeasured in a browser reproduce exactly. `nowrap` marks the span atomic\nfor word-wrap (CSS `white-space: nowrap` semantics): the runtime never\nbreaks inside it. `background` draws a band behind the span's glyphs\n(`color`, plus optional `height_ratio`, `inset_y_ratio`, `padding_x`,\n`skew_x`, `border_radius` \u2014 see the schema for exact semantics).\n\n### 5.3. `image` and `video`\n\n```ts\ninterface ImageElement extends BaseElement {\n type: \"image\";\n source: string; // URL or path\n fit?: \"cover\" | \"contain\" | \"fill\" | \"none\"; // default \"cover\"\n border_radius?: number; // corner radius px, default 0\n crop_x?: number; // source crop, normalized 0..1 (see \u00A75.3.1)\n crop_y?: number;\n crop_width?: number;\n crop_height?: number;\n}\n\ninterface VideoElement extends BaseElement {\n type: \"video\";\n source: string;\n fit?: \"cover\" | \"contain\" | \"fill\" | \"none\"; // default \"cover\"\n crop_x?: number; // source crop, normalized 0..1 (see \u00A75.3.1)\n crop_y?: number;\n crop_width?: number;\n crop_height?: number;\n volume?: number | Keyframe[]; // 0..100, default 100 (animatable)\n playback_rate?: number | Keyframe[]; // media seconds per timeline second, default 1\n // (schema type; the runtime requires it static \u2014 \u00A75.3.2)\n trim_start?: number; // seconds into the media, default 0\n trim_duration?: number; // playable media window after trim_start\n loop?: boolean;\n}\n```\n\nAsset reference rules are defined in \u00A78.\n\n#### 5.3.1. Object fit and source crop\n\n`fit` follows CSS `object-fit` against the element box:\n\n| Value | Behavior |\n|---|---|\n| `cover` (default) | scale media to fill the box; crop the overflow, centered |\n| `contain` | scale media to fit inside the box; letterbox |\n| `fill` | stretch media to the box exactly |\n| `none` | natural media size, centered, cropped to the box |\n\n**Source crop.** `crop_x` / `crop_y` / `crop_width` / `crop_height` select a normalized\nsub-rectangle of the **source** media (each in `0..1`, origin top-left)\nthat is shown in place of the whole source. The default is the whole\nsource \u2014 `crop_x = 0, crop_y = 0, crop_width = 1, crop_height = 1` \u2014 and\nomitting the fields is identical to that identity crop.\n\nCrop applies BEFORE `fit`: the cropped sub-rectangle becomes the\neffective media, and `fit` then maps it into the element box exactly as\nin \u00A75.3.1. The element box (its `x` / `y` / `width` / `height`) is\n**unchanged** \u2014 crop only chooses which part of the source fills it, so\ncrop composes orthogonally with transform, `border_radius`, filters, and\n3D. A runtime MUST clamp the rect to the unit square (`crop_width` to\n`1 \u2212 crop_x`, `crop_height` to `1 \u2212 crop_y`); a zero-area crop is treated\nas the identity.\n\nEach component MAY be keyframed (\u00A76.3). Animating the crop origin pans\nacross the source and animating its size zooms \u2014 a Ken Burns move with no\nchange to the element's layout.\n\n#### 5.3.2. Media time mapping\n\nThe playable *trim window* is:\n\n```\nwindow_start = max(0, trim_start)\nwindow_length = min(trim_duration ?? \u221E, media_duration \u2212 window_start)\n```\n\nThe media time sampled at composition time `t` is:\n\n```\nconsumed = max(0, t \u2212 element.time) \u00D7 playback_rate\nmedia_t = window_start + (consumed mod window_length) if loop\n = window_start + min(consumed, window_length \u2212 \u03B5) otherwise\n```\n\ni.e. `loop` wraps WITHIN the trim window; without `loop` the last frame\nholds. `playback_rate` MUST be a static number (keyframed rates are not\ndefined in CKP/1.0).\n\n**Time remapping.** A video MAY carry `time_remap` \u2014 a keyframe array\n(\u00A76.3 semantics: destination-keyframe easing, element-local time) whose\nVALUES are media times in seconds. When present, it REPLACES the\nmapping above entirely (`trim_start`, `trim_duration`,\n`playback_rate`, and `loop` are ignored):\n\n```\nmedia_t = clamp(interpolate(time_remap, t \u2212 element.time), 0, media_duration \u2212 \u03B5)\n```\n\nSpeed ramps are steep segments, freeze frames are flat ones, and\nreverse playback follows decreasing values; easings shape the ramp.\nDecoders quantize `media_t` to the media's own frames, which is\nconformant (\u00A72.1 motion-blur note applies the same way).\n\n**Varispeed audio.** Audio under a warped clock \u2014 a video's embedded\ntrack under `time_remap`, or any sound element inside a time-remapped\ngroup (\u00A75.8.3) \u2014 plays VARISPEED, tape-style: at each instant the\naudio advances through the media at `rate(t) = d(media_t)/dt`, and\npitch shifts with the rate (2\u00D7 plays an octave up, slow-motion plays\nlow). Flat segments (rate 0) are silent; decreasing segments play the\nmedia REVERSED at `|rate|`. The reference implementation samples the\neffective media-time function at 10 ms through the full warp chain,\nsplits it into monotonic runs, and schedules each run with a rate\ncurve (reversed runs play a sample-reversed copy of the buffer).\nPitch-preserving time-stretch is NOT defined in CKP/1.0. Fade\nenvelopes (`audio_fade_in`/`audio_fade_out`) are not applied under\nwarps in v1.\n\n#### 5.3.3. Video audio\n\nIf the media container carries an audio track, conforming Level 3\nruntimes MUST play/mix its FIRST audio track using the same timing as\n\u00A75.3.2, with gain `volume / 100`. `playback_rate` resamples the audio\n(pitch shifts accordingly; time-stretch is not defined in CKP/1.0).\nVideos without an audio track are silent \u2014 not an error.\n\n#### 5.3.4. Audio fades\n\n`audio_fade_in` / `audio_fade_out` (on both `audio` and `video`\nelements, seconds, default 0) shape the gain over the element's\nTIMELINE window `[0, L]`:\n\n```\ng(\u03C4) = volume/100 \u00D7 min(1, \u03C4 / fade_in) \u00D7 min(1, (L \u2212 \u03C4) / fade_out)\n```\n\n(each factor is 1 when its fade is 0). The envelope is piecewise linear\nbetween its corner points; runtimes MUST reproduce it within normal\ngain-ramp accuracy, including when playback starts mid-fade (seek).\n\n### 5.4. `audio`\n\n```ts\ninterface AudioElement extends BaseElement {\n type: \"audio\";\n source: string;\n volume?: number | Keyframe[]; // 0..100\n trim_start?: number;\n trim_duration?: number;\n loop?: boolean;\n}\n```\n\nAudio elements have no visual representation. They contribute to the\nmixed audio track produced by export-conformant runtimes (\u00A710.3).\nPreview-only runtimes MAY play them via `HTMLAudioElement` or similar.\n\n### 5.5. `caption`\n\nWord-timed captions with optional kinetic styling.\n\n```ts\ninterface CaptionElement extends BaseElement {\n type: \"caption\";\n words: { text: string; start: number; end: number }[]; // start/end relative to element.time\n style?: \"tiktok_bounce\" | \"fade_reveal\" | \"kinetic_typewriter\" | \"word_pop\";\n // Windowing \u2014 how much of the transcript shows at once (\u00A75.5.1). A whole\n // transcript otherwise renders as one block; with `max_length` set, the words\n // are split into chunks and only the chunk active at the current time shows.\n max_length?: number | \"auto\"; // number = max LETTERS per chunk;\n // \"auto\" = a few words per chunk;\n // absent = show all at once.\n\n // Text-like styling\n font_family?: string;\n font_size?: number | string; // \"auto\" fits joined words to width\n font_weight?: number | string;\n fill_color?: string;\n highlight_color?: string;\n highlight_background_color?: string;\n text_align?: \"left\" | \"center\" | \"right\";\n}\n```\n\n`words[*].start` and `words[*].end` are seconds RELATIVE to the\nelement's `time`. The exact kinetic behavior for each `style` is\ndefined by the reference implementation; deviations MAY occur in third-\nparty renderers but the timing MUST match.\n\n#### 5.5.1. Windowing (`max_length`)\n\nA full transcript on one element would render as a single unreadable block.\n`max_length` splits `words` into CHUNKS; at any time only the chunk active then\nis shown (within the element's box, wrapped). Chunking:\n\n- **number** \u2014 grow a chunk word-by-word until adding the next word would exceed\n this many LETTERS (characters), then start a new chunk.\n- **`\"auto\"`** \u2014 chunk by a few words (and break on pauses) \u2014 the speech default.\n- **absent** \u2014 no windowing; the whole transcript shows at once.\n\nThe active chunk at element-local time `t` is the last chunk whose first word has\nstarted (so a chunk lingers through silent gaps until the next begins). Word\nkinetics (`style`) apply WITHIN the active chunk. Word `start`/`end` are\nunchanged \u2014 `max_length` is a display rule, not a re-timing.\n\n### 5.6. `shape` paths (vector geometry)\n\nA `shape` (\u00A75.1) that carries `paths` renders as arbitrary vector geometry \u2014\na restricted SVG-path subset (this absorbs the former standalone `svg`\nelement). Conforming runtimes MUST support viewBox-scaled paths with linear\ngradients, clip-to-path, stroke-dashoffset progress, and per-path opacity.\n\nThe path-form fields live on `ShapeElement` (\u00A75.1): `view_box`, `gradients`,\nand `paths`. Their element types:\n\n```ts\ninterface PathGradient {\n id: string;\n type: \"linear\";\n x1: number; y1: number; x2: number; y2: number; // viewBox coords\n stops: GradientStop[];\n}\n\ninterface PathDef {\n d: string | Keyframe[]; // SVG path data, or d-string keyframes (\u00A75.6.2)\n fill?: string; // hex or \"url(#gradient-id)\"\n stroke?: string;\n stroke_width?: number;\n stroke_progress?: number | Keyframe[]; // 0..1\n trim_start?: number | Keyframe[]; // \u00A75.6.1\n trim_end?: number | Keyframe[];\n trim_offset?: number | Keyframe[];\n clip_path?: string; // another path that clips this one\n stroke_linecap?: \"butt\" | \"round\" | \"square\";\n stroke_linejoin?: \"miter\" | \"round\" | \"bevel\";\n opacity?: number; // 0..1\n}\n```\n\n`stroke_progress` MUST drive the standard `stroke-dasharray` /\n`stroke-dashoffset` reveal \u2014 a `progress` of `0` shows no stroke; `1`\nshows the entire stroke. Implementations MUST measure path length\ndeterministically (the reference implementation uses\n`SVGPathElement.getTotalLength()`).\n\n#### 5.6.1. Trim paths\n\nEach path MAY carry a trim window: only the stroke between\n`trim_start` and `trim_end` \u2014 fractions of the path's TOTAL LENGTH,\n0..1 \u2014 is drawn. `trim_offset` rotates the window around the path,\nWRAPPING at the ends (an offset of 1 is a full lap), so an animated\noffset is the classic traveling-dash \"snake\" and an animated\n`trim_end` is the draw-on reveal. All three are animatable;\n`stroke_progress` remains as sugar for `[0, progress]` and is ignored\nwhen any trim field is present. Fill is unaffected \u2014 trimming applies\nto the STROKE only.\n\nReference evaluation: with window width `w = clamp(trim_end, 0, 1) \u2212\nclamp(trim_start, 0, 1)` (nothing draws when w \u2264 0; the full stroke\nwhen w \u2265 1) and wrapped anchor `a = (trim_start + trim_offset) mod 1`,\nthe stroke uses a dash pattern of `[w\u00B7L, L \u2212 w\u00B7L]` with dash offset\n`\u2212a\u00B7L`, where `L` is the path's total length \u2014 the pattern's period\nequals `L`, so windows crossing the path's start wrap exactly.\n\n#### 5.6.2. Path morphing\n\nA path's `d` MAY be a keyframe array whose values are d-strings.\nBetween two keyframes the path MORPHS when the pair is COMPATIBLE \u2014\nidentical command-letter sequences, equal numeric-argument counts, and\nno arc commands (`A`/`a`, whose boolean flags cannot interpolate):\nevery numeric argument interpolates with the destination keyframe's\neasing. An INCOMPATIBLE pair SNAPS: the source value holds until the\ndestination keyframe's time. No path normalization is performed \u2014 the\nprotocol stays literal; authors export morph targets with matching\ncommand structure (the standard practice).\n\n### 5.7. `particles`\n\nA deterministic particle system with two modes: ballistic emission\nand target-point convergence.\n\n```ts\ninterface ParticlesElement extends BaseElement {\n type: \"particles\";\n // Common\n size?: number; // pixels, default 12\n size_variation?: number; // 0..1, default 0.4\n particle_shape?: \"square\" | \"circle\";\n color?: string | string[]; // array randomizes per particle\n rotation_speed?: number; // deg/s\n lifetime?: number; // seconds per particle, default 1.5\n fade_at?: number; // 0..1 fraction of lifetime where fade begins, default 0.7\n\n // Ballistic emission\n rate?: number; // particles per second\n velocity?: number; // initial speed px/s\n spread?: number; // cone in degrees, default 360\n direction?: number; // 0=right, 90=down, -90=up\n gravity?: number; // px/s\u00B2, positive=down\n\n // Depth (CKP/1.0, \u00A75.7.3)\n z_velocity?: number; // px/s along the plane normal, default 0\n z_spread?: number; // uniform vz range width px/s, default 0\n\n // Burst (used by both modes)\n burst?: boolean;\n burst_count?: number;\n\n // Convergence (set target_points to enter convergence mode)\n target_points?: [number, number][]; // canvas-space targets\n convergence_easing?: EasingFunction;\n scatter_radius?: number; // disk radius around emitter\n}\n```\n\n#### 5.7.1. Determinism\n\nEvery particle's position, rotation, size, and color MUST be a pure\nfunction of `(element.id, particle_index, age)`. This means a runtime\nthat seeks to time T MUST produce the same composition as a runtime\nthat played continuously to T. The reference implementation seeds a\nPRNG with a hash of `element.id` and the particle index; third-party\nruntimes MAY use any algorithm that produces the same output as the\nreference.\n\n#### 5.7.2. Convergence mode\n\nWhen `target_points` is present and non-empty:\n\n- Each particle `n` is assigned the target `target_points[n % length]`.\n- Each particle's start position is randomly placed within a disk of\n radius `scatter_radius` (default = `max(canvas_width, canvas_height)`)\n centered on `(x, y)`.\n- The particle's position is `lerp(start, target, easing(age/lifetime))`\n using `convergence_easing` (default `\"ease-out-quart\"`).\n\n#### 5.7.3. Depth (CKP/1.0)\n\nPer particle, `vz = z_velocity + (r \u2212 0.5) \u00D7 z_spread` with `r` the\nparticle's uniform random draw, and its depth offset is `vz \u00D7 age` px\nalong the emitter plane's normal (+z toward the viewer, \u00A74.4). The\noffset applies in BOTH modes (it is orthogonal to the in-plane\nposition) and is part of the \u00A75.7.1 determinism contract. There is no\nz gravity \u2014 `gravity` stays in-plane y. Like the `z` field (\u00A74.4.2),\ndepth has no visual effect without perspective in the chain, and\nparticles draw in spawn order, never depth-sorted among themselves \u2014\nthe \u00A74.4.3 camera sort orders whole elements, not a particle system's\ninternal quads. With both fields absent or 0 the simulation is\nexactly the 2D one.\n\n### 5.8. `group`\n\n```ts\ninterface GroupElement extends BaseElement {\n type: \"group\";\n elements: Element[];\n clip?: boolean; // default false\n mask?: {\n mode: \"alpha\" | \"alpha-inverted\" | \"luma\" | \"luma-inverted\";\n elements: Element[];\n };\n}\n```\n\nA positioned container. Children's `x`/`y` are coordinates in the\ngroup's LOCAL space, origin at the group's top-left box corner; a\nchild's `time` is relative to the group's start; child `layer` and `z`\norder locally (\u00A74.2). The group's transform (\u00A74.4) and\nopacity stack multiplicatively onto children. Percentage/viewport units\ninside a group still resolve against the COMPOSITION canvas.\n\n#### 5.8.1. Clipping\n\nWith `clip: true` (requires explicit `width` and `height`), children\nrender into an offscreen layer the size of the group's box; pixels\noutside the box are discarded (CSS `overflow: hidden`). The group's\nown transform and opacity apply to the composited layer as a whole \u2014\nopacity therefore applies ONCE to the flattened layer (overlapping\nsemi-transparent children do not double-blend).\n\n`border_radius` (px) rounds the clip box: children are masked to a\nrounded rectangle, matching a rounded card that clips its content (CSS\n`overflow: hidden` + `border-radius`). It is clamped to half the\nsmaller box dimension and is ignored on an unclipped group. (Rounded\nclipping currently applies to the plain `clip` path; a `mask` group\nignores `border_radius` since the mask layer already defines coverage.)\n\n#### 5.8.2. Masks\n\nThe mask belongs to the group it masks \u2014 declared on the masked\nelement, never inferred from siblings or layer adjacency. Mask\n`elements` render into a second box-sized layer using the same local\ncoordinate space and timing rules as children, and may animate.\nThe content layer composites through the mask layer per pixel:\n\n```\nfactor = mask.alpha (alpha)\n = 1 \u2212 mask.alpha (alpha-inverted)\n = luminance(mask.rgb) (luma; Rec. 709 weights 0.2126/0.7152/0.0722,\n computed on premultiplied values)\n = 1 \u2212 luminance(mask.rgb) (luma-inverted)\noutput = content \u00D7 factor\n```\n\n`mask` requires explicit `width`/`height` and implies clipping (both\nlayers are box-sized).\n\n#### 5.8.3. Group time remapping\n\nA group MAY carry `time_remap` \u2014 a keyframe array (\u00A76.3 semantics)\nwhose VALUES are warped local times in seconds. The group's SUBTREE\nruns on the warped clock:\n\n```\nlocal = t \u2212 group_start\nwarped = max(0, interpolate(time_remap, local))\n```\n\nChildren evaluate exactly as if the group's local time were `warped`:\ntheir `time` windows, animations, keyframes, transitions, and nested\nmedia all read the warped clock. Nested remapped groups compose (each\nwarps its parent's clock in turn). The group's OWN animated properties\n(opacity, rotation, scale, position) read REAL time \u2014 the container\nmoves on the composition's clock; only its contents are warped.\n\nFlat segments freeze the subtree, steep segments speed-ramp it, and\ndecreasing values run it backwards. Nested video decodes the frame at\nits warped media time (through \u00A75.3.2's mapping). Audio inside a\nremapped subtree follows the varispeed rule (\u00A75.3.2).\n\n### 5.9. No nested-composition element\n\nCKP deliberately has NO `composition` (pre-comp) element. Both things a\npre-comp bundles are covered by orthogonal features on plain elements:\n\n- **Reuse** is an authoring-time concern: template functions expand into\n plain elements before the Source is serialized (see `@clipkit/patterns`\n for the first-party library). The wire format stays fully decomposed \u2014\n a runtime never resolves references or instantiates templates.\n- **Nested timing** is `time_remap` on a plain `group` (\u00A75.8.3).\n\nA Source containing `type: \"composition\"` is invalid under CKP/1.0.\n\n---\n\n## 6. Animation\n\nEvery animatable property may be driven in three ways: a static value,\na named-preset animation, or a keyframe animation. These compose with\nthe precedence:\n\n```\nkeyframe_animation > named_animation > static_value\n```\n\nThat is, if both a keyframe animation and a named animation target the\nsame property at the same time, the keyframe wins.\n\n### 6.1. Static values\n\nThe value as written in JSON. No interpolation; the value is used as-is\nfor the entire duration the element is active.\n\n### 6.2. Named animations\n\n```ts\ninterface Animation {\n type: AnimationType;\n duration?: number; // seconds; defaults in \u00A76.2.1\n easing?: Easing; // default \"ease-out\" unless noted\n time?: \"start\" | \"end\" | number; // start, relative to element.time\n\n // Parameters read by specific types (ignored otherwise):\n frequency?: number; // Hz \u2014 shake (8), wiggle (2), text-wave (1.5)\n rotation?: number; // degrees \u2014 spin (360), wiggle amplitude (8),\n // text-flip start angle (90)\n distance?: number; // px \u2014 pan/shift (200), shake (24),\n // text-slide (40), text-fly (140), text-wave (12)\n direction?: \"left\" | \"right\" | \"up\" | \"down\"; // pan/shift/text-slide/text-fly\n scale?: number; // squash depth 0..1 (0.3)\n split?: \"letter\" | \"word\"; // text-* unit granularity (\u00A76.5)\n stagger?: number; // text-* seconds between units (\u00A76.5)\n axis?: \"x\" | \"y\" | \"z\"; // text-flip rotation axis (\u00A76.5, CKP/1.0)\n}\n```\n\n`time: \"start\"` (or absent) resolves to local time `0`; `\"end\"` to\n`element_duration \u2212 duration`; a number is local seconds.\n\nNormative tween recipes (deltas apply to the listed property; *relative*\nadds to the static value, *absolute* replaces it during the window):\n\n| Type | Property | From \u2192 To | Mode | Notes |\n|---|---|---|---|---|\n| `fade-in` / `fade-out` | opacity | 0\u21921 / 1\u21920 | absolute | |\n| `slide-left-in` | x | \u2212200\u21920 | relative | starts left, moves right into place |\n| `slide-right-in` | x | +200\u21920 | relative | |\n| `slide-up-in` | y | +200\u21920 | relative | starts below, rises |\n| `slide-down-in` | y | \u2212200\u21920 | relative | |\n| `slide-*-out` | x/y | 0\u2192\u00B1200 | relative | motion direction matches the name |\n| `scale-in` / `scale-out` | scale | 0\u21921 / 1\u21920 | absolute | |\n| `rotate-in` / `rotate-out` | rotation | \u221290\u21920 / 0\u2192+90 | relative | |\n| `bounce-in` / `bounce-out` | scale | 0\u21921 / 1\u21920 | absolute | default easing `ease-out-back` / `ease-in-back` |\n| `spin` | rotation | 0\u2192`rotation` | relative | default easing `linear` |\n| `shake` | x | oscillation, amplitude `distance`\u21920 | relative | `sin(2\u03C0\u00B7frequency\u00B7t)` \u00D7 eased envelope |\n| `wiggle` | rotation | oscillation, constant amplitude `rotation` | relative | same formula |\n| `squash` | y_scale, x_scale | 1\u21921\u2212`scale`\u21921; 1\u21921+0.6\u00B7`scale`\u21921 | absolute | two half-duration phases; in `ease-in-quad`, out `ease-out-back` |\n| `pan` | x or y | \u2212`distance`/2\u2192+`distance`/2 along `direction` | relative | drifts through rest position; default easing `linear` |\n| `shift` | x or y | 0\u2192`distance` along `direction` | relative | **fill-forward**: the end value holds for the rest of the element's life |\n| `drift` | x, y | smooth random walk, amplitude `distance` (30) | relative | offsets = `(noise1d(frequency\u00B7t, seed) \u2212 0.5) \u00D7 2 \u00D7 distance` per axis (y uses `seed + 7919`); `frequency` default 0.5, `seed` default 0 |\n| `breathe` | scale | oscillation, amplitude `scale` (0.05) | relative | `scale \u00D7 sin(2\u03C0\u00B7frequency\u00B7t)`, `frequency` default 0.4 |\n| `orbit` | x, y | circle of radius `distance` (40) | relative | `x += r\u00B7sin(2\u03C0ft + \u03C0/2)`, `y += \u00B1r\u00B7sin(2\u03C0ft)` (`direction: \"left\"` flips y = counter-clockwise); `frequency` default 0.5 rev/s |\n| `text-*` | per-unit | \u2014 | \u2014 | \u00A76.5 |\n\n`drift`'s noise is NORMATIVE \u2014 the 1D form of \u00A74.7's lattice noise:\n`noise1d(x, seed)` is the quintic-faded linear interpolation between\n`h(\u230Ax\u230B)` and `h(\u230Ax\u230B+1)` where `h(i) = pcg(i XOR pcg(seed)) / (2\u00B3\u00B2\u22121)`\nand `pcg` is \u00A74.7's hash. Same seed \u2192 identical motion everywhere.\n\n#### 6.2.1. Duration defaults\n\n`duration` defaults to `0.5`, EXCEPT `spin`, `shake`, `wiggle`, `pan`,\n`drift`, `breathe`, `orbit` and `text-wave`, which default to the\nelement's full duration when both `time` and `duration` are omitted\n(they read as \"for the element's life\"). Outside its window a tween stops contributing (the property\nreturns to its static value), except `shift`'s documented fill-forward.\n\n### 6.3. Keyframe animations\n\n```ts\ninterface KeyframeAnimation {\n property: string; // name of the BaseElement field\n loop?: boolean | \"ping-pong\"; // repeat the pattern (see below)\n keyframes: Keyframe[]; // monotonically increasing times\n easing?: EasingFunction; // default per-keyframe\n}\n```\n\nFor times before the first keyframe, the value is clamped to the first\nkeyframe's value. After the last keyframe, clamped to the last value.\nBetween keyframes `i` and `i+1`, the value is interpolated using the\neasing on keyframe `i+1` (or the animation's `easing` if not specified\nper-keyframe). A single-keyframe track is a constant.\n\n#### 6.3.1. Color keyframes\n\nWhen EVERY keyframe `value` in a track parses as a color (\u00A73.4 \u2014 `#\u2026`,\n`rgb(\u2026)`, `rgba(\u2026)`), the track is a *color track*: values interpolate\ncomponentwise in straight-alpha RGB space (alpha included), using the\nsame easing rules. Color tracks are honored on `fill_color` (shape and\nplain text) and `stroke_color` (shape). Unparseable colors fall back to\nopaque white anywhere colors are parsed.\n\nWith `loop`, local time folds before interpolation: `true` wraps \u2014\n`t' = t mod span` \u2014 and `\"ping-pong\"` reflects \u2014 `t' = span \u2212 |((t mod\n2\u00B7span)) \u2212 span|` \u2014 where `span` is the LAST keyframe's `time`. The\npattern repeats for the element's whole life; without `loop`, time\npast the last keyframe holds the final value (unchanged default).\nLooping applies to scalar, color, and `position` keyframe animations.\n\n### 6.4. Easing functions\n\nCKP/1.0 defines 36 named easing functions plus two parametric forms.\nMathematical definitions are in the reference implementation\n(`packages/runtime/src/animation/easings.ts`); the polynomial/sine/expo/\ncirc/back families follow easings.net.\n\n```\nlinear\n\nease, ease-in, ease-out, ease-in-out\nease-in-cubic, ease-out-cubic, ease-in-out-cubic\nease-in-quad, ease-out-quad, ease-in-out-quad\nease-in-quart, ease-out-quart, ease-in-out-quart\nease-in-quint, ease-out-quint, ease-in-out-quint\nease-in-sine, ease-out-sine, ease-in-out-sine\nease-in-expo, ease-out-expo, ease-in-out-expo\nease-in-circ, ease-out-circ, ease-in-out-circ\nease-in-back, ease-out-back, ease-in-out-back\n\nelastic-in, elastic-out, elastic-in-out (decaying sinusoidal overshoot)\nbounce-in, bounce-out, bounce-in-out (piecewise-parabolic ball drop)\n\nspring (damped harmonic oscillator: mass=1, damping=10, stiffness=100;\n ~5% overshoot then settles. Remotion's signature feel.)\n```\n\nParametric forms (string-valued):\n\n- `cubic-bezier(x1, y1, x2, y2)` \u2014 CSS timing-function semantics;\n `x1`/`x2` MUST be clamped to `[0, 1]`, `y1`/`y2` are unbounded.\n- `steps(n)` \u2014 `n` equidistant steps, jump-at-end (CSS `steps(n, end)`).\n\nUnknown easing names MUST fall back to `linear` (never error). Output\nMUST match the reference within \u00B10.001 at any input value in `[0, 1]`.\n\n### 6.5. Per-unit text animations\n\nThe `text-*` animation types apply to `text` elements only (ignored on\nother element types; `caption` elements have their own kinetic system).\nThe text splits into *units* and each unit runs the same animation,\noffset in time by `stagger` seconds per unit index.\n\n**Unit indexing.** Letter index counts drawn glyphs (whitespace\nexcluded); word index counts whitespace-separated runs. Both run\ncontinuously across spans and line breaks. Unit `u` starts at\n`time + u \u00D7 stagger`.\n\n**Defaults.** `split`: `\"word\"` for `text-appear`/`text-slide`/\n`text-fly`, `\"letter\"` for `text-typewriter`/`text-wave`/`text-flip`.\n`stagger`: `0.09` for word splits, `0.035` for letter splits.\nPer-unit `duration` default `0.5`.\n\n| Type | Per-unit effect | Defaults |\n|---|---|---|\n| `text-appear` | opacity 0\u21921 over `duration` | easing `ease-out-cubic` |\n| `text-slide` | opacity 0\u21921 + displaced `distance` px opposite `direction`, settling at rest | `distance` 40, `direction` up, easing `ease-out-cubic` |\n| `text-fly` | as `text-slide`, farther | `distance` 140, easing `ease-out-back` |\n| `text-typewriter` | opacity steps 0\u21921 at the unit's start time | no fade |\n| `text-wave` | y offset `distance \u00D7 sin(2\u03C0\u00B7frequency\u00B7t \u2212 0.6\u00B7u)` | `distance` 12, `frequency` 1.5; full-length default (\u00A76.2.1) |\n| `text-flip` | opacity 0\u21921 + 3D rotation `rotation \u00D7 (1\u2212eased)` degrees about `axis` through the unit's center, settling flat (CKP/1.0) | `rotation` 90, `axis` `\"x\"`, easing `ease-out-cubic` |\n\nPer-unit effects fold into the glyph's tint (opacity) and an\nelement-local offset applied BEFORE the element transform (\u00A74.4), so\nkinetic type composes with scale/skew/rotation.\n\n**`text-flip` semantics (CKP/1.0).** The rotation pivot is the unit's\nrest-layout center \u2014 the glyph cell's center for letter splits, the\nword's glyph bounding-box center for word splits \u2014 translated by the\nunit's current per-unit offset, so a unit sliding and flipping stays\nrigid. A word split rotates the word as ONE slab (its glyphs never\nsplay). When multiple `text-flip` animations target an element, word\nrotations compose OUTSIDE letter rotations. Axis `\"x\"` flips up\n(rotation about the horizontal axis), `\"y\"` swings in, `\"z\"` spins\nin-plane. Like all 3D (\u00A74.4.2), the depth component is orthographic\nwithout a `camera`; the foreshortening is `cos \u03B8` exactly. An active\n`text-flip` puts the text element on the full-matrix path; elements\nwithout one are unaffected (\u00A74.4.3 cost rules).\n\nCKP/1.0 defines text animations as entrances only: `time: \"end\"` on a\n`text-*` animation MUST be ignored.\n\n### 6.6. Transitions (non-feature)\n\nCKP/1.0 deliberately defines NO transition primitive \u2014 every transition\ndecomposes into existing primitives, and the document is exactly what\nrenders:\n\n- **Crossfades, pushes, zoom swaps** \u2014 two overlapping elements with\n paired animations (AGENTS.md \u00A7\"Transitions\").\n- **Wipes (circular, linear, stripe, soft-edged)** \u2014 the incoming slide\n inside a masked group (\u00A75.8.2) whose mask elements animate: a growing\n ellipse, a sweeping rectangle, a luma gradient band (AGENTS.md\n \u00A7\"Wipes\").\n\nAn earlier draft reserved a first-class two-layer transition object for\nwipes; group masks made it unnecessary and it is no longer planned.\n\n### 6.7. Spatial motion paths\n\nA `keyframe_animations` entry with `property: \"position\"` moves the\nelement along a path; it overrides the element's `x` and `y` (and\nany scalar `x`/`y` keyframe animations). Keyframe `value`s are\n`[x, y]` pairs in canvas pixels \u2014 or `[x, y, z]` triples for a 3D\npath (z in pixels, +z toward the viewer, \u00A74.4). A 3D path\nadditionally overrides the element's `z` (and any scalar `z`\nkeyframe animations); a 2D path leaves `z` untouched. All keyframes\nof one position path MUST agree in dimensionality \u2014 mixing `[x, y]`\nand `[x, y, z]` in one animation is a validation error (no silent\nz = 0 promotion).\n\n```json\n{ \"property\": \"position\", \"auto_orient\": true, \"keyframes\": [\n { \"time\": 0, \"value\": [200, 800], \"out_tangent\": [240, -300] },\n { \"time\": 2, \"value\": [1700, 300], \"in_tangent\": [-200, -120],\n \"easing\": \"ease-in-out\" }\n] }\n```\n\nEach consecutive keyframe pair is one CUBIC BEZIER segment with\ncontrol points\n\n```\nP0 = a.value P3 = b.value\nP1 = P0 + a.out_tangent P2 = P3 + b.in_tangent\n```\n\nwhere an omitted tangent defaults to the straight-line third-point\n(`P1 = P0 + (P3\u2212P0)/3`, `P2 = P3 \u2212 (P3\u2212P0)/3`) \u2014 a path with no\nhandles is exact polyline motion. On a 3D path, tangents are\n`[dx, dy]` or `[dx, dy, dz]`; a 2-component tangent's `dz` defaults\nto the straight-line third-point in z (the omitted-handle rule,\napplied per axis). 3-component tangents on a 2D path are a\nvalidation error.\n\nTravel is ARC-LENGTH parameterized (NORMATIVE): the destination\nkeyframe's easing maps segment-local time to a fraction `u` of the\nsegment's length; the bezier parameter is found on a 64-chord\ncumulative-length table (curve sampled at `t = i/64`, `i = 0\u202664`;\nlinear interpolation between chords). With linear easing the element\ntravels at constant speed however the handles stretch the curve's\nparameterization. On a 3D path the chord lengths are measured on the\n3D curve \u2014 constant speed means constant speed through depth too.\nBefore the first keyframe the element holds the first point; after\nthe last, the last point.\n\n`auto_orient: true` adds the path's travel direction \u2014\n`atan2(dy, dx)` of the bezier derivative at the sampled parameter, in\ndegrees \u2014 to the element's own `rotation`. Orientation is STRICTLY\nIN-PLANE: on a 3D path the tangent's xy projection is used and `dz`\nis ignored \u2014 auto_orient never derives `x_rotation` or `y_rotation`\n(a path does not tilt the element's plane). A zero xy derivative\n(z-only travel or coincident control points) falls back to the\nsegment chord's xy projection. Position values are numbers (pixels);\nlength strings are not valid inside path keyframes.\n\nLike the `z` field itself (\u00A74.4.2), path z has no visual effect\nwithout perspective somewhere in the chain \u2014 under no camera the\nelement renders at the path's xy projection.\n\n---\n\n## 7. Time, duration, sequencing\n\n### 7.1. Element activity windows\n\nAn element is *active* at composition time `t` if:\n\n```\nstart <= t <= start + duration\n```\n\nwhere:\n\n- `start = element.time` (default `0`).\n- `duration = element.duration` if numeric, else the composition's\n remaining time if `\"auto\"` or `\"end\"`.\n\nInactive elements MUST NOT be rendered.\n\n### 7.2. Local time\n\nMany properties (keyframes, named-animation timing, particle simulation)\noperate in *local time* \u2014 seconds elapsed since the element\nbecame active. Local time `0` corresponds to composition time\n`element.time`.\n\nFor animation evaluation, local time MUST be clamped to the element's\nduration:\n\n```\nlocal_t = min(t \u2212 element.time, element_duration)\n```\n\nRationale: the activity check (\u00A77.1) computes the element's end as\n`time + duration` while local time computes `t \u2212 time`; at exact frame\nboundaries these two float roundings can disagree by ~1 ulp, leaving an\nelement active at a local time fractionally PAST its duration \u2014 which\nwould skip every end-anchored animation for one frame and flash the\nstatic value. The clamp makes the boundary frame well-defined.\n\n### 7.3. The composition duration\n\nIf `Source.duration` is `\"auto\"`, the composition's effective duration\nis the maximum end time across all elements. Otherwise, it is the\ndeclared value.\n\nRuntimes MUST produce frames from composition time `0` to `duration`\ninclusive of `0`, exclusive of `duration`. At 30 fps and `duration: 5`\nthis produces 150 frames at times `0, 1/30, 2/30, ..., 149/30`.\n\n---\n\n## 8. Asset references\n\nAsset-bearing elements (`image`, `video`, `audio`) carry a `source`\nstring identifying the asset. The protocol does not embed binaries.\n\n### 8.1. Allowed schemes\n\nConforming runtimes MUST attempt to resolve:\n\n- `https://` URLs\n- `http://` URLs (MAY require user opt-in for mixed content)\n\nConforming runtimes MAY additionally support:\n\n- `file://` URLs (local file references)\n- Absolute paths (`/path/to/file`)\n- Relative paths (`./asset.png`) \u2014 resolved against an\n implementation-defined base\n- `data:` URIs\n\nRuntimes MUST fail with a clear error when a `source` cannot be\nresolved. They MUST NOT silently substitute a placeholder.\n\n### 8.2. Preloading\n\nRuntimes SHOULD provide a `preload` step that resolves all asset\nreferences before rendering begins. This is REQUIRED for export\nconformance (\u00A710.3) \u2014 exports cannot tolerate runtime asset failures.\n\n---\n\n## 9. Output and rendering\n\n### 9.1. Determinism\n\nGiven identical `(Source, time)` inputs, every conforming runtime MUST\nproduce visually equivalent frames. \"Visually equivalent\" means:\n\n- Same element positions, sizes, rotations, opacities to within \u00B10.5 px.\n- Same animation values to within \u00B10.001 for normalized properties.\n- Same particle state (positions, alphas, sizes) to within \u00B10.5 px /\n \u00B10.001 alpha.\n- Color output MAY differ by up to ~1/255 per channel due to\n rasterization backends. Pixel-exact equivalence is NOT REQUIRED.\n\n### 9.2. Frame timing\n\nA runtime asked to produce frame `f` of a composition with `frame_rate`\nFPS MUST render the scene state at composition time:\n\n```\nt = f / frame_rate\n```\n\n`f = 0` is the first frame.\n\n### 9.3. Output formats\n\n| Format | Behavior |\n|---|---|\n| `mp4` | Encoded video. H.264 baseline + AAC audio is the recommended baseline. Higher profiles MAY be used. |\n| `gif` | Animated GIF. Audio is silently dropped. |\n\nRuntimes MAY support output formats beyond these.\n\n---\n\n## 10. Conformance levels\n\nCKP defines three nested conformance levels. An implementation MAY claim\nany level it actually supports.\n\n### 10.1. Level 1 \u2014 Validation\n\nThe implementation MUST be able to parse a Source JSON object and\nreport whether it is a valid CKP/1.0 document. Specifically:\n\n- Accept any valid document per \u00A72\u20138.\n- Reject documents with invalid `type` discriminators on required\n fields, missing REQUIRED fields, or out-of-range values.\n- Tolerate unknown additional fields (forward compatibility, \u00A72.2).\n\nThe `@clipkit/protocol` package provides Level 1 validation as a\nreference.\n\n### 10.2. Level 2 \u2014 Rendering\n\nThe implementation MUST also be able to produce frame images from a\nvalid Source. Specifically:\n\n- All element types (\u00A75) MUST render.\n- All animations (\u00A76) MUST animate.\n- Output MUST satisfy the determinism requirements (\u00A79.1).\n\nA Level 2 implementation MUST NOT silently skip element types unless\nthe document is at a higher protocol version than the implementation\nsupports.\n\n### 10.3. Level 3 \u2014 Export\n\nThe implementation MUST also be able to produce encoded output:\ntypically MP4 with mixed audio.\n\n- All Level 2 requirements.\n- Audio elements (\u00A75.4) MUST be mixed into the output track.\n- Output duration MUST match `Source.duration` precisely (frame-accurate).\n\nThe reference runtime `@clipkit/runtime` is a Level 3 implementation: it\nsums all sources to the master bus, then applies a fixed peak limiter\n(transparent below 0 dBFS) so a hot mix is contained rather than\nhard-clipped. The same limiter runs in preview, so what you hear matches\nthe render. The exact mix algorithm otherwise remains\nimplementation-defined (\u00A71).\n\n---\n\n## 11. Versioning and extensions\n\n### 11.1. The `clipkit_version` field\n\nDocuments SHOULD declare their protocol version:\n\n```json\n{ \"clipkit_version\": \"1.0\", \"elements\": [...] }\n```\n\nAbsence is interpreted as `\"1.0\"` for the lifetime of CKP/1.x.\n\n### 11.2. Version compatibility\n\nVersions follow `MAJOR.MINOR` (semver-style without patch):\n\n- **Same MINOR**: runtimes MUST render documents at the same minor\n version with no warning.\n- **Higher MINOR**: runtimes MUST attempt to render. They SHOULD warn\n that unknown fields may be ignored.\n- **Higher MAJOR**: runtimes MUST refuse to render and report the\n version mismatch. Major versions indicate breaking changes.\n- **Lower MAJOR**: runtimes MUST render if they implement the older\n major version. Backward compatibility within a major line is\n permanent.\n\n### 11.3. Adding fields\n\nAny minor version MAY add fields to existing element types. Unknown\nfields in older runtimes pass through harmlessly per \u00A72.2.\n\n### 11.4. Adding element types\n\nAny minor version MAY add new element types. Runtimes that do not\nrecognize a new `type` MAY skip the element with a warning.\n\n### 11.5. Breaking changes\n\nRemoving fields, changing field types, or changing the meaning of an\nexisting field requires a major-version bump.\n\n### 11.6. Extension namespace\n\nImplementations and tools MAY include vendor-specific fields prefixed\nwith `x_`. The protocol reserves bare names; `x_` names are\nimplementation-defined and ignored by other implementations. Names\nthe protocol itself defines (e.g. `x_scale`, `x_skew`, `x_rotation`)\nare bare protocol names, not extensions, regardless of prefix.\n\n### 11.7. Version history\n\n| Version | Additions |\n|---|---|\n| 1.0 | Initial protocol, including the 3D transform model (\u00A74.4): `x_rotation` / `y_rotation` / `z_rotation` (alias of `rotation`) / `z` on every element; Source-level `camera`; paint-order + flattening compositing rules (\u00A74.4.3); glass under 3D via the pane-plane homography (\u00A74.7); 3D motion paths (`[x, y, z]` position keyframes, \u00A76.7); `text-flip` per-unit 3D reveals (\u00A76.5); particle depth (`z_velocity` / `z_spread`, \u00A75.7.3). These are opt-in and additive \u2014 documents that use none of them render the same as a pure-2D source. |\n\n---\n\n## 12. Implementation notes (non-normative)\n\nThese notes are advisory. They reflect lessons from the reference\nimplementation and MAY help third-party implementers avoid pitfalls.\n\n### 12.1. Premultiplied alpha\n\nAll blending in the reference runtime uses premultiplied alpha:\ntextures are uploaded premultiplied, shaders output premultiplied\nvalues, and the canvas swap-chain is configured with\n`alphaMode: \"premultiplied\"`. This avoids the \"dark halo\" artifacts\nthat appear with straight-alpha blending. Third-party runtimes are\nfree to use any blending convention internally as long as final output\nmatches \u00A79.1.\n\n### 12.2. Corner radii on non-square rectangles\n\nIf you implement rounded rectangles with a signed-distance function\nin shaders, the SDF MUST operate in pixel space, not in normalized UV\nspace. UV space is anisotropic for non-square rectangles, so doing the\ncorner math in UV stretches the arc into an ellipse. Pass the\nrectangle's `(width, height)` to the shader and convert\n`uv * size` before the SDF.\n\n### 12.3. Font atlases\n\nThe reference runtime generates a glyph atlas per (family, size,\nweight). It covers ASCII (0x20\u20130x7E) only; characters outside this\nrange are silently dropped. Third-party implementations MAY support\nlarger glyph ranges; documents SHOULD avoid relying on non-ASCII text\nin v1.0 unless the target runtime is known to support it.\n\n### 12.4. Particle PRNG\n\nThe reference runtime uses a `mulberry32` PRNG seeded by\n`FNV-1a(element.id) + n * 0x9e3779b9`. Third-party runtimes are NOT\nrequired to match this exact PRNG, only the *outputs* implied by \u00A75.7.1.\n\n---\n\n## Appendix A: Reference implementation map\n\n| Spec section | Reference implementation |\n|---|---|\n| \u00A72 Source | `packages/protocol/src/types.ts` (Source) + `zod.ts` (sourceSchema) |\n| \u00A73 Units | `packages/runtime/src/compositor/unit.ts` |\n| \u00A74 Element model | `packages/protocol/src/types.ts` (BaseElement) |\n| \u00A75.1 shape | `packages/runtime/src/compositor/element-renderers/shape.ts` |\n| \u00A75.2 text | `packages/runtime/src/compositor/element-renderers/text.ts` |\n| \u00A75.3\u20135.4 video/image/audio | `packages/runtime/src/{compositor/element-renderers,audio}/` |\n| \u00A75.5 caption | `packages/runtime/src/compositor/element-renderers/caption.ts` |\n| \u00A75.6 shape paths | `packages/runtime/src/svg/svg-renderer.ts` |\n| \u00A75.7 particles | `packages/runtime/src/compositor/element-renderers/particles.ts` |\n| \u00A76 Animation | `packages/runtime/src/animation/` |\n| \u00A710 Conformance | `packages/protocol/src/validate.ts` |\n\n## Appendix B: Document history\n\n- *2026-05-28* \u2014 CKP/1.0 draft. Initial publication alongside the\n reference runtime.\n";
|
|
3
|
+
//# sourceMappingURL=agents-content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agents-content.d.ts","sourceRoot":"","sources":["../../src/templates/agents-content.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,iBAAiB,ok2CAy8B7B,CAAC;AAEF,eAAO,MAAM,mBAAmB,45yGAowE/B,CAAC"}
|