@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,3282 @@
|
|
|
1
|
+
// AUTO-GENERATED — do not edit by hand.
|
|
2
|
+
// Source: scripts/embed-agents.mjs (run via npm run prebuild).
|
|
3
|
+
export const AGENTS_MD_CONTENT = `# Clipkit — Authoring guide (AGENTS.md)
|
|
4
|
+
|
|
5
|
+
> This file is the authoring-time context for AI tools (and humans)
|
|
6
|
+
> writing Clipkit videos. Paste it into a system prompt or attach it
|
|
7
|
+
> to a Claude project. For the **formal protocol spec** — element types,
|
|
8
|
+
> field semantics, conformance levels, normative MUST/SHOULD/MAY — see
|
|
9
|
+
> [\`PROTOCOL.md\`](./PROTOCOL.md).
|
|
10
|
+
|
|
11
|
+
Clipkit is a protocol for describing motion-graphics videos as JSON.
|
|
12
|
+
A \`Source\` document is the list of \`elements\` (with timing and
|
|
13
|
+
animations) that a conforming runtime turns into frames or MP4.
|
|
14
|
+
|
|
15
|
+
There are two layers you work with as an author:
|
|
16
|
+
|
|
17
|
+
1. **Primitives** — the element types defined by the Clipkit Protocol.
|
|
18
|
+
The formal spec is in [PROTOCOL.md](./PROTOCOL.md). Section 1 below
|
|
19
|
+
is a quick cheat sheet so you don't have to bounce out for common
|
|
20
|
+
lookups, but PROTOCOL.md is the source of truth.
|
|
21
|
+
2. **Patterns** — authoring-time TypeScript helpers in \`@clipkit/patterns\`
|
|
22
|
+
that produce primitive elements. You never reference patterns in
|
|
23
|
+
a Source JSON; you call them in TS/JS and inline their output.
|
|
24
|
+
Patterns are documented in section 2 (Pattern catalog).
|
|
25
|
+
|
|
26
|
+
Section 3 (Recipes) walks through complete example videos showing how
|
|
27
|
+
patterns compose into a full piece.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 0. Which surface to use
|
|
32
|
+
|
|
33
|
+
Before authoring anything, pick the right surface for your context.
|
|
34
|
+
There are two — they layer cleanly, no overlap.
|
|
35
|
+
|
|
36
|
+
| You are running in… | Use… | How |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| **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. |
|
|
39
|
+
| **Claude.ai chat, Claude Desktop without local tools, or any chat-mode AI without filesystem access** | \`@clipkit/mcp-server\` | Call the MCP tools — \`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. |
|
|
40
|
+
| **A human developer** | \`@clipkit/cli\` | \`npx @clipkit/cli init my-video\`, edit files, \`npx @clipkit/cli render\`. Same path as IDE-embedded AI. |
|
|
41
|
+
| **CI / a build script** | \`@clipkit/cli\` | One-shot \`clipkit render input.json --out out.mp4\`. |
|
|
42
|
+
|
|
43
|
+
A few clarifications agents sometimes need:
|
|
44
|
+
|
|
45
|
+
- **The CLI and the MCP server are not interchangeable.** They target
|
|
46
|
+
different deployments. CLI assumes you have a shell. MCP assumes you
|
|
47
|
+
don't.
|
|
48
|
+
- **If you can run \`Bash\`, you're in CLI territory.** You don't need
|
|
49
|
+
MCP at all — go straight to files + CLI.
|
|
50
|
+
- **There is no "Clipkit Skill."** The skill concept was considered and
|
|
51
|
+
dropped because everything it would do (auto-load this doc, wrap CLI
|
|
52
|
+
commands as slash commands) is already covered by the CLI and by MCP
|
|
53
|
+
resource-loading. If a user asks about installing the skill, redirect
|
|
54
|
+
them to the CLI.
|
|
55
|
+
|
|
56
|
+
Once you know your surface, continue to §1 for the schema cheat sheet.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 1. Schema cheat sheet
|
|
61
|
+
|
|
62
|
+
Authoritative spec: **[PROTOCOL.md](./PROTOCOL.md)** (CKP/1.0). What
|
|
63
|
+
follows is the at-a-glance reference for things you look up constantly.
|
|
64
|
+
|
|
65
|
+
### Source root
|
|
66
|
+
|
|
67
|
+
\`\`\`json
|
|
68
|
+
{
|
|
69
|
+
"clipkit_version": "1.0",
|
|
70
|
+
"output_format": "mp4",
|
|
71
|
+
"width": 1920, "height": 1080,
|
|
72
|
+
"duration": 30, "frame_rate": 30,
|
|
73
|
+
"background_color": "#000000",
|
|
74
|
+
"elements": [ /* ... */ ]
|
|
75
|
+
}
|
|
76
|
+
\`\`\`
|
|
77
|
+
|
|
78
|
+
Optional root field \`motion_blur: { "samples": 8, "shutter": 0.5 }\`
|
|
79
|
+
adds whole-frame motion blur at render time — the frame becomes the
|
|
80
|
+
exact average of \`samples\` sub-frame renders across a shutter window
|
|
81
|
+
(\`shutter\` = fraction of the frame interval; 0.5 ≈ a film camera's
|
|
82
|
+
180° shutter). Use it when fast motion strobes: whip-pans, fast
|
|
83
|
+
travel, spinning elements. Renders cost \`samples\`× the frames, so keep
|
|
84
|
+
samples at 8 (default) unless trails visibly stair-step. Previews play
|
|
85
|
+
unblurred; the blur appears in the exported file.
|
|
86
|
+
|
|
87
|
+
### Element types
|
|
88
|
+
|
|
89
|
+
| \`type\` | Renders | Key fields |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| \`shape\` | rect / ellipse / rounded rect, **or** vector paths | primitive: \`shape\`, \`fill_color\`, \`gradient\`, \`border_radius\`; path form: \`paths[]\`, \`view_box\`, \`gradients\` |
|
|
92
|
+
| \`text\` | one-line text | \`text\`, \`spans\`, \`font_family\`, \`font_size\`, \`background_color\`, \`text_shadow\`, \`mask\` |
|
|
93
|
+
| \`image\` | bitmap | \`source\`, \`fit\`, \`crop_*\` |
|
|
94
|
+
| \`video\` | video frame at element time | \`source\`, \`volume\`, \`trim_start\`, \`loop\`, \`fit\`, \`crop_*\` |
|
|
95
|
+
| \`audio\` | non-visual; mixed in export | \`source\`, \`volume\` |
|
|
96
|
+
| \`caption\` | word-timed captions | \`words\`, \`style\`, \`highlight_color\` |
|
|
97
|
+
| \`particles\` | ballistic or convergence | \`rate\`/\`burst\`/\`target_points\`, \`lifetime\`, \`velocity\`, \`z_velocity\` |
|
|
98
|
+
| \`group\` | container; transforms/animates children as one | \`elements[]\`, \`clip\`, \`mask\`, \`time_remap\` |
|
|
99
|
+
|
|
100
|
+
A \`shape\` is **either** a primitive (\`shape: "rectangle" | "ellipse"\`, default
|
|
101
|
+
rectangle) **or** vector geometry (\`paths\`) — never both on one element. When
|
|
102
|
+
\`paths\` is present it wins and the primitive fields (\`shape\`, \`fill_color\`,
|
|
103
|
+
\`gradient\`, \`border_radius\`, \`stroke_*\`) are ignored; geometry and fill come
|
|
104
|
+
from inside each path. There is no \`triangle\`/\`polygon\` primitive — use \`paths\`
|
|
105
|
+
(e.g. \`"M ... L ... L ... Z"\`) for triangles and any arbitrary/morphing shape.
|
|
106
|
+
|
|
107
|
+
\`text\` and \`caption\` take \`background_color\` (+ \`background_border_radius\`,
|
|
108
|
+
\`background_padding\` as a number or \`[x, y]\`) for a solid bg drawn as **one
|
|
109
|
+
band per line**, each shrink-wrapped to that line's glyphs (the social-
|
|
110
|
+
caption look) — don't put a separate shape behind text; the integral bg
|
|
111
|
+
auto-sizes to the wrapped / auto-fit text and moves with it.
|
|
112
|
+
|
|
113
|
+
For text shadows: \`text_shadow\` ({\`color\`, \`offset_x\`, \`offset_y\`, \`blur\`,
|
|
114
|
+
\`opacity\`}, or an **array** for stacked / 3D-extrusion shadows) is
|
|
115
|
+
**per-glyph** — each letter casts its own, tracking per-letter animation.
|
|
116
|
+
Use the \`drop_shadow\` *effect* instead for one soft shadow of the whole
|
|
117
|
+
text silhouette.
|
|
118
|
+
|
|
119
|
+
\`image\` and \`video\` take \`fit\` (CSS \`object-fit\`: \`cover\` default,
|
|
120
|
+
\`contain\`, \`fill\`, \`none\`) and a source **crop** — \`crop_x\` / \`crop_y\` /
|
|
121
|
+
\`crop_width\` / \`crop_height\`, normalized \`0..1\` of the source, origin
|
|
122
|
+
top-left, default \`0,0,1,1\` (whole source). Crop selects a sub-rectangle
|
|
123
|
+
of the media BEFORE \`fit\` maps it into the box; the element box is
|
|
124
|
+
unchanged. Each crop component is keyframeable — animate the origin to
|
|
125
|
+
pan and the size to zoom (Ken Burns) without touching layout.
|
|
126
|
+
|
|
127
|
+
There is deliberately NO nested-composition ("pre-comp") element. Reuse
|
|
128
|
+
is an authoring-time concern — component patterns in \`@clipkit/patterns\`
|
|
129
|
+
(section 2) expand into plain elements before serialization. Nested
|
|
130
|
+
timing (speed-ramp / freeze / reverse a whole scene) is \`time_remap\` on
|
|
131
|
+
a plain \`group\`.
|
|
132
|
+
|
|
133
|
+
### Shared transform fields (every element)
|
|
134
|
+
|
|
135
|
+
\`x\`, \`y\`, \`x_anchor\` (0..1), \`y_anchor\` (0..1), \`width\`, \`height\`,
|
|
136
|
+
\`rotation\` (degrees), \`scale\`, \`opacity\` (0..1 — CSS convention),
|
|
137
|
+
and the CKP/1.0 3D fields \`x_rotation\` / \`y_rotation\` / \`z_rotation\` /
|
|
138
|
+
\`z\` (see "3D transforms" below).
|
|
139
|
+
|
|
140
|
+
All of \`x\` / \`y\` / \`width\` / \`height\` accept numbers (pixels) or
|
|
141
|
+
length strings — \`"50%"\`, \`"10vw"\`, \`"15vh"\`, \`"5vmin"\`, \`"5vmax"\`.
|
|
142
|
+
|
|
143
|
+
> ⚠️ **\`%\` is relative to the CANVAS, not the parent** (unlike CSS).
|
|
144
|
+
> \`"50%"\` on \`x\` means half the canvas width regardless of nesting.
|
|
145
|
+
> \`vw\`/\`vh\`/\`vmin\`/\`vmax\` likewise resolve against the canvas. There is
|
|
146
|
+
> no parent-relative percentage; use pixels inside groups when you need
|
|
147
|
+
> child-relative sizing.
|
|
148
|
+
|
|
149
|
+
### Coming from CSS/HTML
|
|
150
|
+
|
|
151
|
+
Clipkit deliberately tracks CSS where an agent's CSS prior would
|
|
152
|
+
otherwise misfire, and deliberately diverges where the protocol earns
|
|
153
|
+
it. Aligned (write them like CSS): **\`opacity\`** is \`0..1\` (default 1);
|
|
154
|
+
**gradient \`angle\`** is the CSS convention (\`0deg\` = to top, clockwise,
|
|
155
|
+
so \`90\` = to right, \`180\`/default = to bottom); **colors** accept hex,
|
|
156
|
+
\`rgb()/rgba()\`, \`hsl()/hsla()\`, the 148 CSS named colors, and
|
|
157
|
+
\`transparent\`. Field name map:
|
|
158
|
+
|
|
159
|
+
| CSS | Clipkit | Note |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| \`left\` / \`top\` | \`x\` / \`y\` | top-left corner, like CSS |
|
|
162
|
+
| \`width\` / \`height\` | \`width\` / \`height\` | px or canvas-% / vw / vh |
|
|
163
|
+
| \`opacity\` | \`opacity\` | \`0..1\`, default 1 |
|
|
164
|
+
| \`border-radius\` | \`border_radius\` | px |
|
|
165
|
+
| \`color\` / \`background\` | \`fill_color\` | SVG-flavored name |
|
|
166
|
+
| \`border\` color | \`stroke_color\` / \`stroke_width\` | |
|
|
167
|
+
| \`z-index\` | \`z\` (+ \`layer\`) | see below |
|
|
168
|
+
| \`transform: rotate()\` | \`rotation\` (= \`z_rotation\`) | degrees |
|
|
169
|
+
| \`transform: scale()\` | \`scale\` / \`x_scale\` / \`y_scale\` | |
|
|
170
|
+
| \`linear-gradient(θ)\` | \`gradient.angle\` | same θ convention |
|
|
171
|
+
| \`transition\` / \`@keyframes\` | \`animations\` / \`keyframe_animations\` | declarative timeline |
|
|
172
|
+
|
|
173
|
+
**Positioning is the CSS model:** \`x\`/\`y\` place the element's **top-left
|
|
174
|
+
corner** (\`x_anchor\`/\`y_anchor\` default \`0\`), exactly like \`left\`/\`top\` /
|
|
175
|
+
SVG / Canvas. Set \`x_anchor: 0.5, y_anchor: 0.5\` to position by center
|
|
176
|
+
(or \`1\` for the far edge). Rotation and scale always pivot the element's
|
|
177
|
+
center — like CSS \`transform-origin\` — independent of the anchor. So a
|
|
178
|
+
full-frame layer is just \`x:0, y:0, width:W, height:H\`.
|
|
179
|
+
|
|
180
|
+
**Layers (required).** Every element gets its own integer \`layer\` (1..1000),
|
|
181
|
+
like an After Effects layer: **layer 1 is on top**, higher numbers go toward
|
|
182
|
+
the back. Give each element a UNIQUE layer — number them 1..N within a container
|
|
183
|
+
(the top-level \`elements\`, each group's \`elements\`, each group mask's
|
|
184
|
+
\`elements\`). Duplicate layers are a validation error. \`z\` depth still wins over
|
|
185
|
+
\`layer\`; \`layer\` orders elements within equal depth.
|
|
186
|
+
|
|
187
|
+
Kept divergences (intentional — don't "fix" them):
|
|
188
|
+
- **\`z\` not \`z_index\`.** \`z\` is the single depth axis (layering +
|
|
189
|
+
perspective foreshortening); \`layer\` orders within equal depth (lower = front). See "3D
|
|
190
|
+
transforms".
|
|
191
|
+
- **snake_case keys** and SVG-flavored \`fill_color\` / \`stroke_color\`.
|
|
192
|
+
- **Easing names are a superset of CSS** (\`ease-out-back\`, springs, …).
|
|
193
|
+
|
|
194
|
+
### Expressions (procedural motion)
|
|
195
|
+
|
|
196
|
+
A numeric property can be a **formula** instead of a number or a keyframe
|
|
197
|
+
table — a pure function of the element's own clock. Reach for it on the
|
|
198
|
+
motions that are one line as math and a nightmare as keyframes: bobs,
|
|
199
|
+
orbits, spins, handheld shake, staggered reveals.
|
|
200
|
+
|
|
201
|
+
\`\`\`jsonc
|
|
202
|
+
{ "y": { "expr": "540 + sin(t * PI) * 30" } } // bob ±30px at 0.5 Hz
|
|
203
|
+
{ "rotation": { "expr": "t * 90" } } // 90°/s spin
|
|
204
|
+
{ "x": { "expr": "960 + wiggle(3, 12)" } } // handheld shake, ±12px
|
|
205
|
+
{ "opacity": { "expr": "clamp(t / 0.4, 0, 1)" } } // 0.4s linear fade-in
|
|
206
|
+
\`\`\`
|
|
207
|
+
|
|
208
|
+
In scope: \`t\` (element-local seconds), \`dur\`, \`i\` (index in a generated
|
|
209
|
+
set), \`n\` (sibling count), \`value\` (the property's base default); the
|
|
210
|
+
constants \`PI\`/\`TAU\`/\`E\`; and a fixed function set (\`sin cos … clamp lerp
|
|
211
|
+
smoothstep linear ease noise wiggle random\`). Operators \`+ - * / % ^\`,
|
|
212
|
+
comparisons, \`&& || !\`, and \`cond ? a : b\`. The full normative grammar is
|
|
213
|
+
PROTOCOL.md §3.6.
|
|
214
|
+
|
|
215
|
+
Use \`i\` for staggered fleets — give every generated element the **same**
|
|
216
|
+
expression and they self-offset by index:
|
|
217
|
+
|
|
218
|
+
\`\`\`jsonc
|
|
219
|
+
{ "y": { "expr": "300 + i * 80" }, // stack by index
|
|
220
|
+
"opacity": { "expr": "clamp((t - i * 0.1) / 0.3, 0, 1)" } } // ripple-in
|
|
221
|
+
\`\`\`
|
|
222
|
+
|
|
223
|
+
The rules that keep it safe and portable (all NORMATIVE):
|
|
224
|
+
- **No cross-element references, no runtime inputs.** An \`expr\` sees only
|
|
225
|
+
its own clock — never another element, the mouse, or audio. (Those are
|
|
226
|
+
"Tier-B" and permanently unsupported. If you're tempted, bake the
|
|
227
|
+
relationship into keyframes, or parent the elements.)
|
|
228
|
+
- **Deterministic.** \`noise\`/\`wiggle\`/\`random\` are seeded hashes, not
|
|
229
|
+
wall-clock RNG, so every render is identical; \`seed\` defaults to \`0\`.
|
|
230
|
+
- **Bakeable.** An expression and a \`Keyframe[]\` are interchangeable — the
|
|
231
|
+
importer/editor can sample one into the other.
|
|
232
|
+
- A typo (unknown name, member access, a string) does **not** stop the
|
|
233
|
+
render — the property silently falls back to its base value. So if a
|
|
234
|
+
move "does nothing," check the formula.
|
|
235
|
+
|
|
236
|
+
### 3D transforms (CKP/1.0)
|
|
237
|
+
|
|
238
|
+
Every element also takes \`x_rotation\` (tip top edge away), \`y_rotation\`
|
|
239
|
+
(turn right edge away), \`z_rotation\` (alias of \`rotation\` — author one
|
|
240
|
+
or the other, both is a validation error), and \`z\` — **the one depth
|
|
241
|
+
axis** (px toward the viewer). \`z\` is also the stacking control: it
|
|
242
|
+
orders elements always (higher \`z\` = in front), and *additionally*
|
|
243
|
+
foreshortens once a camera is present. There is no separate \`z_index\` —
|
|
244
|
+
use \`z\` for layering, \`layer\` orders within equal depth (lower = front, unique per container). Add a
|
|
245
|
+
Source-level camera for true perspective:
|
|
246
|
+
|
|
247
|
+
\`\`\`jsonc
|
|
248
|
+
{ "camera": { "perspective": 1200 }, // px; smaller = stronger
|
|
249
|
+
"elements": [{ "type": "group", "clip": true, "y_rotation": -24,
|
|
250
|
+
"width": 620, "height": 420, "elements": [ /* UI mock */ ] }] }
|
|
251
|
+
\`\`\`
|
|
252
|
+
|
|
253
|
+
The tilted-UI promo recipe: put the screenshot/mock in a \`clip: true\`
|
|
254
|
+
group and rotate THE GROUP — the flattened layer projects as one card.
|
|
255
|
+
|
|
256
|
+
The camera also MOVES (CKP/1.0): \`x\`/\`y\`/\`z\` position and \`x_rotation\`/
|
|
257
|
+
\`y_rotation\`/\`z_rotation\` Euler orientation (all keyframeable) orbit,
|
|
258
|
+
pan, tilt, and dolly the viewpoint — not just push/pull on \`perspective\`.
|
|
259
|
+
A "look at the logo" move is authored as resolved orientation angles
|
|
260
|
+
(there is no \`target\` field). Identity pose = the plain perspective lens.
|
|
261
|
+
|
|
262
|
+
GOTCHAS:
|
|
263
|
+
- **Paint order is by \`z\` depth — always.** Sibling cards paint
|
|
264
|
+
back-to-front by \`z\` (nearer = in front), whether or not there's a
|
|
265
|
+
camera; a camera just adds perspective on top. So adding/removing a
|
|
266
|
+
camera never reorders — only the foreshortening appears. With every
|
|
267
|
+
\`z = 0\` it collapses to \`layer\` order (layer 1 on top, like a plain 2D doc).
|
|
268
|
+
Set \`camera.sort: "paint"\` to force fixed \`layer\` order under a camera.
|
|
269
|
+
The sort is whole-card (no per-pixel depth buffer): interpenetrating or
|
|
270
|
+
depth-ambiguous cards can mis-order — keep cards from crossing, or use
|
|
271
|
+
\`sort: "paint"\`.
|
|
272
|
+
- **Animate 3D via \`keyframe_animations\`** (\`property: "y_rotation"\`),
|
|
273
|
+
not a keyframe array in the field itself (same rule as \`rotation\`).
|
|
274
|
+
- **No camera = affine + flat.** 3D rotations still foreshorten (a
|
|
275
|
+
y-rotated card narrows) but edges stay parallel, and \`z\` orders the
|
|
276
|
+
stack without adding perspective; add the camera for converging
|
|
277
|
+
perspective and parallax.
|
|
278
|
+
- **Glass works in 3D** — tilt a glass pane (\`y_rotation\` + camera)
|
|
279
|
+
and the whole optical model rides the plane: footprint, refraction,
|
|
280
|
+
highlights and shadow all project correctly. Signature move: a glass
|
|
281
|
+
panel swinging over a UI card. (Edge-on with no camera = invisible.)
|
|
282
|
+
- **Plain groups nest 3D for free** (children live in the parent's 3D
|
|
283
|
+
space); \`clip\`/\`mask\` groups flatten, then the layer tilts as a unit.
|
|
284
|
+
Filter fields and \`effects\` evaluate on the PROJECTED pixels — glow
|
|
285
|
+
radius / stroke width stay uniform in screen px on a tilted card.
|
|
286
|
+
|
|
287
|
+
3D text reveals — \`{ "type": "text-flip", "axis": "x" }\` flips each
|
|
288
|
+
letter in from 90° to flat about its own center (AE's classic 3D
|
|
289
|
+
per-character entrance). \`axis: "y"\` swings units in sideways, \`"z"\`
|
|
290
|
+
spins them in-plane; \`split: "word"\` flips whole words as rigid slabs;
|
|
291
|
+
\`rotation\` sets the start angle (try 180 for a full tumble). Composes
|
|
292
|
+
with \`text-slide\` (units slide AND flip). Pair with a \`camera\` for true
|
|
293
|
+
perspective on the mid-flip letters; without one the flip still reads
|
|
294
|
+
(pure foreshortening). Captions don't take text-* animations.
|
|
295
|
+
|
|
296
|
+
### Lighting + materials (CKP/1.0)
|
|
297
|
+
|
|
298
|
+
Surfaces can respond to light — PBR. **Opt-in:** with no \`lights\` and no
|
|
299
|
+
\`material\`, everything renders unlit exactly as before. Add scene
|
|
300
|
+
\`lights\` + a \`material\` on an element and the runtime shades it (the
|
|
301
|
+
element's own pixels are the albedo).
|
|
302
|
+
|
|
303
|
+
\`\`\`jsonc
|
|
304
|
+
{ "lights": [ { "type": "ambient", "intensity": 0.4 },
|
|
305
|
+
{ "type": "directional", "azimuth": 45, "elevation": 30, "intensity": 1.2 } ],
|
|
306
|
+
"environment": { "type": "gradient",
|
|
307
|
+
"stops": [ { "offset": 0, "color": "#0b0f1a" },
|
|
308
|
+
{ "offset": 1, "color": "#7da2ff" } ] },
|
|
309
|
+
"camera": { "perspective": 1400 },
|
|
310
|
+
"elements": [ { "type": "group", "clip": true, "y_rotation": -20,
|
|
311
|
+
"material": { "roughness": 0.25, "metalness": 0.6, "reflectivity": 1 },
|
|
312
|
+
"elements": [ /* dark UI */ ] } ] }
|
|
313
|
+
\`\`\`
|
|
314
|
+
|
|
315
|
+
- **\`material\`**: \`roughness\` (0 glossy/tight .. 1 matte), \`metalness\`
|
|
316
|
+
(0 dielectric .. 1 metal), \`reflectivity\` (env-reflection strength),
|
|
317
|
+
\`emissive\` (self-lit), \`normal_map\` (tangent-space normal map URL for
|
|
318
|
+
surface detail/bumps — flat = \`#8080ff\`) + \`normal_scale\`. Scalars
|
|
319
|
+
animatable.
|
|
320
|
+
- **The reflection sweeps with the camera.** Specular + environment
|
|
321
|
+
reflection are view-dependent, so orbiting/dollying the camera slides
|
|
322
|
+
the highlight across a glossy dark UI — the premium look. In 2D (no
|
|
323
|
+
camera) animate the **lights** instead for the sweep.
|
|
324
|
+
- **\`environment\`** is what reflective surfaces mirror: either a gradient
|
|
325
|
+
"sky" (\`{ type: 'gradient', stops }\`, offset 0 = down, 1 = up) or an
|
|
326
|
+
**equirectangular image** (\`{ type: 'image', src }\`) for real
|
|
327
|
+
photographic reflections. Roughness blurs the reflection toward the
|
|
328
|
+
environment average. It's what makes glossy screens/metal look
|
|
329
|
+
expensive.
|
|
330
|
+
- **Works on shapes AND textured surfaces** — images, video, and
|
|
331
|
+
flattened \`clip\` groups light from their own pixels, so a whole UI card
|
|
332
|
+
wrapped in a clip group shades as one lit plane. \`@clipkit/patterns\`
|
|
333
|
+
ships \`litSurface()\` for exactly this.
|
|
334
|
+
- **Bloom** (\`source.bloom { threshold, knee, intensity, radius }\`): a
|
|
335
|
+
whole-frame post that makes BRIGHT things bleed light (specular hits,
|
|
336
|
+
emissive surfaces, bright media) — driven by each pixel's brightness,
|
|
337
|
+
not a per-element knob. For a deliberate per-element halo (even on a
|
|
338
|
+
dark element), use the \`glow\` effect instead.
|
|
339
|
+
- **Editor authoring:** Material is a per-element inspector block; Lights,
|
|
340
|
+
Environment, and Bloom live under Video settings (source scope).
|
|
341
|
+
- Local illumination only (no cast shadows yet). Flat 2.5D surfaces.
|
|
342
|
+
|
|
343
|
+
### Blend modes + filters (every element)
|
|
344
|
+
|
|
345
|
+
\`blend_mode\`: \`"multiply"\` (darken; white neutral), \`"screen"\`
|
|
346
|
+
(lighten; black neutral), \`"add"\` (glow). Element-local. On a group it
|
|
347
|
+
needs \`clip: true\` or a \`mask\` (the group must flatten to a layer).
|
|
348
|
+
|
|
349
|
+
Filters, all animatable, CSS semantics: \`blur_radius\` (Gaussian σ px),
|
|
350
|
+
\`brightness\` / \`contrast\` / \`saturation\` (multipliers, 1 = unchanged,
|
|
351
|
+
\`saturation: 0\` = grayscale). Work on any element including groups —
|
|
352
|
+
a group filters as one flattened image.
|
|
353
|
+
|
|
354
|
+
There is no \`color_overlay\` field — it decomposes: put a same-sized
|
|
355
|
+
shape of the overlay color on a lower layer, in front of the element (use
|
|
356
|
+
\`opacity\` or \`blend_mode: "multiply"\` for tint strength). For a
|
|
357
|
+
grayscale-then-tint look, combine \`saturation: 0\` with an overlay.
|
|
358
|
+
|
|
359
|
+
### Stylize effects (every element)
|
|
360
|
+
|
|
361
|
+
\`effects\` is an ordered array of passes, applied after the filters:
|
|
362
|
+
|
|
363
|
+
\`\`\`json
|
|
364
|
+
"effects": [{ "type": "pixelate", "cell_size": 12 }]
|
|
365
|
+
\`\`\`
|
|
366
|
+
|
|
367
|
+
Types: \`pixelate\` (\`cell_size\`), \`dither\` (\`levels\`, 2 = 1-bit retro),
|
|
368
|
+
\`halftone\` (\`cell_size\`, \`angle\`), \`ascii\` (\`cell_size\`). Every param
|
|
369
|
+
takes a number or keyframes (element-local time) — animating
|
|
370
|
+
\`cell_size\` from 1 upward makes a great "depixelate" reveal. Effects
|
|
371
|
+
chain in array order and work on groups (the subtree flattens first).
|
|
372
|
+
Layer styles — \`glow\` (\`radius\`, \`intensity\`, \`color\`), \`drop_shadow\`
|
|
373
|
+
(\`offset_x/y\`, \`blur\`, \`color\`, \`opacity\`), \`stroke\` (\`width\`,
|
|
374
|
+
\`color\`) — composite beneath the element and work on text, images,
|
|
375
|
+
and groups (not just shapes).
|
|
376
|
+
|
|
377
|
+
Group time remapping — \`time_remap\` on a GROUP warps the clock for
|
|
378
|
+
everything inside it: slow a whole composed scene to 20% at the
|
|
379
|
+
dramatic beat, freeze it, or run it backwards — children's animations
|
|
380
|
+
and nested videos all follow — and so does AUDIO, varispeed-style
|
|
381
|
+
(ramps pitch with speed, freezes silent, reverse plays backwards).
|
|
382
|
+
The group's own position/opacity still animate on real time, so you
|
|
383
|
+
can move a frozen scene around. Same keyframe shape as video
|
|
384
|
+
time_remap below.
|
|
385
|
+
|
|
386
|
+
Time remapping — \`time_remap\` on a video element maps element-local
|
|
387
|
+
time → media seconds with plain keyframes: \`[{time: 0, value: 0},
|
|
388
|
+
{time: 1, value: 3}]\` is a 3× speed ramp, a flat segment is a freeze
|
|
389
|
+
frame, decreasing values play in reverse, easings make the ramps
|
|
390
|
+
cinematic. It replaces \`trim_start\`/\`playback_rate\`/\`loop\`. Audio
|
|
391
|
+
follows the warp VARISPEED, like tape: ramps pitch up/down with
|
|
392
|
+
speed, freezes go silent, reverse plays the sound backwards — usually
|
|
393
|
+
exactly the cinematic speed-ramp sound you want.
|
|
394
|
+
|
|
395
|
+
GOTCHA: the runtime keys ONE decoder + texture per unique video URL.
|
|
396
|
+
Two video elements with the same URL but different playheads
|
|
397
|
+
(different \`time_remap\`, trims, or rates) will fight over it — all
|
|
398
|
+
copies display the same frames and seeking thrashes. Give each its
|
|
399
|
+
own URL; a query suffix on the same file (\`clip.mp4?copy=2\`) is
|
|
400
|
+
enough.
|
|
401
|
+
|
|
402
|
+
Trim paths — on any shape path: \`trim_start\`/\`trim_end\` draw only that
|
|
403
|
+
slice of the stroke (animate \`trim_end\` 0\\u21921 for the logo-draws-itself
|
|
404
|
+
reveal), \`trim_offset\` rotates the window with wrap (animate it for
|
|
405
|
+
the traveling-dash "snake"). \`stroke_progress\` is the simple sugar.
|
|
406
|
+
Path morphing: keyframe the \`d\` itself — values with the SAME command
|
|
407
|
+
structure (e.g. two cubics with equal point counts) tween smoothly;
|
|
408
|
+
mismatched structures snap. Author morph targets with matching
|
|
409
|
+
commands, like every motion tool expects.
|
|
410
|
+
|
|
411
|
+
Living motion, one line each — \`drift\` (seeded organic float; the
|
|
412
|
+
"alive logo" look: \`{type: "drift", distance: 20}\`), \`breathe\` (gentle
|
|
413
|
+
scale pulse), \`orbit\` (circular motion: \`distance\` radius,
|
|
414
|
+
\`direction: "left"\` for counter-clockwise). All run the element's full
|
|
415
|
+
life by default and are exactly seeded — same seed, same motion. And
|
|
416
|
+
any keyframe_animation can now \`loop: true\` (repeat) or
|
|
417
|
+
\`loop: "ping-pong"\` (back-and-forth) — build one cycle, loop forever.
|
|
418
|
+
|
|
419
|
+
Motion paths — \`keyframe_animations\` with \`property: "position"\` and
|
|
420
|
+
\`[x, y]\` values moves an element along a bezier path: \`out_tangent\` /
|
|
421
|
+
\`in_tangent\` on the keyframes shape the curve (omit them for straight
|
|
422
|
+
polyline hops), \`auto_orient: true\` turns the element to face its
|
|
423
|
+
travel direction. Speed is arc-length-true: linear easing = constant
|
|
424
|
+
speed; easings shape acceleration along the path. Use for swooping
|
|
425
|
+
entrances, orbit flybys, anything that should move like it has
|
|
426
|
+
momentum instead of lerping x and y separately.
|
|
427
|
+
|
|
428
|
+
3D paths — use \`[x, y, z]\` values (and optionally \`[dx, dy, dz]\`
|
|
429
|
+
tangents) to carve the path through depth; the path then drives \`z\`
|
|
430
|
+
too, and arc-length speed is true in 3D. Don't mix \`[x, y]\` and
|
|
431
|
+
\`[x, y, z]\` keyframes in one path (validation error). Two gotchas:
|
|
432
|
+
path z is invisible without a \`camera\` (same as the \`z\` field), and
|
|
433
|
+
\`auto_orient\` stays in-plane — it spins \`rotation\` from the xy
|
|
434
|
+
direction but never tilts the element's plane (add \`x_rotation\` /
|
|
435
|
+
\`y_rotation\` yourself if you want banking). Signature move: fly an
|
|
436
|
+
element from deep background to right up against the camera along a
|
|
437
|
+
curve.
|
|
438
|
+
|
|
439
|
+
Procedural noise — \`fractal_noise\` (\`scale\`, \`evolution\`, \`offset_x/y\`,
|
|
440
|
+
\`octaves\`, \`seed\`) fills the element's footprint with seeded grayscale
|
|
441
|
+
fBM: animate \`evolution\` for churning fog/plasma, chain \`levels\` to
|
|
442
|
+
shape contrast and a duotone \`lut\` for color (grayscale has no hue, so
|
|
443
|
+
\`hue_rotate\` alone can't tint it), or put it on TEXT for noise-filled
|
|
444
|
+
type. \`turbulent_displace\` (\`amount\`, \`scale\`, \`evolution\`, \`octaves\`,
|
|
445
|
+
\`seed\`) warps the element's own pixels — wavy text, heat shimmer.
|
|
446
|
+
Same seed = same pixels, always (the noise function is normative).
|
|
447
|
+
|
|
448
|
+
Grading — the \`hue_rotate\` filter field (degrees, joins
|
|
449
|
+
blur/brightness/contrast/saturation on every element) plus two
|
|
450
|
+
effects: \`levels\` (\`in_black\`, \`in_white\`, \`gamma\`, \`out_black\`,
|
|
451
|
+
\`out_white\` — punchy shadows = \`in_black: 0.08, gamma: 1.15\`) and
|
|
452
|
+
\`lut\` (\`source\` = a .cube file URL or data: URI, \`intensity\` to dial
|
|
453
|
+
it back). A LUT is the one-liner for a whole color story
|
|
454
|
+
(teal-orange, film stocks); levels is the surgical tool.
|
|
455
|
+
|
|
456
|
+
Keying — \`chroma_key\` (\`color\`, \`tolerance\`, \`softness\`, \`spill\`)
|
|
457
|
+
removes a screen color by chroma distance: put it on a green-screen
|
|
458
|
+
\`video\` element with \`color\` set to the screen's ACTUAL green (sample
|
|
459
|
+
it — real screens are darker than \`#00FF00\`), then place the video
|
|
460
|
+
over any backdrop. \`spill\` (default 0.5) desaturates green bounce on
|
|
461
|
+
the subject. \`luma_key\` (\`threshold\`, \`softness\`, \`invert\`) drops dark
|
|
462
|
+
pixels — the way to lift white-on-black mattes, flares, and smoke
|
|
463
|
+
stock onto a scene. On a group, the composited children key as one
|
|
464
|
+
layer.
|
|
465
|
+
|
|
466
|
+
\`glass\` is the one effect that reads the BACKDROP (everything drawn
|
|
467
|
+
beneath the element). It applies to SHAPE elements only (the pane
|
|
468
|
+
geometry must be exact; other element types skip it with a warning).
|
|
469
|
+
Defaults give the classic clear liquid-glass button — most uses need
|
|
470
|
+
NO params at all:
|
|
471
|
+
|
|
472
|
+
\`\`\`json
|
|
473
|
+
{ "type": "shape", "border_radius": 65, "width": 380, "height": 300,
|
|
474
|
+
"fill_color": "#FFFFFF",
|
|
475
|
+
"effects": [{ "type": "glass" }] }
|
|
476
|
+
\`\`\`
|
|
477
|
+
|
|
478
|
+
Two materials, one dial: \`blur_radius: 0\` (default) = CLEAR glass,
|
|
479
|
+
\`blur_radius > 0\` = FROSTED. \`mode: "dome"\` + \`edge_width\` = the
|
|
480
|
+
shape's radius = a half-sphere magnifier:
|
|
481
|
+
|
|
482
|
+
\`\`\`json
|
|
483
|
+
{ "id": "magnifier", "type": "shape", "shape": "ellipse",
|
|
484
|
+
"width": 320, "height": 320, "fill_color": "#FFFFFF",
|
|
485
|
+
"effects": [{ "type": "glass", "mode": "dome",
|
|
486
|
+
"edge_width": 160, "refraction": 25 }] }
|
|
487
|
+
\`\`\`
|
|
488
|
+
|
|
489
|
+
Other dials (reference-tuned defaults — change sparingly):
|
|
490
|
+
\`refraction\` (bend strength, ≈px), \`edge_width\` (lens depth; small =
|
|
491
|
+
flat card with curl at the rim), \`edge_highlight\` (the whole light
|
|
492
|
+
rig), \`dispersion\` (color fringing), \`shadow\` (built-in, outside-only
|
|
493
|
+
— never add a shadow sibling under glass), \`backdrop_saturation\`,
|
|
494
|
+
\`tint\`. The pane's fill color is ignored — only its geometry matters.
|
|
495
|
+
A crisp ring is a stroked transparent sibling above; wrap pane + ring
|
|
496
|
+
in an UNLAYERED group (no clip/mask) to move them as one element.
|
|
497
|
+
|
|
498
|
+
### Easings (the 36)
|
|
499
|
+
|
|
500
|
+
\`linear\`, \`ease\`, \`ease-in\`, \`ease-out\`, \`ease-in-out\`, plus
|
|
501
|
+
ease-{in,out,in-out}-{cubic, quad, quart, quint, sine, expo, circ,
|
|
502
|
+
back}, plus \`spring\` (damped harmonic oscillator — overshoots ~5% then
|
|
503
|
+
settles; Remotion's signature feel), \`elastic-{in,out,in-out}\` (decaying
|
|
504
|
+
sinusoidal wobble), and \`bounce-{in,out,in-out}\` (ball-drop bounces — the
|
|
505
|
+
easing curves, distinct from the bounce-in/out animation PRESETS, which
|
|
506
|
+
are scale tweens). Two parametric forms also count: \`cubic-bezier(x1, y1,
|
|
507
|
+
x2, y2)\` and \`steps(n)\`.
|
|
508
|
+
|
|
509
|
+
Quick guide:
|
|
510
|
+
- **\`spring\`** — hero text reveals, signature scale-ins
|
|
511
|
+
- **\`ease-out-cubic\`** — smooth, well-behaved ramps; default choice
|
|
512
|
+
- **\`ease-out-quart\`** — fast start, long slow tail; great for measure bars
|
|
513
|
+
- **\`ease-out-back\`** — slight overshoot at the end (UI bounce)
|
|
514
|
+
- **\`linear\`** — when you specifically want no easing
|
|
515
|
+
|
|
516
|
+
### Particles — two modes
|
|
517
|
+
|
|
518
|
+
Ballistic (emit + physics):
|
|
519
|
+
|
|
520
|
+
\`\`\`json
|
|
521
|
+
{
|
|
522
|
+
"type": "particles",
|
|
523
|
+
"x": 540, "y": 960,
|
|
524
|
+
"burst": true, "burst_count": 140,
|
|
525
|
+
"lifetime": 2.5, "velocity": 900, "spread": 360, "gravity": 700,
|
|
526
|
+
"color": ["#22d3ee", "#ec4899", "#22c55e", "#fbbf24"],
|
|
527
|
+
"particle_shape": "square", "size": 18, "rotation_speed": 540,
|
|
528
|
+
"fade_at": 0.8
|
|
529
|
+
}
|
|
530
|
+
\`\`\`
|
|
531
|
+
|
|
532
|
+
Convergence (assemble into a target shape):
|
|
533
|
+
|
|
534
|
+
\`\`\`json
|
|
535
|
+
{
|
|
536
|
+
"type": "particles",
|
|
537
|
+
"x": 960, "y": 540,
|
|
538
|
+
"burst": true, "burst_count": 500,
|
|
539
|
+
"target_points": [[100, 200], [110, 200], /* ... */ ],
|
|
540
|
+
"scatter_radius": 1100,
|
|
541
|
+
"convergence_easing": "ease-out-quart",
|
|
542
|
+
"lifetime": 1.6, "size": 10, "particle_shape": "circle"
|
|
543
|
+
}
|
|
544
|
+
\`\`\`
|
|
545
|
+
|
|
546
|
+
Depth — add \`z_velocity\` (px/s toward the viewer, can be negative) and
|
|
547
|
+
\`z_spread\` (random vz range) to blow particles out of the screen:
|
|
548
|
+
nearer ones grow, receding ones shrink. Needs a Source \`camera\` to be
|
|
549
|
+
visible (no perspective = no depth cue), and pairs beautifully with a
|
|
550
|
+
tilted emitter (\`x_rotation\` on the particles element): confetti
|
|
551
|
+
erupting off a card's surface. Paint order stays spawn order — depth
|
|
552
|
+
never re-sorts.
|
|
553
|
+
|
|
554
|
+
### Shape paths — vector graphics
|
|
555
|
+
|
|
556
|
+
\`\`\`json
|
|
557
|
+
{
|
|
558
|
+
"type": "shape",
|
|
559
|
+
"view_box": [0, 0, 180, 180],
|
|
560
|
+
"gradients": [
|
|
561
|
+
{ "id": "g1", "type": "linear", "x1": 0, "y1": 0, "x2": 180, "y2": 0,
|
|
562
|
+
"stops": [{ "offset": 0, "color": "#FF4E00" }, { "offset": 1, "color": "#FF1791" }] }
|
|
563
|
+
],
|
|
564
|
+
"paths": [
|
|
565
|
+
{ "d": "M 10 10 L 90 90", "stroke": "url(#g1)", "stroke_width": 5,
|
|
566
|
+
"stroke_progress": [{ "time": 0, "value": 0 }, { "time": 1, "value": 1 }] }
|
|
567
|
+
]
|
|
568
|
+
}
|
|
569
|
+
\`\`\`
|
|
570
|
+
|
|
571
|
+
\`stroke_progress\` from 0→1 makes the path draw in via dashoffset. Use a
|
|
572
|
+
thick stroke on a small circle for pie/donut charts.
|
|
573
|
+
|
|
574
|
+
### Diagonal text reveal
|
|
575
|
+
|
|
576
|
+
\`\`\`json
|
|
577
|
+
{
|
|
578
|
+
"type": "text", "text": "Vercel and Clipkit", "font_size": 70,
|
|
579
|
+
"mask": {
|
|
580
|
+
"type": "linear-wipe", "angle": -45,
|
|
581
|
+
"progress": [{ "time": 0, "value": 0 }, { "time": 2.7, "value": 1, "easing": "ease-out-cubic" }],
|
|
582
|
+
"softness": 0.3
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
\`\`\`
|
|
586
|
+
|
|
587
|
+
That's the at-a-glance set. For full normative semantics — units, edge
|
|
588
|
+
cases, conformance — go to [PROTOCOL.md](./PROTOCOL.md).
|
|
589
|
+
|
|
590
|
+
## 2. Pattern catalog
|
|
591
|
+
|
|
592
|
+
Patterns live in \`@clipkit/patterns\`. Each is a TS function that emits
|
|
593
|
+
plain elements — most return \`Element[]\` you spread into your \`elements\`
|
|
594
|
+
array; *component* patterns (IntroCard, LowerThird) return a single
|
|
595
|
+
\`group\` element you push, so the unit moves / animates / time-remaps as
|
|
596
|
+
one. They're opinionated: pick a variant by passing a \`color\` slot
|
|
597
|
+
("pink" | "green" | "blue" | "lavender" | "purple" | "yellow" | "gray")
|
|
598
|
+
and a \`theme\` (\`'mux'\` is the default; \`'minimal'\` exists too).
|
|
599
|
+
|
|
600
|
+
Common params shared by most patterns:
|
|
601
|
+
|
|
602
|
+
- \`id\`: string — prefix for produced element ids
|
|
603
|
+
- \`theme?: 'mux' | 'minimal'\` — default \`'mux'\`
|
|
604
|
+
- \`color\`: ColorName — picks the accent palette
|
|
605
|
+
- \`time\`, \`duration\`: number — scene-local seconds
|
|
606
|
+
- \`layerBase\`: number — pattern uses a small range starting here
|
|
607
|
+
|
|
608
|
+
### HeaderBar
|
|
609
|
+
|
|
610
|
+
The scene framing for dashboard-style scenes: white header strip with
|
|
611
|
+
left logo + middle title + right date, colored body fill below.
|
|
612
|
+
|
|
613
|
+
\`\`\`ts
|
|
614
|
+
headerBar({
|
|
615
|
+
id: 'overall',
|
|
616
|
+
title: 'Overall stats',
|
|
617
|
+
dateRange: 'Nov. 17 - Dec. 16 2021',
|
|
618
|
+
bodyColor: 'pink',
|
|
619
|
+
canvasWidth: 1920,
|
|
620
|
+
canvasHeight: 1080,
|
|
621
|
+
logo: { viewBox: [0, 0, 215, 70], paths: MUX_LOGO_PATHS, gradients: [MUX_GRADIENT] },
|
|
622
|
+
time: 4.33,
|
|
623
|
+
duration: 6.0,
|
|
624
|
+
layerBase: 100,
|
|
625
|
+
})
|
|
626
|
+
\`\`\`
|
|
627
|
+
|
|
628
|
+
Use as the first elements of every scene that follows the "logo + title"
|
|
629
|
+
convention.
|
|
630
|
+
|
|
631
|
+
### StatBlock
|
|
632
|
+
|
|
633
|
+
Themed top border, big spring-counted number, label, optional trend pill.
|
|
634
|
+
|
|
635
|
+
\`\`\`ts
|
|
636
|
+
statBlock({
|
|
637
|
+
id: 'views',
|
|
638
|
+
current: 195654112,
|
|
639
|
+
previous: 159560036, // optional — adds the trend pill
|
|
640
|
+
label: 'Total views',
|
|
641
|
+
color: 'pink',
|
|
642
|
+
x: 120, y: 380,
|
|
643
|
+
width: 1680,
|
|
644
|
+
time: 4.33, duration: 6.0,
|
|
645
|
+
layerBase: 200,
|
|
646
|
+
})
|
|
647
|
+
\`\`\`
|
|
648
|
+
|
|
649
|
+
Stack two with \`y\` 340 px apart for a "Total views / Total minutes watched"
|
|
650
|
+
layout.
|
|
651
|
+
|
|
652
|
+
### BarChartRow
|
|
653
|
+
|
|
654
|
+
Row with an animated white background bar, top border, value count-up,
|
|
655
|
+
optional icon, label, optional trend pill. Used for "Top 5 by X" lists.
|
|
656
|
+
|
|
657
|
+
\`\`\`ts
|
|
658
|
+
barChartRow({
|
|
659
|
+
id: 'phone',
|
|
660
|
+
value: 130733672,
|
|
661
|
+
max: 130733672, // the leading value in the dataset
|
|
662
|
+
previous: 133478441, // optional — adds trend pill
|
|
663
|
+
label: 'Phone',
|
|
664
|
+
icon: { viewBox: [0, 0, 32, 52], paths: PHONE_ICON_PATHS },
|
|
665
|
+
color: 'green',
|
|
666
|
+
x: 120, y: 320,
|
|
667
|
+
width: 1680,
|
|
668
|
+
time: 10.33, duration: 6.0,
|
|
669
|
+
staggerIndex: 0, // 0..n for cascading entry
|
|
670
|
+
layerBase: 400,
|
|
671
|
+
})
|
|
672
|
+
\`\`\`
|
|
673
|
+
|
|
674
|
+
Loop over your dataset, increment \`y\` by \`168\` and \`staggerIndex\` by 1
|
|
675
|
+
per row.
|
|
676
|
+
|
|
677
|
+
### RankedList
|
|
678
|
+
|
|
679
|
+
Numbered list of items with rank, name, value, animated measure bar.
|
|
680
|
+
1 or 2 columns. Used for "Top 10" style scenes.
|
|
681
|
+
|
|
682
|
+
\`\`\`ts
|
|
683
|
+
rankedList({
|
|
684
|
+
id: 'titles',
|
|
685
|
+
items: [
|
|
686
|
+
{ label: 'Burgandy Alert', value: 2733413 },
|
|
687
|
+
{ label: 'Tough Affection', value: 1694421 },
|
|
688
|
+
// ...
|
|
689
|
+
],
|
|
690
|
+
color: 'lavender',
|
|
691
|
+
x: 120, y: 320,
|
|
692
|
+
width: 1680,
|
|
693
|
+
columns: 2,
|
|
694
|
+
time: 16.33, duration: 6.0,
|
|
695
|
+
layerBase: 600,
|
|
696
|
+
})
|
|
697
|
+
\`\`\`
|
|
698
|
+
|
|
699
|
+
### PieCard
|
|
700
|
+
|
|
701
|
+
Animated pie chart (white slice growing over the colored body), big
|
|
702
|
+
percentage label, count-up view count, optional trend pill, optional
|
|
703
|
+
logo on a rounded white square, bottom label. One card per data point;
|
|
704
|
+
position multiple horizontally for a "Top 4 X" scene.
|
|
705
|
+
|
|
706
|
+
\`\`\`ts
|
|
707
|
+
pieCard({
|
|
708
|
+
id: 'chrome',
|
|
709
|
+
value: 2233793,
|
|
710
|
+
total: 3396911, // dataset total — drives the percentage
|
|
711
|
+
previous: 2337628, // optional — adds trend pill
|
|
712
|
+
label: 'Chrome',
|
|
713
|
+
logoUrl: '/chrome.png', // optional — image over the white square
|
|
714
|
+
color: 'blue',
|
|
715
|
+
cx: 300, // center x of the card
|
|
716
|
+
time: 28.33, duration: 6.0,
|
|
717
|
+
staggerIndex: 0,
|
|
718
|
+
layerBase: 1000,
|
|
719
|
+
})
|
|
720
|
+
\`\`\`
|
|
721
|
+
|
|
722
|
+
### IntroCard (component)
|
|
723
|
+
|
|
724
|
+
Full-frame opening title: themed backdrop, optional all-caps kicker,
|
|
725
|
+
big headline, accent rule wipe, optional subtitle. Entrance + exit
|
|
726
|
+
animation built in. Returns ONE \`group\` element — position, animate, or
|
|
727
|
+
\`time_remap\` the whole card as a unit.
|
|
728
|
+
|
|
729
|
+
\`\`\`ts
|
|
730
|
+
elements.push(introCard({
|
|
731
|
+
id: 'open',
|
|
732
|
+
kicker: 'Mux Data',
|
|
733
|
+
headline: '2021 in review',
|
|
734
|
+
subtitle: 'Your year of video, in numbers',
|
|
735
|
+
color: 'pink',
|
|
736
|
+
canvasWidth: 1920, canvasHeight: 1080,
|
|
737
|
+
time: 0, duration: 4,
|
|
738
|
+
layer: 100,
|
|
739
|
+
}))
|
|
740
|
+
\`\`\`
|
|
741
|
+
|
|
742
|
+
Note components return a single \`Element\` — \`push\`, don't spread.
|
|
743
|
+
|
|
744
|
+
### LowerThird (component)
|
|
745
|
+
|
|
746
|
+
Broadcast-style name strip: accent bar + white panel + bold name +
|
|
747
|
+
optional role line. Slides in from the left, fades out at the end of
|
|
748
|
+
its window. Returns ONE \`group\` element.
|
|
749
|
+
|
|
750
|
+
\`\`\`ts
|
|
751
|
+
elements.push(lowerThird({
|
|
752
|
+
id: 'speaker',
|
|
753
|
+
name: 'Dana Cruz',
|
|
754
|
+
role: 'Head of Video',
|
|
755
|
+
color: 'blue',
|
|
756
|
+
x: 80, y: 900, // left edge / vertical center
|
|
757
|
+
time: 12, duration: 5,
|
|
758
|
+
layer: 300,
|
|
759
|
+
}))
|
|
760
|
+
\`\`\`
|
|
761
|
+
|
|
762
|
+
### TiltedShowcase (component)
|
|
763
|
+
|
|
764
|
+
The promo signature shot: a screenshot in a browser-chrome frame,
|
|
765
|
+
tilted in 3D (CKP/1.0) and gently swinging. Returns ONE \`clip: true\`
|
|
766
|
+
group, so the framed card projects as a unit. Pair with a Source-level
|
|
767
|
+
\`camera: { perspective: 1200 }\` for converging perspective.
|
|
768
|
+
|
|
769
|
+
\`\`\`ts
|
|
770
|
+
elements.push(tiltedShowcase({
|
|
771
|
+
id: 'app-shot',
|
|
772
|
+
source: '/screens/dashboard.png',
|
|
773
|
+
color: 'blue',
|
|
774
|
+
x: 960, y: 540, // card center
|
|
775
|
+
width: 760, height: 500,
|
|
776
|
+
tilt: 22, // swings ±22°; 0 = static
|
|
777
|
+
z: 40,
|
|
778
|
+
time: 2, duration: 6,
|
|
779
|
+
layer: 200,
|
|
780
|
+
}))
|
|
781
|
+
\`\`\`
|
|
782
|
+
|
|
783
|
+
### cameraOrbit (scene camera)
|
|
784
|
+
|
|
785
|
+
Unlike the others, this returns a \`Camera\` for \`source.camera\` (not an
|
|
786
|
+
element): a ready-made keyframed move — orbit (\`yaw\`), constant \`pitch\`
|
|
787
|
+
tilt, \`dolly\` (z), \`truck\` (x). Place content at varied \`z\` for parallax;
|
|
788
|
+
depth-correct occlusion is automatic (§4.4.3).
|
|
789
|
+
|
|
790
|
+
\`\`\`ts
|
|
791
|
+
const source = {
|
|
792
|
+
camera: cameraOrbit({ perspective: 1500, yaw: 40, pitch: 6, dolly: 120, duration: 5 }),
|
|
793
|
+
elements: [ /* cards at different z */ ],
|
|
794
|
+
};
|
|
795
|
+
\`\`\`
|
|
796
|
+
|
|
797
|
+
### TrendPill (helper)
|
|
798
|
+
|
|
799
|
+
Small pill rendering a signed percentage delta. Used internally by
|
|
800
|
+
StatBlock, BarChartRow, PieCard, but exported so you can place one
|
|
801
|
+
standalone.
|
|
802
|
+
|
|
803
|
+
\`\`\`ts
|
|
804
|
+
trendPill({
|
|
805
|
+
id: 'standalone-trend',
|
|
806
|
+
cx: 1500, cy: 540,
|
|
807
|
+
delta: trendPct(2233793, 2337628), // → -4.4
|
|
808
|
+
color: 'pink',
|
|
809
|
+
time: 4.33, duration: 6.0,
|
|
810
|
+
layerBase: 250,
|
|
811
|
+
})
|
|
812
|
+
\`\`\`
|
|
813
|
+
|
|
814
|
+
---
|
|
815
|
+
|
|
816
|
+
## 3. Recipe gallery
|
|
817
|
+
|
|
818
|
+
Example videos demonstrating patterns in context. Source files live in
|
|
819
|
+
\`apps/playground/src/\`:
|
|
820
|
+
|
|
821
|
+
- **\`mux-demo.ts\`** — \`MUX_DEMO\`. 1920×1080, 39 s, 7 scenes (intro, big
|
|
822
|
+
stats, devices, top-10 titles, US heatmap, browsers, outro). The
|
|
823
|
+
full-fidelity reference; every pattern in section 2 shows up here.
|
|
824
|
+
- **\`examples.ts\`** — \`VERCEL_TEMPLATE\`. 1280×720, 6.7 s. Showcases the
|
|
825
|
+
SVG stroke-evolution primitive (Next.js logo paths drawing in), the
|
|
826
|
+
text linear-wipe mask, and keyframe-animated rings.
|
|
827
|
+
- **\`examples.ts\`** — \`CODE_WRAPPED\`. 9:16, 15 s. Particle confetti
|
|
828
|
+
bursts at celebration moments, gradient backgrounds, spring stat
|
|
829
|
+
reveals — the github-unwrapped-style template.
|
|
830
|
+
|
|
831
|
+
When you author a new video, lean on these as references. If a scene
|
|
832
|
+
"looks like X scene in mux-demo," call the same pattern with the same
|
|
833
|
+
shape of args.
|
|
834
|
+
|
|
835
|
+
### Captions from media (transcription)
|
|
836
|
+
|
|
837
|
+
To caption a video or audio clip, you don't hand-write \`words\` — transcribe
|
|
838
|
+
the media into a \`caption\` element. Runs Whisper **locally** (no API key, no
|
|
839
|
+
upload) via \`@clipkit/speech-to-text\`; needs \`ffmpeg\` on the host.
|
|
840
|
+
|
|
841
|
+
- **CLI** (local files): \`clipkit transcribe <file>\` prints the caption
|
|
842
|
+
\`words[]\` JSON to stdout (progress on stderr). Add \`--element\` for a full
|
|
843
|
+
\`caption\` element ready to drop into a Source, \`--model Xenova/whisper-small\`
|
|
844
|
+
for accuracy or \`…-tiny.en\` for speed, \`--out cap.json\` to write a file.
|
|
845
|
+
- **MCP** (chat-mode agents): the \`transcribe_media\` tool takes a local
|
|
846
|
+
\`path\`, transcribes, and adds a \`caption\` element to the current project
|
|
847
|
+
(\`add: false\` to only return it).
|
|
848
|
+
|
|
849
|
+
Both produce the same thing: a \`caption\` element with \`words: [{ text, start,
|
|
850
|
+
end }]\` (times relative to the caption's \`time\`). The protocol/runtime are
|
|
851
|
+
unchanged — this only *fills* the caption the renderer already supports. Style
|
|
852
|
+
it with the usual \`caption\` fields (\`style\`, \`highlight_color\`, …).
|
|
853
|
+
|
|
854
|
+
---
|
|
855
|
+
|
|
856
|
+
## 4. Authoring guidance
|
|
857
|
+
|
|
858
|
+
A few rules of thumb that go beyond the schema:
|
|
859
|
+
|
|
860
|
+
- **Lead with patterns; reach for primitives only for one-offs.** If a
|
|
861
|
+
scene fits HeaderBar + BarChartRow, do that. Don't reinvent.
|
|
862
|
+
- **Stagger entrances.** For lists, use \`staggerIndex\` (or compute a
|
|
863
|
+
\`time: time + i*0.07\` offset for primitives). All-at-once entrances
|
|
864
|
+
look amateurish.
|
|
865
|
+
- **Sound matters.** Set up an \`audio\` element early. The runtime mixes
|
|
866
|
+
it into the export and the playground plays it during preview.
|
|
867
|
+
- **Pace by frames at 30 fps.** Most scenes are 180 frames = 6 s. Title
|
|
868
|
+
intros are 100–130 frames. Outros 120–150. Big stat reveals 180.
|
|
869
|
+
- **Trend pills are optional.** Only add \`previous\` to a pattern if the
|
|
870
|
+
comparison is genuinely interesting; otherwise let the number land
|
|
871
|
+
on its own.
|
|
872
|
+
|
|
873
|
+
---
|
|
874
|
+
|
|
875
|
+
## 5. Output format
|
|
876
|
+
|
|
877
|
+
Whatever you generate, the final output the user runs is a Clipkit
|
|
878
|
+
\`Source\` JSON object. Patterns are TypeScript-only — they help you
|
|
879
|
+
construct that JSON but never appear in it.
|
|
880
|
+
|
|
881
|
+
If you're emitting code for the user:
|
|
882
|
+
|
|
883
|
+
\`\`\`ts
|
|
884
|
+
import type { Source } from '@clipkit/protocol';
|
|
885
|
+
import { headerBar, statBlock /* ... */ } from '@clipkit/patterns';
|
|
886
|
+
|
|
887
|
+
export const MY_VIDEO: Source = {
|
|
888
|
+
width: 1920, height: 1080, duration: 30, frame_rate: 30,
|
|
889
|
+
elements: [
|
|
890
|
+
...headerBar({ ... }),
|
|
891
|
+
...statBlock({ ... }),
|
|
892
|
+
// ...
|
|
893
|
+
],
|
|
894
|
+
};
|
|
895
|
+
\`\`\`
|
|
896
|
+
|
|
897
|
+
If you're emitting raw JSON (no TS), inline the patterns' output
|
|
898
|
+
directly. There's no \`{ "type": "stat_block" }\` — only the primitive
|
|
899
|
+
elements the patterns produce.
|
|
900
|
+
|
|
901
|
+
---
|
|
902
|
+
|
|
903
|
+
## Appendix — Protocol change checklist (maintainers)
|
|
904
|
+
|
|
905
|
+
> **This section is for editing the protocol itself, not for authoring
|
|
906
|
+
> videos.** If you're writing a Source, ignore it. If you're adding or
|
|
907
|
+
> changing an element type, field, effect, animation preset, output
|
|
908
|
+
> format, etc., work this list — a protocol change "bleeds down" into
|
|
909
|
+
> more places than it looks.
|
|
910
|
+
|
|
911
|
+
**The schema source of truth (\`packages/protocol/src/\`) — all manual:**
|
|
912
|
+
|
|
913
|
+
- \`types.ts\` — the interface / const enum (\`ELEMENT_TYPES\`,
|
|
914
|
+
\`ANIMATION_TYPES\`, \`OUTPUT_FORMATS\`, the effect union, …).
|
|
915
|
+
- \`zod.ts\` — the matching validator. Const-driven enums sync from the
|
|
916
|
+
arrays; a new interface needs a new schema object. **Put the field's
|
|
917
|
+
default in its \`.describe()\` text** — that string is the only place a
|
|
918
|
+
default is declared protocol-side. It's surfaced to agents via
|
|
919
|
+
\`get_schema\` → \`zodToJsonSchema\`, so the wording an agent reads ("…
|
|
920
|
+
(default 1)") comes straight from here. Keep it honest against the
|
|
921
|
+
runtime.
|
|
922
|
+
- \`CLIPKIT_PROTOCOL_VERSION\` in \`types.ts\` — bump only on a real spec
|
|
923
|
+
change.
|
|
924
|
+
|
|
925
|
+
There is **no central defaults table.** (A former \`defaults.ts\` /
|
|
926
|
+
\`applyDefaults()\` was dead code — never imported or called — and has
|
|
927
|
+
been deleted; don't re-add it.) The *effective* default for a field is
|
|
928
|
+
the inline \`?? fallback\` in whichever runtime renderer reads it
|
|
929
|
+
(\`packages/runtime/src/compositor/element-renderers/*.ts\`, e.g.
|
|
930
|
+
\`fillColor = span.fill_color ?? defaults.color\` in \`text.ts\`) — one
|
|
931
|
+
fallback per field, at its point of use. So a new field with a default
|
|
932
|
+
means **two** edits that must agree: the runtime's \`??\` fallback, and
|
|
933
|
+
the \`.describe()\` text in \`zod.ts\` that documents it.
|
|
934
|
+
|
|
935
|
+
**Runtime (\`packages/runtime/src/\`):** a new field on an existing
|
|
936
|
+
element auto-works if the renderer already reads it. A new **element
|
|
937
|
+
type** needs a renderer in \`compositor/element-renderers/\`; a new
|
|
938
|
+
**effect type** needs handling in the effect chain; a new **animation
|
|
939
|
+
preset** needs a handler in \`animation/presets.ts\`.
|
|
940
|
+
|
|
941
|
+
**Editor inspector — mostly automatic.** The registry deriver
|
|
942
|
+
(\`editor-core/src/registry/derive.ts\`) reads the zod schema by
|
|
943
|
+
structure, so a new field gets a working control with no edits. Only
|
|
944
|
+
touch \`registry/overrides.ts\` when the auto-derived control is wrong
|
|
945
|
+
(range, label, section, or it fell back to a raw JSON chip). Verify
|
|
946
|
+
with \`node probes/probe-editor-registry.mjs\` — it asserts every field
|
|
947
|
+
has exactly one knob and lists anything that fell through.
|
|
948
|
+
|
|
949
|
+
**Docs (all hand-written, no codegen):**
|
|
950
|
+
|
|
951
|
+
- \`PROTOCOL.md\` — the formal spec.
|
|
952
|
+
- This file (\`AGENTS.md\`) — authoring patterns, if it's a preset/effect.
|
|
953
|
+
- \`apps/web/content/docs/fields/*.md\` — per-element field reference
|
|
954
|
+
(separate from PROTOCOL.md; easy to miss).
|
|
955
|
+
- \`packages/mcp-server/src/embedded-docs.ts\` — a COPY of the agent
|
|
956
|
+
guidance baked into the MCP server; it does **not** auto-sync with
|
|
957
|
+
this file, so it drifts silently.
|
|
958
|
+
|
|
959
|
+
**Agent / MCP surface:** \`packages/mcp-server/src/tools.ts\` —
|
|
960
|
+
tool input schemas are hand-written. A new top-level Source field or a
|
|
961
|
+
new \`output_format\` needs a parameter added here. Element-field CRUD
|
|
962
|
+
validates against zod automatically.
|
|
963
|
+
|
|
964
|
+
**Situational:** \`apps/web/lib/playground/examples.ts\` (showcase the
|
|
965
|
+
new feature) and \`packages/snapshot-importer/\` (if it should map from
|
|
966
|
+
imported HTML/CSS).
|
|
967
|
+
|
|
968
|
+
The genuinely forgettable manual ones: **the runtime \`??\` fallback that
|
|
969
|
+
sets the field's default, the matching \`.describe()\` default text in
|
|
970
|
+
\`zod.ts\`, the \`content/docs/fields/*.md\` reference, and the MCP
|
|
971
|
+
\`embedded-docs.ts\` copy** — then run the registry probe.
|
|
972
|
+
`;
|
|
973
|
+
export const PROTOCOL_MD_CONTENT = `# The Clipkit Protocol
|
|
974
|
+
|
|
975
|
+
**Version:** 1.0 (CKP/1.0)
|
|
976
|
+
**Status:** Draft
|
|
977
|
+
**License:** Apache-2.0
|
|
978
|
+
|
|
979
|
+
This document specifies the Clipkit Protocol — the JSON-based interchange
|
|
980
|
+
format for describing motion-graphics videos. Documents that conform to
|
|
981
|
+
this protocol can be rendered by any conforming runtime: the reference
|
|
982
|
+
implementation \`@clipkit/runtime\`, future server-side renderers, or
|
|
983
|
+
third-party implementations.
|
|
984
|
+
|
|
985
|
+
The key words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, **MAY**,
|
|
986
|
+
and **REQUIRED** in this document are to be interpreted as described in
|
|
987
|
+
[RFC 2119](https://www.rfc-editor.org/rfc/rfc2119).
|
|
988
|
+
|
|
989
|
+
The reference implementation lives in [\`packages/protocol\`](./packages/protocol)
|
|
990
|
+
of this repository.
|
|
991
|
+
|
|
992
|
+
---
|
|
993
|
+
|
|
994
|
+
## Table of contents
|
|
995
|
+
|
|
996
|
+
1. [Introduction](#1-introduction)
|
|
997
|
+
2. [Document structure](#2-document-structure)
|
|
998
|
+
3. [Coordinate system, units, and types](#3-coordinate-system-units-and-types)
|
|
999
|
+
4. [The element model](#4-the-element-model)
|
|
1000
|
+
5. [Element types](#5-element-types)
|
|
1001
|
+
6. [Animation](#6-animation)
|
|
1002
|
+
7. [Time, duration, sequencing](#7-time-duration-sequencing)
|
|
1003
|
+
8. [Asset references](#8-asset-references)
|
|
1004
|
+
9. [Output and rendering](#9-output-and-rendering)
|
|
1005
|
+
10. [Conformance levels](#10-conformance-levels)
|
|
1006
|
+
11. [Versioning and extensions](#11-versioning-and-extensions)
|
|
1007
|
+
12. [Implementation notes](#12-implementation-notes-non-normative)
|
|
1008
|
+
|
|
1009
|
+
---
|
|
1010
|
+
|
|
1011
|
+
## 1. Introduction
|
|
1012
|
+
|
|
1013
|
+
### 1.1. Goals
|
|
1014
|
+
|
|
1015
|
+
The Clipkit Protocol aims to be:
|
|
1016
|
+
|
|
1017
|
+
- **JSON-native.** A Clipkit document is plain JSON. Any tool that can
|
|
1018
|
+
write JSON can produce one. AI agents in particular benefit from this
|
|
1019
|
+
— they emit structured data better than they emit code.
|
|
1020
|
+
- **Renderer-agnostic.** The protocol describes *what* a video looks
|
|
1021
|
+
like, not *how* to render it. A conforming runtime may use WebGPU,
|
|
1022
|
+
WebGL2, software rasterization, server-side headless browsers, or any
|
|
1023
|
+
other mechanism.
|
|
1024
|
+
- **Composable.** Documents are built from a small set of primitive
|
|
1025
|
+
element types. Higher-level patterns are authoring concerns — they
|
|
1026
|
+
produce primitive elements; the protocol itself has no notion of
|
|
1027
|
+
"pattern."
|
|
1028
|
+
- **Deterministic.** Given the same document and the same time, every
|
|
1029
|
+
conforming runtime MUST produce the same frame composition. Exact
|
|
1030
|
+
pixel output may differ between rasterization backends, but the
|
|
1031
|
+
scene description is unambiguous.
|
|
1032
|
+
|
|
1033
|
+
### 1.2. Non-goals
|
|
1034
|
+
|
|
1035
|
+
- **Visual editing format.** Editors MAY store additional state alongside
|
|
1036
|
+
a Source (selection, undo history, asset binaries) but that state is
|
|
1037
|
+
outside this protocol.
|
|
1038
|
+
- **Media container.** Clipkit references external media (images, video,
|
|
1039
|
+
audio) by URL or path. The protocol does not embed binary media.
|
|
1040
|
+
- **Audio mixing graph.** Audio elements are positioned in time; the
|
|
1041
|
+
exact mix algorithm (gains, codecs, bitrates) is implementation-defined.
|
|
1042
|
+
|
|
1043
|
+
### 1.3. Reference implementation
|
|
1044
|
+
|
|
1045
|
+
\`@clipkit/protocol\` is the canonical TypeScript + Zod implementation of
|
|
1046
|
+
this protocol. Other implementations SHOULD treat it as authoritative on
|
|
1047
|
+
ambiguous points until those points are resolved here.
|
|
1048
|
+
|
|
1049
|
+
---
|
|
1050
|
+
|
|
1051
|
+
## 2. Document structure
|
|
1052
|
+
|
|
1053
|
+
### 2.1. The Source object
|
|
1054
|
+
|
|
1055
|
+
A Clipkit document is a JSON object with the following shape:
|
|
1056
|
+
|
|
1057
|
+
\`\`\`json
|
|
1058
|
+
{
|
|
1059
|
+
"clipkit_version": "1.0",
|
|
1060
|
+
"output_format": "mp4",
|
|
1061
|
+
"width": 1920,
|
|
1062
|
+
"height": 1080,
|
|
1063
|
+
"duration": 30,
|
|
1064
|
+
"frame_rate": 30,
|
|
1065
|
+
"background_color": "#000000",
|
|
1066
|
+
"elements": [ /* ... */ ]
|
|
1067
|
+
}
|
|
1068
|
+
\`\`\`
|
|
1069
|
+
|
|
1070
|
+
#### Fields
|
|
1071
|
+
|
|
1072
|
+
| Field | Type | Required? | Default | Meaning |
|
|
1073
|
+
|---|---|---|---|---|
|
|
1074
|
+
| \`clipkit_version\` | string | SHOULD | \`"1.0"\` | The protocol version this document targets. See §11. |
|
|
1075
|
+
| \`output_format\` | string | MAY | \`"mp4"\` | One of \`"mp4"\` (video) or \`"gif"\` (animated). Clipkit is video-only. |
|
|
1076
|
+
| \`width\` | integer | SHOULD | \`1920\` | Composition width in pixels. MUST be positive. |
|
|
1077
|
+
| \`height\` | integer | SHOULD | \`1080\` | Composition height in pixels. MUST be positive. |
|
|
1078
|
+
| \`duration\` | number \\| \`"auto"\` | SHOULD | \`"auto"\` | Total composition duration in seconds. \`"auto"\` MUST be interpreted as the maximum end-time of any active element. |
|
|
1079
|
+
| \`frame_rate\` | number | SHOULD | \`30\` | Frames per second. MUST be positive. |
|
|
1080
|
+
| \`background_color\` | string | MAY | \`"#000000"\` | Color (§3.4) the frame is cleared to before any element draws. Absent → opaque black. |
|
|
1081
|
+
| \`motion_blur\` | object | MAY | — | Whole-frame motion blur by exact sub-frame supersampling. See "Motion blur" below. |
|
|
1082
|
+
| \`camera\` | object | MAY | — | Scene camera (CKP/1.0): perspective lens + movable pose (position/orientation) + \`sort\`. Absent = identity = exact 2D. See §4.4.2, §4.4.3. |
|
|
1083
|
+
| \`fonts\` | array | MAY | — | Font faces the renderer MUST register before rendering. See "Fonts" below. |
|
|
1084
|
+
| \`elements\` | array | REQUIRED | — | At least one element. See §4–5. |
|
|
1085
|
+
|
|
1086
|
+
#### Motion blur
|
|
1087
|
+
|
|
1088
|
+
\`motion_blur\` is an object with two optional fields:
|
|
1089
|
+
|
|
1090
|
+
| Field | Type | Default | Meaning |
|
|
1091
|
+
|---|---|---|---|
|
|
1092
|
+
| \`samples\` | integer 1–32 | \`8\` | Sub-frame samples rendered per output frame. \`1\` disables blur. |
|
|
1093
|
+
| \`shutter\` | number (0, 1] | \`0.5\` | Fraction of the frame interval the shutter is open. \`0.5\` corresponds to a 180° film shutter. |
|
|
1094
|
+
|
|
1095
|
+
When present with \`samples\` ≥ 2, the renderer MUST produce each output
|
|
1096
|
+
frame as the arithmetic mean of \`samples\` full-scene renders taken at
|
|
1097
|
+
sub-frame times centered on the frame time. For the output frame at
|
|
1098
|
+
time \`t\` with frame rate \`f\` and composition duration \`D\`:
|
|
1099
|
+
|
|
1100
|
+
\`\`\`
|
|
1101
|
+
t_k = clamp(t + ((k + 0.5) / samples − 0.5) × shutter / f, 0, D)
|
|
1102
|
+
for k = 0 … samples−1
|
|
1103
|
+
\`\`\`
|
|
1104
|
+
|
|
1105
|
+
Each sample is a complete render of the composition at \`t_k\` under the
|
|
1106
|
+
normal rendering model (every animated value, transition, and effect is
|
|
1107
|
+
evaluated at \`t_k\`). The output pixel is the per-channel arithmetic
|
|
1108
|
+
mean of the \`samples\` sample pixels in the output color space (8-bit
|
|
1109
|
+
sRGB, after compositing over the opaque background), rounded once,
|
|
1110
|
+
half away from zero. Accumulation MUST be carried at a precision that
|
|
1111
|
+
makes the result exact before that single rounding (e.g. float
|
|
1112
|
+
accumulation of 8-bit samples).
|
|
1113
|
+
|
|
1114
|
+
This is deterministic: the same document produces the same pixels.
|
|
1115
|
+
Media elements evaluate \`t_k\` through their own media-time mapping
|
|
1116
|
+
(§5.3.2); a video whose decoder quantizes to its own frame times
|
|
1117
|
+
contributes the same decoded frame to several samples, which is
|
|
1118
|
+
conformant.
|
|
1119
|
+
|
|
1120
|
+
Motion blur applies at export/render time. Interactive previews MAY
|
|
1121
|
+
render the unblurred scene (a single sample at \`t\`) for speed and MUST
|
|
1122
|
+
NOT be treated as the reference output when \`motion_blur\` is present.
|
|
1123
|
+
|
|
1124
|
+
#### Fonts
|
|
1125
|
+
|
|
1126
|
+
\`fonts\` is an array of font-face objects. The renderer MUST register
|
|
1127
|
+
every face before the first frame renders, so a document carrying its
|
|
1128
|
+
fonts is self-sufficient — text metrics don't depend on what the host
|
|
1129
|
+
environment happens to have installed.
|
|
1130
|
+
|
|
1131
|
+
| Field | Type | Required? | Default | Meaning |
|
|
1132
|
+
|---|---|---|---|---|
|
|
1133
|
+
| \`family\` | string | REQUIRED | — | The \`font_family\` name text elements reference. |
|
|
1134
|
+
| \`weight\` | number \\| string | MAY | \`"normal"\` | CSS font-weight. Variable fonts MAY declare a range (e.g. \`"100 900"\`). |
|
|
1135
|
+
| \`style\` | string | MAY | \`"normal"\` | \`"normal"\` or \`"italic"\`. |
|
|
1136
|
+
| \`src\` | string | REQUIRED | — | URL of the font bytes: absolute, relative (resolved against the document hosting the Source), or a \`data:\` URI. |
|
|
1137
|
+
| \`unicode_range\` | string | MAY | — | 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. |
|
|
1138
|
+
|
|
1139
|
+
Multiple entries MAY share a \`family\` (different weights, styles, or
|
|
1140
|
+
unicode-range subsets). Renderers MUST treat each entry as a distinct
|
|
1141
|
+
face, exactly as CSS treats multiple \`@font-face\` rules.
|
|
1142
|
+
|
|
1143
|
+
### 2.2. Forward compatibility
|
|
1144
|
+
|
|
1145
|
+
Documents MAY contain additional top-level fields not listed here.
|
|
1146
|
+
Conforming runtimes MUST ignore unknown top-level fields (passthrough).
|
|
1147
|
+
This applies to all objects in this protocol unless otherwise stated.
|
|
1148
|
+
|
|
1149
|
+
---
|
|
1150
|
+
|
|
1151
|
+
## 3. Coordinate system, units, and types
|
|
1152
|
+
|
|
1153
|
+
### 3.1. Coordinate system
|
|
1154
|
+
|
|
1155
|
+
- The origin \`(0, 0)\` is at the **top-left** of the composition.
|
|
1156
|
+
- The positive **x** axis runs **right**.
|
|
1157
|
+
- The positive **y** axis runs **down**.
|
|
1158
|
+
- Positions, sizes, and offsets are in pixels relative to the composition
|
|
1159
|
+
width / height unless an explicit unit suffix is used (§3.3).
|
|
1160
|
+
|
|
1161
|
+
### 3.2. Anchors
|
|
1162
|
+
|
|
1163
|
+
Most elements support \`x_anchor\` and \`y_anchor\` values in \`[0, 1]\`.
|
|
1164
|
+
These define which point inside the element's bounding box is positioned
|
|
1165
|
+
at \`(x, y)\`:
|
|
1166
|
+
|
|
1167
|
+
- \`0\` = left edge (or top edge) — **the default**
|
|
1168
|
+
- \`0.5\` = center
|
|
1169
|
+
- \`1\` = right edge (or bottom edge)
|
|
1170
|
+
|
|
1171
|
+
By default both anchors are \`0\`, so \`(x, y)\` is the element's **top-left
|
|
1172
|
+
corner** — the CSS \`left\`/\`top\` / SVG / Canvas convention. For example,
|
|
1173
|
+
\`x: 960\` (anchor \`0\`) places the element's left edge at x=960;
|
|
1174
|
+
\`x: 960, x_anchor: 0.5\` places its center at x=960. The anchor only
|
|
1175
|
+
moves where the box sits — rotation and scale always pivot the element's
|
|
1176
|
+
geometric center (§4.4), independent of the anchor.
|
|
1177
|
+
|
|
1178
|
+
### 3.3. Length values
|
|
1179
|
+
|
|
1180
|
+
Wherever a position or dimension is accepted, a string with one of the
|
|
1181
|
+
following unit suffixes MAY be used:
|
|
1182
|
+
|
|
1183
|
+
| Unit | Meaning |
|
|
1184
|
+
|---|---|
|
|
1185
|
+
| \`"100px"\` | pixels (same as bare number \`100\`) |
|
|
1186
|
+
| \`"50%"\` | percentage of the property's natural reference (width → composition width; etc.) |
|
|
1187
|
+
| \`"10vw"\` | percentage of composition width |
|
|
1188
|
+
| \`"15vh"\` | percentage of composition height |
|
|
1189
|
+
| \`"5vmin"\` | percentage of the smaller composition dimension |
|
|
1190
|
+
| \`"5vmax"\` | percentage of the larger composition dimension |
|
|
1191
|
+
|
|
1192
|
+
A bare number is interpreted as pixels.
|
|
1193
|
+
|
|
1194
|
+
> **Divergence from CSS:** \`%\` and the viewport units resolve against the
|
|
1195
|
+
> **composition (canvas)**, never the parent element. \`"50%"\` is always
|
|
1196
|
+
> half the canvas, even on a child nested inside a group. There is no
|
|
1197
|
+
> parent-relative percentage; use pixels for child-relative sizing inside
|
|
1198
|
+
> groups.
|
|
1199
|
+
|
|
1200
|
+
### 3.4. Color values
|
|
1201
|
+
|
|
1202
|
+
Colors are CSS-style strings. The reference runtime accepts:
|
|
1203
|
+
- hex — \`"#rgb"\`, \`"#rgba"\`, \`"#rrggbb"\`, \`"#rrggbbaa"\`
|
|
1204
|
+
- \`rgb()\` / \`rgba()\` — comma or space separated, alpha as 0..1 or \`%\`
|
|
1205
|
+
- \`hsl()\` / \`hsla()\` — same separators, optional \`deg\` on the hue
|
|
1206
|
+
- the 148 CSS named colors (\`"red"\`, \`"rebeccapurple"\`, …) and
|
|
1207
|
+
\`"transparent"\`
|
|
1208
|
+
|
|
1209
|
+
Unrecognized strings fall back to white. (Conformance: only hex support
|
|
1210
|
+
is REQUIRED of a runtime; the rest are RECOMMENDED for CSS parity.)
|
|
1211
|
+
|
|
1212
|
+
Internally, all colors flow through rendering as straight-alpha sRGB,
|
|
1213
|
+
premultiplied at the final composition step. Authors do not need to
|
|
1214
|
+
think about this; it is mentioned to document the reference runtime's
|
|
1215
|
+
behavior.
|
|
1216
|
+
|
|
1217
|
+
### 3.5. Keyframe values
|
|
1218
|
+
|
|
1219
|
+
Many properties accept either a static value or a \`Keyframe[]\` array
|
|
1220
|
+
for animation. The keyframe form is:
|
|
1221
|
+
|
|
1222
|
+
\`\`\`json
|
|
1223
|
+
[
|
|
1224
|
+
{ "time": 0, "value": 0, "easing": "ease-out-cubic" },
|
|
1225
|
+
{ "time": 1.0, "value": 100, "easing": "ease-out-cubic" }
|
|
1226
|
+
]
|
|
1227
|
+
\`\`\`
|
|
1228
|
+
|
|
1229
|
+
\`time\` is in seconds, relative to the element's \`time\` (see §7).
|
|
1230
|
+
\`value\` can be a number, a string, or \`[x, y]\` for position-like
|
|
1231
|
+
properties. \`easing\` is OPTIONAL; see §6.4.
|
|
1232
|
+
|
|
1233
|
+
### 3.6. Expression values
|
|
1234
|
+
|
|
1235
|
+
Any **numeric** property (transform, \`opacity\`, blur/\`brightness\`/
|
|
1236
|
+
\`contrast\`/\`saturation\`, effect params, camera/light numbers — every
|
|
1237
|
+
property whose type admits a number) MAY instead be given as an
|
|
1238
|
+
**expression object**: a closed-form, deterministic function of the
|
|
1239
|
+
element's own clock.
|
|
1240
|
+
|
|
1241
|
+
\`\`\`json
|
|
1242
|
+
{ "y": { "expr": "540 + sin(t * PI) * 30" } }
|
|
1243
|
+
{ "rotation": { "expr": "t * 90" } }
|
|
1244
|
+
{ "x": { "expr": "960 + wiggle(3, 12)" } }
|
|
1245
|
+
\`\`\`
|
|
1246
|
+
|
|
1247
|
+
An expression is a **pure function of element-local time and the
|
|
1248
|
+
element's own index** — nothing else. This is the entire safety
|
|
1249
|
+
boundary, and it is NORMATIVE:
|
|
1250
|
+
|
|
1251
|
+
- It MUST NOT reference any other element (\`ref()\`, \`valueAtTime\`) or
|
|
1252
|
+
read any runtime input (mouse, audio). Those are Tier-B and are
|
|
1253
|
+
permanently unsupported.
|
|
1254
|
+
- It MUST be deterministic: every conforming runtime MUST produce
|
|
1255
|
+
identical results for the same expression and clock. \`noise\`,
|
|
1256
|
+
\`wiggle\`, and \`random\` derive from the protocol's normative
|
|
1257
|
+
value-noise hash (bit-stable across runtimes; seed defaults to \`0\`,
|
|
1258
|
+
never to wall-clock).
|
|
1259
|
+
- Because it is a function of \`t\`/\`i\`, it is **bakeable**: a runtime or
|
|
1260
|
+
tool MAY sample it to a \`Keyframe[]\` at any frame rate. A keyframe
|
|
1261
|
+
table and an expression are two encodings of the same value.
|
|
1262
|
+
|
|
1263
|
+
**Scope** — the only identifiers in scope:
|
|
1264
|
+
|
|
1265
|
+
| Variable | Meaning |
|
|
1266
|
+
|---|---|
|
|
1267
|
+
| \`t\` | element-local time, seconds (\`0\` at the element's \`time\`) |
|
|
1268
|
+
| \`dur\` | element duration, seconds |
|
|
1269
|
+
| \`i\` | element index within its generated/particle set (default \`0\`) |
|
|
1270
|
+
| \`n\` | sibling count (default \`1\`) |
|
|
1271
|
+
| \`value\` | the property's base value (its documented default) |
|
|
1272
|
+
|
|
1273
|
+
Constants: \`PI\`, \`TAU\`, \`E\`. Functions (the ONLY callable identifiers):
|
|
1274
|
+
|
|
1275
|
+
\`\`\`
|
|
1276
|
+
sin cos tan asin acos atan atan2 sinh cosh tanh
|
|
1277
|
+
abs sign sqrt cbrt pow exp log log2 floor ceil round trunc fract
|
|
1278
|
+
min max mod hypot
|
|
1279
|
+
clamp(x,lo,hi) lerp(a,b,u) mix(=lerp) step(edge,x) smoothstep(e0,e1,x)
|
|
1280
|
+
linear(x,x0,x1,y0,y1) // map x∈[x0,x1] → [y0,y1], clamped
|
|
1281
|
+
ease(x,x0,x1,y0,y1) // same, cubic in-out
|
|
1282
|
+
noise(x[,seed]) // value noise, ∈[-1,1]
|
|
1283
|
+
wiggle(freq,amp[,seed]) // amp · fractal noise(t·freq)
|
|
1284
|
+
random(seed) // deterministic [0,1) hash, time-independent
|
|
1285
|
+
\`\`\`
|
|
1286
|
+
|
|
1287
|
+
Operators: \`+ - * / % ^\` (\`^\` = exponent, right-assoc), unary \`-\`,
|
|
1288
|
+
comparisons \`< > <= >= == !=\`, logical \`&& || !\`, and the ternary
|
|
1289
|
+
\`cond ? a : b\`. Nothing else — no member access, assignment, indexing,
|
|
1290
|
+
statements, or string values.
|
|
1291
|
+
|
|
1292
|
+
**Evaluation (NORMATIVE).** A conforming runtime MUST evaluate
|
|
1293
|
+
expressions with a restricted parser/evaluator, NOT a general
|
|
1294
|
+
code-execution facility (\`eval\` / \`Function\`). Any unknown identifier or
|
|
1295
|
+
function, member access, assignment, or string literal is a parse error;
|
|
1296
|
+
a parse error or a non-finite (\`NaN\` / \`±∞\`) result MUST fall back to the
|
|
1297
|
+
property's base value. Expressions are numeric-only in this version;
|
|
1298
|
+
string/text expressions are reserved.
|
|
1299
|
+
|
|
1300
|
+
---
|
|
1301
|
+
|
|
1302
|
+
## 4. The element model
|
|
1303
|
+
|
|
1304
|
+
Every element extends a common base:
|
|
1305
|
+
|
|
1306
|
+
\`\`\`ts
|
|
1307
|
+
interface BaseElement {
|
|
1308
|
+
id?: string;
|
|
1309
|
+
name?: string;
|
|
1310
|
+
type: ElementType; // discriminator
|
|
1311
|
+
layer: number; // REQUIRED, unique per container; 1..1000; LOWER draws in front (layer 1 = on top)
|
|
1312
|
+
time?: number | string; // seconds
|
|
1313
|
+
duration?: number | string | "auto" | "end";
|
|
1314
|
+
|
|
1315
|
+
// Transform (numbers OR length strings OR Keyframe[])
|
|
1316
|
+
x?: number | string | Keyframe[];
|
|
1317
|
+
y?: number | string | Keyframe[];
|
|
1318
|
+
x_anchor?: number | string;
|
|
1319
|
+
y_anchor?: number | string;
|
|
1320
|
+
width?: number | string | Keyframe[];
|
|
1321
|
+
height?: number | string | Keyframe[];
|
|
1322
|
+
rotation?: number | Keyframe[]; // degrees, around center
|
|
1323
|
+
scale?: number | Keyframe[];
|
|
1324
|
+
|
|
1325
|
+
// Visual
|
|
1326
|
+
opacity?: number | Keyframe[]; // 0..1 (CSS convention), default 1
|
|
1327
|
+
visible?: boolean; // false skips rendering (§4.2)
|
|
1328
|
+
blend_mode?: 'normal' | 'multiply' | 'screen' | 'add' | 'overlay' | 'hard-light' | 'soft-light'; // §4.5
|
|
1329
|
+
blur_radius?: number | Keyframe[]; // Gaussian σ in px (§4.6)
|
|
1330
|
+
brightness?: number | Keyframe[]; // multiplier, 1 = unchanged (§4.6)
|
|
1331
|
+
contrast?: number | Keyframe[]; // multiplier, 1 = unchanged (§4.6)
|
|
1332
|
+
saturation?: number | Keyframe[]; // multiplier, 1 = unchanged (§4.6)
|
|
1333
|
+
hue_rotate?: number | Keyframe[]; // degrees, default 0 (§4.6)
|
|
1334
|
+
effects?: Effect[]; // ordered stylize passes (§4.7)
|
|
1335
|
+
material?: Material; // PBR material; only shaded under scene lights (§4.8)
|
|
1336
|
+
|
|
1337
|
+
// Animation
|
|
1338
|
+
animations?: Animation[];
|
|
1339
|
+
keyframe_animations?: KeyframeAnimation[];
|
|
1340
|
+
}
|
|
1341
|
+
\`\`\`
|
|
1342
|
+
|
|
1343
|
+
### 4.1. The \`type\` discriminator
|
|
1344
|
+
|
|
1345
|
+
\`type\` is REQUIRED and identifies which variant the object is. Conforming
|
|
1346
|
+
runtimes MUST recognize the values defined in §5. Documents containing
|
|
1347
|
+
unknown \`type\` values are valid documents; runtimes MAY skip such
|
|
1348
|
+
elements with a warning rather than failing the whole document.
|
|
1349
|
+
|
|
1350
|
+
### 4.2. Draw order
|
|
1351
|
+
|
|
1352
|
+
Every element owns a **\`layer\`** — a unique integer 1..1000 within its
|
|
1353
|
+
container (the top-level \`elements\`, each group's \`elements\`, each group
|
|
1354
|
+
mask's \`elements\`), like an After Effects layer. **Lower \`layer\` draws
|
|
1355
|
+
in front; layer 1 is on top.** Elements draw back-to-front by **depth**
|
|
1356
|
+
(\`z\`, §4.4), then by \`layer\`:
|
|
1357
|
+
|
|
1358
|
+
\`\`\`
|
|
1359
|
+
draw_key = (depth descending, i.e. farther first), then layer descending
|
|
1360
|
+
(highest layer drawn first/behind, layer 1 drawn last/on top)
|
|
1361
|
+
\`\`\`
|
|
1362
|
+
|
|
1363
|
+
\`z\` is the single depth axis (§4.4.2): with no camera it is pure
|
|
1364
|
+
stacking order (no perspective), with a camera it additionally
|
|
1365
|
+
foreshortens. \`layer\` orders elements *within* equal depth — when depths
|
|
1366
|
+
are equal (e.g. a 2D document where every \`z\` is 0), draw order is
|
|
1367
|
+
exactly layer descending, so layer 1 ends up on top. \`layer\` is
|
|
1368
|
+
**required and unique per container** (duplicate layers are a validation
|
|
1369
|
+
error), so no two elements tie; the sort MUST nonetheless be stable. The
|
|
1370
|
+
same rule applies to a group's children, locally within the group.
|
|
1371
|
+
(There is no separate \`z_index\` field — depth ordering is unified onto
|
|
1372
|
+
\`z\`. \`camera.sort: 'paint'\` opts a camera composition back into fixed
|
|
1373
|
+
\`layer\` order; see §4.4.3.)
|
|
1374
|
+
|
|
1375
|
+
Elements with \`visible: false\` MUST NOT be rendered. Inactive elements
|
|
1376
|
+
(§7.1) MUST NOT be rendered.
|
|
1377
|
+
|
|
1378
|
+
### 4.3. Anchors and bounding boxes
|
|
1379
|
+
|
|
1380
|
+
The element's bounding box is \`width × height\` pixels, with its
|
|
1381
|
+
anchor point at \`(x, y)\`. The visual center MUST be computed
|
|
1382
|
+
from the anchor:
|
|
1383
|
+
|
|
1384
|
+
\`\`\`
|
|
1385
|
+
center_x = x + (0.5 - x_anchor) * width
|
|
1386
|
+
center_y = y + (0.5 - y_anchor) * height
|
|
1387
|
+
\`\`\`
|
|
1388
|
+
|
|
1389
|
+
Rotation rotates the element around its center.
|
|
1390
|
+
|
|
1391
|
+
### 4.4. Transform composition
|
|
1392
|
+
|
|
1393
|
+
Every element supports, in addition to position and rotation:
|
|
1394
|
+
|
|
1395
|
+
| Field | Type | Default | Meaning |
|
|
1396
|
+
|---|---|---|---|
|
|
1397
|
+
| \`scale\` | number | \`1\` | Uniform scale factor. |
|
|
1398
|
+
| \`x_scale\`, \`y_scale\` | number \\| \`"N%"\` | \`1\` | Per-axis scale factors; \`"150%"\` ≡ \`1.5\`. |
|
|
1399
|
+
| \`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. |
|
|
1400
|
+
|
|
1401
|
+
Since CKP/1.0, every element also supports a 3D transform:
|
|
1402
|
+
|
|
1403
|
+
| Field | Type | Default | Meaning |
|
|
1404
|
+
|---|---|---|---|
|
|
1405
|
+
| \`z_rotation\` | number (degrees) \\| Keyframe[] | \`0\` | Rotation in the element's plane. Exact alias for \`rotation\` — authoring BOTH on one element MUST be rejected by validators. |
|
|
1406
|
+
| \`x_rotation\` | number (degrees) \\| Keyframe[] | \`0\` | Rotation around the element's local x axis; positive tips the top edge away from the viewer. |
|
|
1407
|
+
| \`y_rotation\` | number (degrees) \\| Keyframe[] | \`0\` | Rotation around the element's local y axis; positive turns the right edge away from the viewer. |
|
|
1408
|
+
| \`z\` | number (px) \\| Keyframe[] | \`0\` | Depth toward (+) / away from (−) the viewer. The depth axis for §4.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. |
|
|
1409
|
+
|
|
1410
|
+
All are animatable. The effective axis scales are
|
|
1411
|
+
\`sx = scale × x_scale\`, \`sy = scale × y_scale\`.
|
|
1412
|
+
|
|
1413
|
+
#### 4.4.1. The local matrix (normative)
|
|
1414
|
+
|
|
1415
|
+
An element's local transform is the 4×4 matrix, in pixels, with the
|
|
1416
|
+
anchor-derived center \`(cx, cy)\` (§4.3) as the pivot:
|
|
1417
|
+
|
|
1418
|
+
\`\`\`
|
|
1419
|
+
M_local = T(cx, cy, z) · Rz(z_rotation) · Ry(y_rotation) · Rx(x_rotation)
|
|
1420
|
+
· K(x_skew, y_skew) · S(sx, sy, 1) · T(−cx, −cy, 0)
|
|
1421
|
+
\`\`\`
|
|
1422
|
+
|
|
1423
|
+
where \`Rx/Ry/Rz\` are the standard right-handed rotation matrices in
|
|
1424
|
+
the protocol's coordinate system (x right, y DOWN, z toward the
|
|
1425
|
+
viewer; positive \`z_rotation\` is therefore clockwise on screen, as in
|
|
1426
|
+
CKP/1.0), and \`K\` embeds the 2D shear \`[[1, tan(x_skew)],
|
|
1427
|
+
[tan(y_skew), 1]]\` (CSS \`skew(x, y)\` semantics) in the xy plane. With
|
|
1428
|
+
\`x_rotation = y_rotation = z = 0\` this reduces exactly to the CKP/1.0
|
|
1429
|
+
composition **scale → shear → rotate** around the anchor-derived
|
|
1430
|
+
center; documents without 3D fields MUST render identically under
|
|
1431
|
+
both models.
|
|
1432
|
+
|
|
1433
|
+
Group transforms stack multiplicatively onto children:
|
|
1434
|
+
\`M = M_parent · M_local\`, evaluated in the group's local space, and
|
|
1435
|
+
opacities multiply down the group chain. Children of a group that
|
|
1436
|
+
does NOT rasterize to a layer (no \`clip\`, \`mask\`, filter fields, or
|
|
1437
|
+
\`effects\` — see §4.4.3) therefore live in the parent's 3D space:
|
|
1438
|
+
nested 3D rotations compose, with no opt-in flag.
|
|
1439
|
+
|
|
1440
|
+
#### 4.4.2. The camera
|
|
1441
|
+
|
|
1442
|
+
A Source MAY declare one scene camera — a perspective lens (\`perspective\`,
|
|
1443
|
+
\`origin_x\`, \`origin_y\`) plus an optional rigid **pose** (a position and a
|
|
1444
|
+
look orientation) that moves the viewpoint through the scene:
|
|
1445
|
+
|
|
1446
|
+
\`\`\`ts
|
|
1447
|
+
camera?: {
|
|
1448
|
+
perspective: number | Keyframe[]; // focal distance in px, > 0
|
|
1449
|
+
origin_x?: number | string; // default "50%" (canvas center)
|
|
1450
|
+
origin_y?: number | string; // default "50%"
|
|
1451
|
+
|
|
1452
|
+
// Eye position offset from the default eye, px, about the origin. Default 0.
|
|
1453
|
+
x?: number | Keyframe[]; // +x right
|
|
1454
|
+
y?: number | Keyframe[]; // +y down
|
|
1455
|
+
z?: number | Keyframe[]; // +z = eye toward the scene (dolly in)
|
|
1456
|
+
|
|
1457
|
+
// Eye orientation, degrees, Euler (applied Rz·Ry·Rx). Default 0.
|
|
1458
|
+
x_rotation?: number | Keyframe[]; // pitch (tilt)
|
|
1459
|
+
y_rotation?: number | Keyframe[]; // yaw (pan)
|
|
1460
|
+
z_rotation?: number | Keyframe[]; // roll
|
|
1461
|
+
|
|
1462
|
+
sort?: 'depth' | 'paint'; // compositing order, §4.4.3. Default 'depth'.
|
|
1463
|
+
}
|
|
1464
|
+
\`\`\`
|
|
1465
|
+
|
|
1466
|
+
The lens follows CSS Transforms Module Level 2 \`perspective()\`
|
|
1467
|
+
semantics: with \`d = perspective\` and origin \`(ox, oy)\`,
|
|
1468
|
+
|
|
1469
|
+
\`\`\`
|
|
1470
|
+
P = T(ox, oy, 0) · [ 1 0 0 0 ; 0 1 0 0 ; 0 0 1 0 ; 0 0 −1/d 1 ] · T(−ox, −oy, 0)
|
|
1471
|
+
\`\`\`
|
|
1472
|
+
|
|
1473
|
+
The pose is the **inverse of the camera's rigid world transform**, taken
|
|
1474
|
+
about the origin so it composes with the lens. With eye position
|
|
1475
|
+
\`(x, y, z)\` and rotation \`R = Rz·Ry·Rx\`,
|
|
1476
|
+
|
|
1477
|
+
\`\`\`
|
|
1478
|
+
V = T(ox, oy, 0) · R⁻¹ · T(−x, −y, −z) · T(−ox, −oy, 0)
|
|
1479
|
+
\`\`\`
|
|
1480
|
+
|
|
1481
|
+
and the camera matrix applied once at the root is
|
|
1482
|
+
|
|
1483
|
+
\`\`\`
|
|
1484
|
+
camera = P · V
|
|
1485
|
+
\`\`\`
|
|
1486
|
+
|
|
1487
|
+
so every element renders through \`P · V · M_chain · M_local\`, followed
|
|
1488
|
+
by the perspective divide. Orientation is given as explicit Euler angles
|
|
1489
|
+
rather than a look-at target: a target would derive orientation from
|
|
1490
|
+
geometry (hidden runtime math); a "look at this point" gesture is an
|
|
1491
|
+
authoring convenience that resolves to these angles, not a schema field.
|
|
1492
|
+
|
|
1493
|
+
**Identity reduction (normative).** With the pose at its defaults
|
|
1494
|
+
(\`x=y=z=0\`, all rotations \`0\`), \`V = I\` and \`camera = P\` **bit-for-bit**:
|
|
1495
|
+
a document that uses only \`perspective\`/origin renders identically to a
|
|
1496
|
+
pre-pose runtime. Smaller \`d\` = stronger foreshortening; \`perspective\`
|
|
1497
|
+
is animatable (camera push/pull), as are all pose fields (orbit, pan,
|
|
1498
|
+
tilt, dolly).
|
|
1499
|
+
|
|
1500
|
+
**No camera ⇒ camera = I.** 3D rotations still render (affine
|
|
1501
|
+
foreshortening — a y-rotated card narrows but its edges stay parallel).
|
|
1502
|
+
\`z\` has no *perspective* effect without a camera, but it still **orders**
|
|
1503
|
+
(§4.2, §4.4.3): \`z\` is the single depth axis, so without a camera it acts
|
|
1504
|
+
as pure stacking order. A document with no 3D fields (all \`z = 0\`) and no
|
|
1505
|
+
camera renders as pure layer stacking — equal depth collapses to \`layer\`
|
|
1506
|
+
order (descending, so layer 1 is on top).
|
|
1507
|
+
|
|
1508
|
+
#### 4.4.3. Compositing under 3D (normative)
|
|
1509
|
+
|
|
1510
|
+
- **Paint order — depth (2.5D).** \`z\` is the single depth axis and it
|
|
1511
|
+
orders **always**: each sibling draw list (the top-level elements, and
|
|
1512
|
+
the children of each non-flattened group) is painted **back-to-front
|
|
1513
|
+
by depth** — the eye-space \`z\` of the sibling's anchor (its \`z\` with no
|
|
1514
|
+
camera; \`z\` after \`P · V\` under a camera), far cards first. With **no
|
|
1515
|
+
camera** this is pure stacking with no perspective; with a **camera**
|
|
1516
|
+
it is the same ordering plus foreshortening. This is whole-card
|
|
1517
|
+
(per-element) 2.5D sorting — flat cards ordered by distance — NOT a
|
|
1518
|
+
per-pixel depth buffer. The sort is **stable**: equal depths break by
|
|
1519
|
+
\`layer\` order (descending — layer 1 drawn last/on top), so a document
|
|
1520
|
+
where every \`z = 0\` collapses to exact \`layer\` order, and the same
|
|
1521
|
+
Source always yields the same pixels. A flattened group (clip / mask / filters /
|
|
1522
|
+
effects) is a single card and sorts by its own anchor depth as a
|
|
1523
|
+
unit; the §4.4.3 flattening rule means its children are already
|
|
1524
|
+
coplanar inside the flat layer and keep their in-layer order. An
|
|
1525
|
+
element's own internal quads (a particle system's particles, a text
|
|
1526
|
+
element's glyphs) are likewise NOT reached by this sort — they follow
|
|
1527
|
+
their own documented order (e.g. particles in spawn order, §5.13).
|
|
1528
|
+
**Limitation (normative, not a bug):** because the unit of sorting is
|
|
1529
|
+
the whole card, cards that interpenetrate, or whose anchor-depth order
|
|
1530
|
+
disagrees with their true per-pixel order, can be ordered "wrong" at
|
|
1531
|
+
some camera angles. There is no per-pixel resolution in the 2.5D
|
|
1532
|
+
model. \`sort: 'paint'\` opts a camera composition back into fixed
|
|
1533
|
+
\`layer\` order for authors who want explicit, camera-stable
|
|
1534
|
+
layering. There is no depth buffer in the rendering model.
|
|
1535
|
+
- **Flattening at layer boundaries.** A group with \`clip: true\` or
|
|
1536
|
+
\`mask\` renders its children in its own flat 2D layer space exactly
|
|
1537
|
+
as in CKP/1.0, and the finished layer's quad is then transformed by
|
|
1538
|
+
the full matrix chain. 3D declared INSIDE such a subtree composes
|
|
1539
|
+
only within that flat layer's plane; 3D declared ON or ABOVE it
|
|
1540
|
+
projects the layer as a unit. (This is how a clipped UI-mock group
|
|
1541
|
+
tilts as one card.)
|
|
1542
|
+
- **Effect surfaces are screen-space.** Filter fields (\`blur_radius\`,
|
|
1543
|
+
\`brightness\`, \`contrast\`, \`saturation\`, \`hue_rotate\`) and \`effects\`
|
|
1544
|
+
entries evaluate on the element's PROJECTED rendering: the element
|
|
1545
|
+
(or group subtree) draws with its full transform — including 3D and
|
|
1546
|
+
the camera — into a surface-sized layer, and the filter/effect chain
|
|
1547
|
+
runs on those screen-space pixels. This matches CKP/1.0, where the
|
|
1548
|
+
element's own transform is likewise baked into its effect surface
|
|
1549
|
+
before filtering, and means e.g. a glow's radius or a stroke's width
|
|
1550
|
+
is uniform in screen pixels on a tilted card. A shape's native
|
|
1551
|
+
\`shadow\` foreshortens with the element's plane, but its
|
|
1552
|
+
\`offset_x\`/\`offset_y\` translate in the PARENT plane — consistent
|
|
1553
|
+
with CKP/1.0, where a rotated shape's shadow offset stays
|
|
1554
|
+
screen-aligned.
|
|
1555
|
+
- **Glass.** Glass is legal under 3D. The pane is a true plane in the
|
|
1556
|
+
scene: pane-local coordinates come from the inverse of the pane's
|
|
1557
|
+
plane homography (the restriction of the full §4.4 matrix chain to
|
|
1558
|
+
the pane's plane — equivalent to exact per-fragment ray/plane
|
|
1559
|
+
intersection), the §4.7 optical model runs unchanged in that local
|
|
1560
|
+
frame, and refracted sample points map FORWARD through the
|
|
1561
|
+
homography onto the screen-space backdrop snapshot. See §4.7 "Glass
|
|
1562
|
+
under 3D" for the normative model and degenerate cases. With no 3D
|
|
1563
|
+
on the element or its un-flattened chain, the orthographic CKP/1.0
|
|
1564
|
+
path applies bit-for-bit.
|
|
1565
|
+
- **Anti-aliasing.** SDF edge anti-aliasing remains derivative-based
|
|
1566
|
+
and scales naturally under projection; the §1 cross-backend
|
|
1567
|
+
tolerance language applies unchanged.
|
|
1568
|
+
|
|
1569
|
+
### 4.5. Blend modes
|
|
1570
|
+
|
|
1571
|
+
\`blend_mode\` selects how the element's pixels combine with the pixels
|
|
1572
|
+
already drawn beneath it. It is **element-local**: it changes only this
|
|
1573
|
+
element's compositing math and MUST NOT alter how any other element
|
|
1574
|
+
renders. With premultiplied sources:
|
|
1575
|
+
|
|
1576
|
+
| Value | Color math | Character |
|
|
1577
|
+
|---|---|---|
|
|
1578
|
+
| \`normal\` (default) | \`out = src + dst·(1 − src.a)\` | Standard over. |
|
|
1579
|
+
| \`multiply\` | \`out = src·dst + dst·(1 − src.a)\` | Darkens; white is neutral; uncovered pixels leave the destination unchanged. |
|
|
1580
|
+
| \`screen\` | \`out = src + dst·(1 − src)\` | Lightens; black is neutral. |
|
|
1581
|
+
| \`add\` | \`out = src + dst\` | Linear dodge; overlaps sum toward white (glow). |
|
|
1582
|
+
| \`overlay\` | \`B(cb,cs)\` = \`2·cb·cs\` if \`cb ≤ 0.5\` else \`1 − 2·(1−cb)·(1−cs)\` | Multiply in dark backdrop areas, screen in light ones; boosts contrast. |
|
|
1583
|
+
| \`hard-light\` | \`B(cb,cs)\` = overlay with source and backdrop swapped | Like shining a harsh light through the source. |
|
|
1584
|
+
| \`soft-light\` | \`B(cb,cs)\` per W3C soft-light | A gentler \`hard-light\` (soft dodge/burn). |
|
|
1585
|
+
|
|
1586
|
+
The blend function \`B(cb, cs)\` operates on **straight-alpha** (un-
|
|
1587
|
+
premultiplied) backdrop \`cb\` and source \`cs\` per channel; the result
|
|
1588
|
+
composites via the general separable formula
|
|
1589
|
+
\`co = αs·(1−αb)·cs + αs·αb·B(cb,cs) + (1−αs)·αb·cb\`, with
|
|
1590
|
+
\`αo = αs + αb·(1 − αs)\`. For \`normal\`/\`multiply\`/\`screen\`/\`add\` this
|
|
1591
|
+
reduces to the closed forms above, expressible with fixed-function
|
|
1592
|
+
blending. \`overlay\`/\`hard-light\`/\`soft-light\` are **piecewise on the
|
|
1593
|
+
backdrop (or source)** and cannot be; a conforming runtime isolates
|
|
1594
|
+
the element to its own layer and composites it against a snapshot of
|
|
1595
|
+
the backdrop. The alpha channel always composites normally, so
|
|
1596
|
+
coverage is unaffected by the mode.
|
|
1597
|
+
|
|
1598
|
+
On a \`group\`, \`blend_mode\` applies when the group's flattened layer is
|
|
1599
|
+
composited — which only exists when the group is layered via
|
|
1600
|
+
\`clip: true\` or \`mask\` (§5.8). On an unlayered group the field MUST be
|
|
1601
|
+
ignored (children draw directly to the parent surface with their own
|
|
1602
|
+
modes); runtimes SHOULD warn. Children inside a layered group
|
|
1603
|
+
composite against each other inside the layer, isolated from the
|
|
1604
|
+
backdrop — matching CSS \`isolation: isolate\` semantics.
|
|
1605
|
+
|
|
1606
|
+
### 4.6. Filters
|
|
1607
|
+
|
|
1608
|
+
Four element-local filter fields, all animatable, all following CSS
|
|
1609
|
+
\`filter\` function semantics:
|
|
1610
|
+
|
|
1611
|
+
| Field | Default | Meaning |
|
|
1612
|
+
|---|---|---|
|
|
1613
|
+
| \`blur_radius\` | \`0\` | Gaussian blur; the value is the standard deviation σ in canvas pixels (CSS \`blur(σ)\`). |
|
|
1614
|
+
| \`brightness\` | \`1\` | Color multiplier: \`c' = c × v\` (CSS \`brightness(v)\`). |
|
|
1615
|
+
| \`contrast\` | \`1\` | Scale around mid-gray: \`c' = (c − 0.5) × v + 0.5\` (CSS \`contrast(v)\`). |
|
|
1616
|
+
| \`saturation\` | \`1\` | Lerp against Rec. 709 luma: \`c' = mix(luma(c), c, v)\`; \`0\` = grayscale (CSS \`saturate(v)\`). |
|
|
1617
|
+
| \`hue_rotate\` | \`0\` | Hue rotation by \`v\` DEGREES: \`c' = M(v) × c\` with the SVG \`feColorMatrix type="hueRotate"\` matrix below (CSS \`hue-rotate(v)\`). |
|
|
1618
|
+
|
|
1619
|
+
The \`hue_rotate\` matrix, NORMATIVE, with \`cosθ\`/\`sinθ\` of the angle:
|
|
1620
|
+
|
|
1621
|
+
\`\`\`
|
|
1622
|
+
M = [ 0.213+0.787cosθ−0.213sinθ 0.715−0.715cosθ−0.715sinθ 0.072−0.072cosθ+0.928sinθ ]
|
|
1623
|
+
[ 0.213−0.213cosθ+0.143sinθ 0.715+0.285cosθ+0.140sinθ 0.072−0.072cosθ−0.283sinθ ]
|
|
1624
|
+
[ 0.213−0.213cosθ−0.787sinθ 0.715−0.715cosθ+0.715sinθ 0.072+0.928cosθ+0.072sinθ ]
|
|
1625
|
+
\`\`\`
|
|
1626
|
+
|
|
1627
|
+
Blur evaluation is a NORMATIVE downsample ladder (so identical sources
|
|
1628
|
+
produce identical pixels everywhere): bilinearly halve the image until
|
|
1629
|
+
the residual \`σ / f ≤ 4\` (\`f\` a power of two, max 16), apply a
|
|
1630
|
+
25-tap Gaussian (taps at \`σ/f ÷ 4\` spacing over ±3σ, weights
|
|
1631
|
+
\`exp(−d²/2σ²)\` normalized by their sum) horizontally then vertically
|
|
1632
|
+
at the reduced size, and bilinearly upsample at the consuming draw.
|
|
1633
|
+
Sparse full-resolution taps are not an acceptable substitute — they
|
|
1634
|
+
leave a visible σ/4-pixel grid on hard edges.
|
|
1635
|
+
|
|
1636
|
+
A filtered element — any type, \`group\` included, layered or not — is
|
|
1637
|
+
rendered with its normal transform into a transparent offscreen layer,
|
|
1638
|
+
then that layer is composited back through the filter. Filters MUST
|
|
1639
|
+
apply in the order **blur → brightness → contrast → saturation →
|
|
1640
|
+
hue_rotate**, and
|
|
1641
|
+
the color ops MUST operate on straight (unpremultiplied) color so
|
|
1642
|
+
translucent pixels don't skew toward the contrast midpoint. Channel
|
|
1643
|
+
results clamp to [0, 1] before re-premultiplying; the alpha channel is
|
|
1644
|
+
never changed by color ops.
|
|
1645
|
+
|
|
1646
|
+
Filters are element-local: the blur may bleed past the element's box
|
|
1647
|
+
(like CSS \`filter: blur()\`) but never reads or alters other elements'
|
|
1648
|
+
pixels. The element's \`opacity\` applies inside the layer and its
|
|
1649
|
+
\`blend_mode\` applies at the filter composite, so all three features
|
|
1650
|
+
compose. On a group, filtering flattens the subtree first — children
|
|
1651
|
+
are filtered as one image, not individually.
|
|
1652
|
+
|
|
1653
|
+
### 4.7. Stylize effects
|
|
1654
|
+
|
|
1655
|
+
\`effects\` is an ordered array of stylize passes over the element's
|
|
1656
|
+
rendered pixels, applied AFTER the filter fields:
|
|
1657
|
+
|
|
1658
|
+
\`\`\`
|
|
1659
|
+
layer → blur → brightness → contrast → saturation → hue_rotate → effects[0] → … → effects[n]
|
|
1660
|
+
\`\`\`
|
|
1661
|
+
|
|
1662
|
+
\`\`\`json
|
|
1663
|
+
{ "effects": [{ "type": "pixelate", "cell_size": 12 }] }
|
|
1664
|
+
\`\`\`
|
|
1665
|
+
|
|
1666
|
+
Each effect is an object discriminated by \`type\`. Effect params accept
|
|
1667
|
+
\`number | Keyframe[]\`; keyframes evaluate against element-local time.
|
|
1668
|
+
(Effect params are NOT addressable from \`keyframe_animations\` — there
|
|
1669
|
+
is no property-path syntax into the array.) Like filters, effects are
|
|
1670
|
+
element-local, work on every element type (a \`group\` flattens its
|
|
1671
|
+
subtree first), and the element's \`blend_mode\` applies at the final
|
|
1672
|
+
composite. Runtimes encountering an effect \`type\` they don't implement
|
|
1673
|
+
MUST skip that effect (rendering the element without it) and SHOULD
|
|
1674
|
+
warn.
|
|
1675
|
+
|
|
1676
|
+
**Effects read only the element's own rendered pixels — with ONE
|
|
1677
|
+
exclusion: \`glass\`.** Glass additionally reads the element's
|
|
1678
|
+
*backdrop*: the current surface's pixels at the element's position in
|
|
1679
|
+
draw order (§4.2), i.e. everything drawn before it on the same
|
|
1680
|
+
surface. It gets this carve-out because the effect is widely known and
|
|
1681
|
+
in high demand, and there is no proper decomposition — refraction
|
|
1682
|
+
needs the pixels behind the pane — and the alternative, a first-class
|
|
1683
|
+
glass element type, is deliberately not part of this protocol. No
|
|
1684
|
+
other effect type reads the backdrop, and backdrop-sampling blend
|
|
1685
|
+
modes (overlay, soft-light) remain excluded (§4.5). Note the
|
|
1686
|
+
relationship stays one-way: glass READS what is beneath it but never
|
|
1687
|
+
alters how any other element renders.
|
|
1688
|
+
|
|
1689
|
+
Pixel-grid coordinates below are the element's layer pixels at output
|
|
1690
|
+
resolution; cells are aligned to the layer's origin. Color math runs
|
|
1691
|
+
on straight (unpremultiplied) color; "ink" factors scale color and
|
|
1692
|
+
alpha together (premultiplied output).
|
|
1693
|
+
|
|
1694
|
+
#### \`pixelate\`
|
|
1695
|
+
|
|
1696
|
+
| Param | Default | Meaning |
|
|
1697
|
+
|---|---|---|
|
|
1698
|
+
| \`cell_size\` | \`8\` (min 1) | Cell size in canvas pixels. |
|
|
1699
|
+
|
|
1700
|
+
Every pixel takes the color sampled at its cell's center:
|
|
1701
|
+
\`out(p) = src((floor(p / cell) + 0.5) × cell)\`.
|
|
1702
|
+
|
|
1703
|
+
#### \`dither\`
|
|
1704
|
+
|
|
1705
|
+
| Param | Default | Meaning |
|
|
1706
|
+
|---|---|---|
|
|
1707
|
+
| \`levels\` | \`4\` (min 2) | Quantization levels per color channel. |
|
|
1708
|
+
|
|
1709
|
+
Ordered dithering with the 4×4 Bayer matrix
|
|
1710
|
+
\`[0 8 2 10; 12 4 14 6; 3 11 1 9; 15 7 13 5]\`:
|
|
1711
|
+
\`t = (B[y mod 4][x mod 4] + 0.5) / 16\`, then per channel
|
|
1712
|
+
\`c' = clamp(floor(c × (levels−1) + t) / (levels−1), 0, 1)\`.
|
|
1713
|
+
Alpha (coverage) is not dithered.
|
|
1714
|
+
|
|
1715
|
+
#### \`halftone\`
|
|
1716
|
+
|
|
1717
|
+
| Param | Default | Meaning |
|
|
1718
|
+
|---|---|---|
|
|
1719
|
+
| \`cell_size\` | \`8\` (min 2) | Dot-grid cell size in canvas pixels. |
|
|
1720
|
+
| \`angle\` | \`45\` | Grid rotation in degrees. |
|
|
1721
|
+
|
|
1722
|
+
In grid space (pixel coords rotated by \`angle\`), each cell draws a dot
|
|
1723
|
+
at its center, colored with the source sample at that center and sized
|
|
1724
|
+
by its luminance: \`luma = Rec709(c) × α\`, \`r = 0.5 × cell × √luma\`
|
|
1725
|
+
(area-proportional ink). Pixel ink is
|
|
1726
|
+
\`(1 − smoothstep(r−1, r+1, d)) × clamp(r, 0, 1)\` where \`d\` is the
|
|
1727
|
+
grid-space distance to the dot center; outside dots the output is
|
|
1728
|
+
transparent.
|
|
1729
|
+
|
|
1730
|
+
#### \`ascii\`
|
|
1731
|
+
|
|
1732
|
+
| Param | Default | Meaning |
|
|
1733
|
+
|---|---|---|
|
|
1734
|
+
| \`cell_size\` | \`12\` (min 4) | Glyph cell size in canvas pixels. |
|
|
1735
|
+
|
|
1736
|
+
Each cell samples its center color; \`luma = Rec709(c) × α\` selects a
|
|
1737
|
+
glyph by \`i = clamp(floor(luma × 10), 0, 9)\` from the ten-step density
|
|
1738
|
+
ramp \`space . - : = + % * @ #\`. The glyph tints with the cell's color;
|
|
1739
|
+
uninked pixels are transparent. Glyph shapes are NORMATIVE — the
|
|
1740
|
+
embedded 8×8 bitmap font below (one byte per row, MSB = leftmost
|
|
1741
|
+
pixel), upscaled nearest-neighbor to the cell — so the effect never
|
|
1742
|
+
depends on platform fonts:
|
|
1743
|
+
|
|
1744
|
+
| Glyph | Rows (hex) |
|
|
1745
|
+
|---|---|
|
|
1746
|
+
| space | \`00 00 00 00 00 00 00 00\` |
|
|
1747
|
+
| \`.\` | \`00 00 00 00 00 18 18 00\` |
|
|
1748
|
+
| \`-\` | \`00 00 00 7E 00 00 00 00\` |
|
|
1749
|
+
| \`:\` | \`00 18 18 00 00 18 18 00\` |
|
|
1750
|
+
| \`=\` | \`00 00 7E 00 7E 00 00 00\` |
|
|
1751
|
+
| \`+\` | \`00 18 18 7E 18 18 00 00\` |
|
|
1752
|
+
| \`%\` | \`00 C6 CC 18 30 66 C6 00\` |
|
|
1753
|
+
| \`*\` | \`00 66 3C FF 3C 66 00 00\` |
|
|
1754
|
+
| \`@\` | \`7C C6 DE DE DE C0 78 00\` |
|
|
1755
|
+
| \`#\` | \`6C 6C FE 6C FE 6C 6C 00\` |
|
|
1756
|
+
|
|
1757
|
+
#### \`glow\`, \`drop_shadow\`, \`stroke\` (layer styles)
|
|
1758
|
+
|
|
1759
|
+
Layer styles composite BENEATH the element's own pixels (premultiplied
|
|
1760
|
+
under-operator: \`out = content + style × (1 − content.α)\`), on any
|
|
1761
|
+
element type.
|
|
1762
|
+
|
|
1763
|
+
| Effect | Params (defaults) | Math |
|
|
1764
|
+
|---|---|---|
|
|
1765
|
+
| \`glow\` | \`radius\` 20, \`intensity\` 1, \`color\` \`"#FFFFFF"\` | silhouette alpha blurred by σ = radius (§4.6 ladder), × intensity (clamped to 1), × color. |
|
|
1766
|
+
| \`drop_shadow\` | \`offset_x\` 0, \`offset_y\` 12, \`blur\` 18, \`color\` \`"#000000"\`, \`opacity\` 0.6 | silhouette alpha blurred by σ = blur, sampled at p − offset, × color × opacity. |
|
|
1767
|
+
| \`stroke\` | \`width\` 4, \`color\` \`"#FFFFFF"\` | outline band outside the silhouette: max alpha over a 16-tap ring of radius = width, × color. |
|
|
1768
|
+
|
|
1769
|
+
Numeric params are animatable; they chain in array order like all
|
|
1770
|
+
effects (a drop_shadow listed before a glow renders beneath it).
|
|
1771
|
+
|
|
1772
|
+
#### \`chroma_key\`, \`luma_key\` (keying)
|
|
1773
|
+
|
|
1774
|
+
Keying makes pixels of the element's own rendered layer transparent
|
|
1775
|
+
based on their color. All math operates on the STRAIGHT-alpha color
|
|
1776
|
+
\`c = premultiplied.rgb / α\` (with \`c = 0\` where \`α = 0\`); the resulting
|
|
1777
|
+
coverage factor \`a\` scales the pixel's alpha (and its premultiplied
|
|
1778
|
+
color with it).
|
|
1779
|
+
|
|
1780
|
+
**\`chroma_key\`** — params \`color\` (default \`"#00FF00"\`), \`tolerance\`
|
|
1781
|
+
(default \`0.18\`), \`softness\` (default \`0.1\`), \`spill\` (default \`0.5\`).
|
|
1782
|
+
With \`k\` the key color and BT.709 luma \`Y(x) = 0.2126·x.r + 0.7152·x.g
|
|
1783
|
+
+ 0.0722·x.b\`:
|
|
1784
|
+
|
|
1785
|
+
\`\`\`
|
|
1786
|
+
CbCr(x) = ( (x.b − Y(x)) / 1.8556 , (x.r − Y(x)) / 1.5748 )
|
|
1787
|
+
d = | CbCr(c) − CbCr(k) | — Euclidean distance
|
|
1788
|
+
a = softness > 0 ? clamp((d − tolerance) / softness, 0, 1)
|
|
1789
|
+
: (d > tolerance ? 1 : 0)
|
|
1790
|
+
\`\`\`
|
|
1791
|
+
|
|
1792
|
+
Spill suppression caps the key color's dominant channel (ties resolve
|
|
1793
|
+
green → red → blue): with \`i\` that channel and \`j, k\` the others,
|
|
1794
|
+
\`c.i −= spill × max(0, c.i − max(c.j, c.k))\`, applied to every pixel
|
|
1795
|
+
regardless of \`d\`. Output: \`α' = α × a\`, color \`c × α'\`.
|
|
1796
|
+
|
|
1797
|
+
**\`luma_key\`** — params \`threshold\` (default \`0.5\`), \`softness\`
|
|
1798
|
+
(default \`0.1\`), \`invert\` (default \`false\`):
|
|
1799
|
+
|
|
1800
|
+
\`\`\`
|
|
1801
|
+
a = softness > 0 ? clamp((Y(c) − threshold) / softness, 0, 1)
|
|
1802
|
+
: (Y(c) > threshold ? 1 : 0)
|
|
1803
|
+
invert → a = 1 − a
|
|
1804
|
+
\`\`\`
|
|
1805
|
+
|
|
1806
|
+
Pixels darker than \`threshold\` are removed (with \`invert\`, brighter).
|
|
1807
|
+
\`tolerance\` / \`softness\` / \`threshold\` / \`spill\` are animatable;
|
|
1808
|
+
\`color\` and \`invert\` are static. Keying reads only the element's own
|
|
1809
|
+
pixels — to key a green-screen video, put the effect on the \`video\`
|
|
1810
|
+
element (\`color\` set to the screen's actual green); a group keys its
|
|
1811
|
+
composited children as one layer.
|
|
1812
|
+
|
|
1813
|
+
#### \`levels\`, \`lut\` (grading)
|
|
1814
|
+
|
|
1815
|
+
Both operate per pixel on the straight-alpha color; alpha is never
|
|
1816
|
+
changed.
|
|
1817
|
+
|
|
1818
|
+
**\`levels\`** — params \`in_black\` 0, \`in_white\` 1, \`gamma\` 1,
|
|
1819
|
+
\`out_black\` 0, \`out_white\` 1 (all animatable, points clamped to
|
|
1820
|
+
[0, 1], gamma > 0). Per channel:
|
|
1821
|
+
|
|
1822
|
+
\`\`\`
|
|
1823
|
+
x = clamp((c − in_black) / (in_white − in_black), 0, 1)
|
|
1824
|
+
y = x^(1/gamma) — gamma > 1 brightens mid-tones
|
|
1825
|
+
out = clamp(out_black + y × (out_white − out_black), 0, 1)
|
|
1826
|
+
\`\`\`
|
|
1827
|
+
|
|
1828
|
+
**\`lut\`** — params \`source\` (URL of a \`.cube\` file — http(s), relative,
|
|
1829
|
+
or \`data:\` URI), \`intensity\` 1 (animatable, 0..1). The file MUST
|
|
1830
|
+
declare \`LUT_3D_SIZE N\` (2 ≤ N ≤ 256) with the default 0..1 domain;
|
|
1831
|
+
data lines are \`r g b\` triples ordered red-fastest, then green, then
|
|
1832
|
+
blue. Values clamp to [0, 1]. The graded color is the TRILINEAR
|
|
1833
|
+
interpolation of the lattice at \`c × (N−1)\` per axis, and the output
|
|
1834
|
+
is \`mix(c, graded, intensity)\`. A LUT that fails to load or parse MUST
|
|
1835
|
+
skip the pass with a warning (the element still renders). The lattice
|
|
1836
|
+
is sampled at the pipeline's working precision (8-bit per channel in
|
|
1837
|
+
the reference runtime).
|
|
1838
|
+
|
|
1839
|
+
#### \`fractal_noise\`, \`turbulent_displace\` (procedural noise)
|
|
1840
|
+
|
|
1841
|
+
Both build on one NORMATIVE noise function so identical documents
|
|
1842
|
+
produce identical pixels on every runtime. All integer math is 32-bit
|
|
1843
|
+
unsigned (wrapping); lattice coordinates convert int→uint by two's
|
|
1844
|
+
complement.
|
|
1845
|
+
|
|
1846
|
+
\`\`\`
|
|
1847
|
+
pcg(v) : s = v × 747796405 + 2891336453 (mod 2³²)
|
|
1848
|
+
w = ((s >> ((s >> 28) + 4)) XOR s) × 277803737 (mod 2³²)
|
|
1849
|
+
→ (w >> 22) XOR w
|
|
1850
|
+
h(c, seed) = pcg(c.x XOR pcg(c.y XOR pcg(c.z XOR pcg(seed)))) / (2³²−1)
|
|
1851
|
+
noise(p, seed): value noise — trilinear blend of h at the 8 corners of
|
|
1852
|
+
floor(p)'s lattice cell, weights faded per axis by
|
|
1853
|
+
u = f³(f(6f − 15) + 10)
|
|
1854
|
+
fbm(p, octaves, seed) = Σₒ 0.5° × noise(p × 2°, seed + o) / Σₒ 0.5°
|
|
1855
|
+
for o = 0 … octaves−1 (lacunarity 2, gain 0.5)
|
|
1856
|
+
\`\`\`
|
|
1857
|
+
|
|
1858
|
+
**\`fractal_noise\`** — params \`scale\` 100 (canvas px per lattice cell),
|
|
1859
|
+
\`evolution\` 0, \`offset_x\`/\`offset_y\` 0 (canvas px), \`octaves\` 4
|
|
1860
|
+
(integer 1–8, static), \`seed\` 0 (integer, static; use values < 2²⁴).
|
|
1861
|
+
The element's pixels are replaced by grayscale noise, keeping its
|
|
1862
|
+
alpha footprint:
|
|
1863
|
+
|
|
1864
|
+
\`\`\`
|
|
1865
|
+
v = fbm((p + offset) / scale ⊕ evolution as the 3rd axis, octaves, seed)
|
|
1866
|
+
out = (v, v, v) × α, α unchanged
|
|
1867
|
+
\`\`\`
|
|
1868
|
+
|
|
1869
|
+
Grayscale by design — chain \`levels\` to shape contrast and a \`lut\` to
|
|
1870
|
+
color it (gray has no chroma for \`hue_rotate\` to act on). Animate
|
|
1871
|
+
\`evolution\` for in-place churn, the offsets to scroll. On a text
|
|
1872
|
+
element the glyphs become a noise-filled matte.
|
|
1873
|
+
|
|
1874
|
+
**\`turbulent_displace\`** — params \`amount\` 16 (max displacement,
|
|
1875
|
+
canvas px), \`scale\` 120, \`evolution\` 0, \`octaves\` 2 (1–8, static),
|
|
1876
|
+
\`seed\` 0 (static). Each output pixel samples the element's layer at a
|
|
1877
|
+
noise-displaced position (bilinear, clamped to the layer):
|
|
1878
|
+
|
|
1879
|
+
\`\`\`
|
|
1880
|
+
d = ( fbm(p/scale ⊕ evolution, octaves, seed) − 0.5 ,
|
|
1881
|
+
fbm(p/scale ⊕ evolution, octaves, seed + 7919) − 0.5 ) × 2 × amount
|
|
1882
|
+
out(p) = layer(p + d)
|
|
1883
|
+
\`\`\`
|
|
1884
|
+
|
|
1885
|
+
\`scale\`, \`evolution\`, offsets, and \`amount\` are animatable; \`octaves\`
|
|
1886
|
+
and \`seed\` are static.
|
|
1887
|
+
|
|
1888
|
+
#### \`glass\`
|
|
1889
|
+
|
|
1890
|
+
Liquid glass — a refractive pane over the backdrop, following the
|
|
1891
|
+
widely-adopted liquidglass optical model (reference:
|
|
1892
|
+
github.com/ybouane/liquidglass). Glass applies to \`shape\` elements
|
|
1893
|
+
(primitive rectangle / ellipse and path shapes) and \`text\` elements,
|
|
1894
|
+
where the pane geometry is known from the element's own geometry rather
|
|
1895
|
+
than rasterized alpha.
|
|
1896
|
+
|
|
1897
|
+
Glass is **legal under 3D**: 3D transforms (\`x_rotation\` /
|
|
1898
|
+
\`y_rotation\` / \`z\`) and a camera on a glass-carrying element or its
|
|
1899
|
+
un-flattened ancestor chain are valid. The pane is a true plane in the
|
|
1900
|
+
scene and the runtime projects the §4.7 optical model through the
|
|
1901
|
+
pane's plane homography (see "Glass under 3D" below). With no 3D in
|
|
1902
|
+
play the orthographic path applies bit-for-bit.
|
|
1903
|
+
|
|
1904
|
+
| Param | Default | Meaning |
|
|
1905
|
+
|---|---|---|
|
|
1906
|
+
| \`blur_radius\` | \`0\` | Backdrop Gaussian blur σ in px. \`0\` = CLEAR glass (pure refraction, the default material); \`> 0\` = FROSTED. |
|
|
1907
|
+
| \`refraction\` | \`21\` | Lens bend strength (≈ px of displacement; internally the reference dial = \`refraction / 30\`). The magnitude is used. |
|
|
1908
|
+
| \`edge_width\` | \`40\` | Bevel z-radius — how deep the lens curvature reaches. With \`mode: "dome"\` and \`edge_width\` = the shape's radius, the pane is a half-sphere magnifier. |
|
|
1909
|
+
| \`mode\` | \`"pill"\` | Lens cross-section: \`"pill"\` (biconvex — entry + exit refraction + depth-scaled centre magnification) or \`"dome"\` (flat bottom, curved top — uniform magnification toward the centre). |
|
|
1910
|
+
| \`edge_highlight\` | \`0.35\` | Scales the stock light rig (rim 0.22×, inner glow 0.15×, 1.5px top-biased inner stroke 0.55×, Fresnel). \`0.35\` reproduces the reference defaults exactly. |
|
|
1911
|
+
| \`dispersion\` | \`0.05\` | Chromatic aberration along the surface normal (×18 px, edge-weighted). |
|
|
1912
|
+
| \`shadow\` | \`0.3\` | Drop-shadow opacity, painted ONLY outside the pane's SDF (spread 10 px, vertical offset 1 px — glass never frosts its own shadow). |
|
|
1913
|
+
| \`backdrop_saturation\` | \`1\` | Saturation of the sampled backdrop (\`1\` = unchanged). |
|
|
1914
|
+
| \`tint\` | none | Color drawn over the glass; alpha = strength. Not keyframable in v1. |
|
|
1915
|
+
|
|
1916
|
+
All numeric params are animatable. The pane's \`fill_color\` is unused
|
|
1917
|
+
under glass; its \`opacity\` scales the pane. An \`ellipse\` evaluates as
|
|
1918
|
+
the rounded-rect SDF with \`r = min(half)\` — a circle when square, a
|
|
1919
|
+
stadium otherwise. Pane content (labels, icons) goes on lower layers (nearer the front).
|
|
1920
|
+
|
|
1921
|
+
The model, NORMATIVE, evaluated per pixel in pane-local rotated
|
|
1922
|
+
coordinates \`p\` with half-size \`half\`, corner radius \`r\`, z-radius
|
|
1923
|
+
\`zR = edge_width\`, and the reference dial \`dial = |refraction| / 30\`:
|
|
1924
|
+
|
|
1925
|
+
\`\`\`
|
|
1926
|
+
sdf = roundedRectSDF(p, half, r)
|
|
1927
|
+
inside = −sdf
|
|
1928
|
+
h(d) = √(d × (2·zR − d)) clamped to [0, zR] — half-circle bevel
|
|
1929
|
+
∇h = central differences of h at step 2 px (the sdf is ANALYTIC:
|
|
1930
|
+
no field facets, no measured-divergence cap, no rim "lip")
|
|
1931
|
+
N = normalize(−∇h, 1)
|
|
1932
|
+
depth = smoothstep(0, zR, inside)
|
|
1933
|
+
edge = smoothstep(0.35 × min(half), 0, inside)
|
|
1934
|
+
|
|
1935
|
+
pill: refr = (2·∇h + ∇h·(h/zR)·0.5) × (1 − 1/1.5) × dial × 30
|
|
1936
|
+
+ (−p / half) × dial × 4 × depth
|
|
1937
|
+
dome: refr = −p × dial × depth × 0.35
|
|
1938
|
+
|
|
1939
|
+
chroma = N.xy × dispersion × 18 × (edge×0.7 + 0.3) × 2;
|
|
1940
|
+
R/G/B sample at refr + chroma / refr / refr − chroma
|
|
1941
|
+
col = mix(sharp, frosted, 1 − edge × 0.15)
|
|
1942
|
+
— frosted everywhere, 15% sharp at the rim; with
|
|
1943
|
+
blur_radius 0 both textures are identical (clear glass)
|
|
1944
|
+
col = saturate(col, backdrop_saturation); mix toward tint;
|
|
1945
|
+
× (1 + 0.06 × depth)
|
|
1946
|
+
fresnel = (1 − |N.z|)⁴ × (edge_highlight / 0.35)
|
|
1947
|
+
spec = Blinn-Phong on N: L(0.4,0.7,1)^90 + L(−0.3,−0.5,1)^50×0.3
|
|
1948
|
+
+ diffuse L(0.1,0.3,1)^6×0.1 + L(0,0.9,0.4)^120×0.6
|
|
1949
|
+
(specular dial; reference default 0)
|
|
1950
|
+
stroke = 1.5px inner band × (0.4 + 0.6·topBias) × edgeHL × 0.55
|
|
1951
|
+
rim/glow = edge × edgeHL × 0.22 / smoothstep(5,0,inside) × edgeHL × 0.15
|
|
1952
|
+
env = (N.y×0.5 + 0.5) × fresnel × 0.08
|
|
1953
|
+
fin = col + spec + rim + glow + stroke + env, then
|
|
1954
|
+
mix(fin, white, fresnel × 0.2)
|
|
1955
|
+
out = premultiply(fin, aaMask × opacity)
|
|
1956
|
+
|
|
1957
|
+
shadow (sdf > 0 only, offset down by offY):
|
|
1958
|
+
d = max(sdfShadow − 1, 0); s = spread
|
|
1959
|
+
α = (e^(−d²/s²) × 0.65
|
|
1960
|
+
+ e^(−0.08·d / max(0.04·s, 0.01)) × 0.35) × shadow
|
|
1961
|
+
\`\`\`
|
|
1962
|
+
|
|
1963
|
+
**Glass under 3D (CKP/1.0, NORMATIVE).** When the pane carries 3D
|
|
1964
|
+
fields or sits under an un-flattened non-affine chain (§4.4), the
|
|
1965
|
+
model above runs unchanged in the pane's LOCAL frame; only the
|
|
1966
|
+
coordinate hand-off changes:
|
|
1967
|
+
|
|
1968
|
+
- Let \`H\` be the pane's plane homography — the 3×3 restriction of the
|
|
1969
|
+
full §4.4 matrix chain (camera included) to the pane's plane,
|
|
1970
|
+
origin at the pane's center, y down. Per fragment, pane-local
|
|
1971
|
+
\`p = (H⁻¹ · (px, 1)).xy / w\`; a non-positive \`w\` lies past the
|
|
1972
|
+
plane's horizon and outputs nothing. This is exactly the camera-ray
|
|
1973
|
+
/ pane-plane intersection, in projective form.
|
|
1974
|
+
- Refracted and dispersed sample points (\`p + refr ± chroma\`) map
|
|
1975
|
+
FORWARD through \`H\` to surface px (clamped to the surface) and
|
|
1976
|
+
sample the same screen-space backdrop snapshot. Glass refracts the
|
|
1977
|
+
COMPOSITED IMAGE at the screen plane — it does not re-render the
|
|
1978
|
+
scene from the refracted direction.
|
|
1979
|
+
- The light rig, bevel field and SDF are defined in the pane's local
|
|
1980
|
+
frame and tilt WITH the pane (highlights track the surface, not the
|
|
1981
|
+
screen).
|
|
1982
|
+
- Degenerate case: a singular \`H\` (an edge-on pane with no
|
|
1983
|
+
perspective anywhere in its chain) is invisible — runtimes MUST
|
|
1984
|
+
draw nothing. Under perspective an off-axis edge-on pane projects
|
|
1985
|
+
to a thin wedge; that wedge is correct rendering, not an error.
|
|
1986
|
+
- The shadow term evaluates in pane-local coordinates like everything
|
|
1987
|
+
else, so the shadow projects with the pane.
|
|
1988
|
+
|
|
1989
|
+
With no 3D in play the orthographic path applies unchanged —
|
|
1990
|
+
documents valid in CKP/1.0 render bit-identically.
|
|
1991
|
+
|
|
1992
|
+
### 4.8. Lighting and materials (CKP/1.0, NORMATIVE)
|
|
1993
|
+
|
|
1994
|
+
A Source MAY declare \`lights\` and an \`environment\`; an element MAY carry
|
|
1995
|
+
a \`material\`. Lighting is **opt-in and additive**: a document with no
|
|
1996
|
+
\`lights\` and no element \`material\` renders **bit-identically** to one
|
|
1997
|
+
without these fields. Only elements with a \`material\` are shaded.
|
|
1998
|
+
|
|
1999
|
+
**Shading model (PBR).** When an element has a \`material\` and the Source
|
|
2000
|
+
has \`lights\`, the element renders its content normally — those pixels are
|
|
2001
|
+
the **albedo** — and the runtime shades each fragment with one model,
|
|
2002
|
+
identical in 2D and 3D:
|
|
2003
|
+
|
|
2004
|
+
\`\`\`
|
|
2005
|
+
out = albedo · (ambient + Σ_dir diffuse(N,L)·Lc) // Lambert
|
|
2006
|
+
+ Σ_dir specular_GGX(N,L,V,roughness)·F(V,H,F0)·Lc // Cook-Torrance
|
|
2007
|
+
+ (kd·albedo·envAvg + envc·Fr) · reflectivity // environment (IBL)
|
|
2008
|
+
+ emissive·albedo
|
|
2009
|
+
\`\`\`
|
|
2010
|
+
|
|
2011
|
+
where \`envc = mix(env(R), envAvg, roughness)\` is the environment sampled
|
|
2012
|
+
along the reflection ray \`R = reflect(−V,N)\` and blurred toward its
|
|
2013
|
+
average by roughness; \`Fr\` is the roughness-aware Schlick–Fresnel at the
|
|
2014
|
+
view angle; \`kd = (1−Fr)·(1−metalness)\` is the dielectric diffuse weight;
|
|
2015
|
+
and \`envAvg\` is the environment's mean color (its diffuse irradiance).
|
|
2016
|
+
|
|
2017
|
+
- **N** is the element's world-space face normal (from its §4.4 3D
|
|
2018
|
+
orientation), perturbed per-fragment by \`material.normal_map\` when
|
|
2019
|
+
present (a tangent-space map sampled in the element's UV space, scaled
|
|
2020
|
+
by \`normal_scale\`; the tangent/bitangent come from the quad's U/V axes).
|
|
2021
|
+
- **V** is the view vector: per-fragment \`normalize(eye − fragmentWorld)\`
|
|
2022
|
+
under a camera, or \`(0,0,1)\` with no camera. This is the only term the
|
|
2023
|
+
camera affects — so specular and reflections **sweep as the camera
|
|
2024
|
+
moves** (3D); with no camera you animate the lights instead (2D). The
|
|
2025
|
+
math is the same.
|
|
2026
|
+
- **F0** (Fresnel base reflectance) is \`0.04\` for dielectric, the albedo
|
|
2027
|
+
for metal, interpolated by \`metalness\`. Fresnel uses Schlick.
|
|
2028
|
+
- **roughness** widens the GGX specular lobe and blurs the environment
|
|
2029
|
+
reflection; **reflectivity** is an art dial over the environment term.
|
|
2030
|
+
|
|
2031
|
+
**Lights** (\`source.lights\`):
|
|
2032
|
+
- \`{ type: 'ambient', color?, intensity? }\` — uniform fill.
|
|
2033
|
+
- \`{ type: 'directional', azimuth?, elevation?, color?, intensity? }\` —
|
|
2034
|
+
a parallel light; \`azimuth\`/\`elevation\` (degrees) give its direction.
|
|
2035
|
+
All scalar fields animatable.
|
|
2036
|
+
|
|
2037
|
+
**Environment** (\`source.environment\`) is what reflective surfaces
|
|
2038
|
+
sample along the reflection vector. Phase 1: \`{ type: 'gradient', stops }\`
|
|
2039
|
+
— a gradient "sky" indexed by the reflection ray's vertical component
|
|
2040
|
+
(offset 0 = looking down, 1 = up), so a surface mirrors the sky when it
|
|
2041
|
+
tilts up and the ground when it tilts down, and the reflection shifts as
|
|
2042
|
+
the surface (or camera) moves. Up to 4 stops. The environment also
|
|
2043
|
+
contributes a diffuse irradiance (its average color) to dielectric
|
|
2044
|
+
surfaces. The other type is \`{ type: 'image', src }\` — an
|
|
2045
|
+
**equirectangular** (2:1 lat-long) image the surface mirrors along the
|
|
2046
|
+
reflection vector (real photographic reflections); roughness blurs it
|
|
2047
|
+
toward the image's average color. Both share one IBL path.
|
|
2048
|
+
|
|
2049
|
+
**Bloom** (\`source.bloom\`, Phase 2) is a whole-frame post-process:
|
|
2050
|
+
pixels brighter than \`threshold\` (luma, soft \`knee\`) are blurred by
|
|
2051
|
+
\`radius\` and added back × \`intensity\`, so bright regions — specular
|
|
2052
|
+
highlights, emissive surfaces, bright media — bleed light across element
|
|
2053
|
+
boundaries. It is **brightness-driven**: the amount each region blooms
|
|
2054
|
+
comes from its own brightness, not a per-element knob (use the
|
|
2055
|
+
per-element \`glow\` effect for deliberate, art-directed halos). Opt-in;
|
|
2056
|
+
absent ⇒ no bloom (byte-identical). All fields animatable.
|
|
2057
|
+
|
|
2058
|
+
**Material** (\`element.material\`): \`roughness\` (0 mirror‑tight .. 1
|
|
2059
|
+
matte), \`metalness\` (0 dielectric .. 1 metal), \`reflectivity\` (env
|
|
2060
|
+
strength), \`emissive\` (self-illumination toward the unlit pixels),
|
|
2061
|
+
\`normal_map\` (tangent-space normal map URL — flat texel = \`#8080ff\`) and
|
|
2062
|
+
\`normal_scale\` (perturbation strength). Scalars animatable. Absent ⇒
|
|
2063
|
+
unlit.
|
|
2064
|
+
|
|
2065
|
+
**Determinism / cost.** No \`material\` ⇒ the element takes the exact
|
|
2066
|
+
unlit path (no shading pass), so unlit content is byte-identical and
|
|
2067
|
+
pays nothing. The model is local illumination (no shadows, no global
|
|
2068
|
+
illumination) — flat 2.5D surfaces lit per-fragment.
|
|
2069
|
+
|
|
2070
|
+
---
|
|
2071
|
+
|
|
2072
|
+
## 5. Element types
|
|
2073
|
+
|
|
2074
|
+
CKP/1.0 defines eight element types — \`video\`, \`image\`, \`text\`,
|
|
2075
|
+
\`shape\`, \`audio\`, \`group\`, \`caption\`, and \`particles\`. (The former
|
|
2076
|
+
\`svg\` element was absorbed into \`shape\`, which carries vector \`paths\`.)
|
|
2077
|
+
Each section below specifies the fields, semantics, and required
|
|
2078
|
+
behavior for one type.
|
|
2079
|
+
|
|
2080
|
+
### 5.1. \`shape\`
|
|
2081
|
+
|
|
2082
|
+
A \`shape\` draws geometry in one of two representations, selected by whether
|
|
2083
|
+
it carries \`paths\`:
|
|
2084
|
+
|
|
2085
|
+
- **Primitive** — \`shape: "rectangle" | "ellipse"\` with optional rounded
|
|
2086
|
+
corners, gradient fills, and stroke. Rendered as a resolution-independent
|
|
2087
|
+
SDF. \`fill_color\`, \`stroke_color\`, \`stroke_width\`, and \`border_radius\` are
|
|
2088
|
+
animatable via \`keyframe_animations\` (e.g. \`property: "border_radius"\`).
|
|
2089
|
+
- **Path** — arbitrary vector geometry via \`paths\`: keyframeable \`d\`
|
|
2090
|
+
morphing, per-sub-path fill/stroke, and stroke trim/draw-on. Rasterized,
|
|
2091
|
+
so resolution is bound by \`view_box\`. Specified in §5.6.
|
|
2092
|
+
|
|
2093
|
+
When \`paths\` is present the primitive fields are ignored.
|
|
2094
|
+
|
|
2095
|
+
\`\`\`ts
|
|
2096
|
+
interface ShapeElement extends BaseElement {
|
|
2097
|
+
type: "shape";
|
|
2098
|
+
// Primitive form (ignored when \`paths\` is present):
|
|
2099
|
+
shape?: "rectangle" | "ellipse"; // default "rectangle"
|
|
2100
|
+
fill_color?: string; // hex, default "#ffffff"
|
|
2101
|
+
gradient?: LinearGradient | RadialGradient; // overrides fill_color
|
|
2102
|
+
stroke_color?: string;
|
|
2103
|
+
stroke_width?: number;
|
|
2104
|
+
border_radius?: number; // PIXELS — see §5.1.2
|
|
2105
|
+
shadow?: BoxShadow; // drop shadow cast by the shape
|
|
2106
|
+
// Path form (§5.6):
|
|
2107
|
+
view_box?: [number, number, number, number]; // default [0, 0, 100, 100]
|
|
2108
|
+
gradients?: PathGradient[];
|
|
2109
|
+
paths?: PathDef[]; // ≥1 when present
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
interface BoxShadow {
|
|
2113
|
+
color: string; // §3.4
|
|
2114
|
+
offset_x?: number; // px. Default 0
|
|
2115
|
+
offset_y?: number; // px. Default 12
|
|
2116
|
+
blur?: number; // Gaussian σ px (0 = crisp). Default 18
|
|
2117
|
+
}
|
|
2118
|
+
\`\`\`
|
|
2119
|
+
|
|
2120
|
+
\`shadow\` casts a soft drop shadow beneath the shape. Under 3D the shadow
|
|
2121
|
+
foreshortens with the element's plane, while \`offset_x\`/\`offset_y\`
|
|
2122
|
+
translate in the parent plane (§4.4.3).
|
|
2123
|
+
|
|
2124
|
+
#### 5.1.1. Gradients
|
|
2125
|
+
|
|
2126
|
+
\`\`\`ts
|
|
2127
|
+
interface LinearGradient {
|
|
2128
|
+
type: "linear";
|
|
2129
|
+
angle?: number; // degrees, CSS linear-gradient() convention:
|
|
2130
|
+
// 0 = to top, clockwise. 90 = to right,
|
|
2131
|
+
// 180 = to bottom (default), 270 = to left.
|
|
2132
|
+
stops: GradientStop[]; // 2..4 stops
|
|
2133
|
+
}
|
|
2134
|
+
interface RadialGradient {
|
|
2135
|
+
type: "radial";
|
|
2136
|
+
cx?: number; // 0..1 of bounding box, default 0.5
|
|
2137
|
+
cy?: number;
|
|
2138
|
+
radius?: number; // 0..1, default 0.5
|
|
2139
|
+
stops: GradientStop[];
|
|
2140
|
+
}
|
|
2141
|
+
interface GradientStop {
|
|
2142
|
+
offset: number; // 0..1
|
|
2143
|
+
color: string; // hex
|
|
2144
|
+
}
|
|
2145
|
+
\`\`\`
|
|
2146
|
+
|
|
2147
|
+
Implementations MUST support at least 4 stops. The gradient direction
|
|
2148
|
+
for linear gradients uses CSS-style angle conventions.
|
|
2149
|
+
|
|
2150
|
+
#### 5.1.2. Corner radius
|
|
2151
|
+
|
|
2152
|
+
\`border_radius\` is in PIXELS, not a normalized 0..1 value. Runtimes MUST
|
|
2153
|
+
clamp the value to half the shorter of \`width\`/\`height\` so that values
|
|
2154
|
+
exceeding the shape produce a pill or circle rather than visual artifacts.
|
|
2155
|
+
|
|
2156
|
+
Conforming runtimes MUST render corner arcs as **true quarter-circles**,
|
|
2157
|
+
not stretched ellipses. This means SDF-based renderers MUST perform
|
|
2158
|
+
corner math in pixel space, not in normalized UV space.
|
|
2159
|
+
|
|
2160
|
+
### 5.2. \`text\`
|
|
2161
|
+
|
|
2162
|
+
Multi-line text. Text soft-wraps within the box (\`text_wrap\`), honors
|
|
2163
|
+
explicit \`\\n\` breaks, \`line_height\`, and per-line backgrounds. The
|
|
2164
|
+
default font is **Inter** (not a platform stack); \`font_family\` selects
|
|
2165
|
+
another registered family (§2.1 \`fonts\`).
|
|
2166
|
+
|
|
2167
|
+
\`\`\`ts
|
|
2168
|
+
interface TextElement extends BaseElement {
|
|
2169
|
+
type: "text";
|
|
2170
|
+
text?: string; // static text
|
|
2171
|
+
spans?: TextSpan[]; // inline-styled runs (§5.2.4)
|
|
2172
|
+
|
|
2173
|
+
font_family?: string; // registered family name; default Inter
|
|
2174
|
+
font_size?: number | string; // "auto" fits to width
|
|
2175
|
+
font_weight?: number | string; // "400", "bold", etc.
|
|
2176
|
+
font_style?: "normal" | "italic";
|
|
2177
|
+
fill_color?: string;
|
|
2178
|
+
stroke_color?: string;
|
|
2179
|
+
stroke_width?: number;
|
|
2180
|
+
text_align?: "left" | "center" | "right";
|
|
2181
|
+
letter_spacing?: number;
|
|
2182
|
+
|
|
2183
|
+
background_color?: string; // solid bg, SHRINK-WRAPPED to glyphs
|
|
2184
|
+
background_border_radius?: number; // corner radius (px) for the bg
|
|
2185
|
+
background_padding?: number | [number, number]; // bg padding px (or [x,y])
|
|
2186
|
+
text_shadow?: TextShadow | TextShadow[]; // per-glyph shadow(s)
|
|
2187
|
+
|
|
2188
|
+
mask?: TextMask; // reveal mask
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
interface TextShadow {
|
|
2192
|
+
color: string; // §3.4
|
|
2193
|
+
offset_x?: number; // px, text-local frame. Default 0
|
|
2194
|
+
offset_y?: number;
|
|
2195
|
+
blur?: number; // Gaussian softness px (0 = crisp). Default 0
|
|
2196
|
+
opacity?: number; // 0..1, multiplies color alpha. Default 1
|
|
2197
|
+
}
|
|
2198
|
+
\`\`\`
|
|
2199
|
+
|
|
2200
|
+
\`text_shadow\` is a **per-glyph** drop shadow (CSS \`text-shadow\`): each glyph
|
|
2201
|
+
casts its own, so it tracks per-letter animation and overlapping glyphs —
|
|
2202
|
+
unlike the silhouette \`drop_shadow\` effect (§4.7), which shadows the
|
|
2203
|
+
flattened text as one shape. Pass an **array** for stacked shadows, painted
|
|
2204
|
+
back-to-front (list farthest → nearest for a clean 3D extrusion). Shadows
|
|
2205
|
+
are drawn behind every glyph (a two-pass render) so they never get clipped
|
|
2206
|
+
by neighbouring letters. Works the same on \`caption\`. (Reach for the
|
|
2207
|
+
\`drop_shadow\` effect instead when you want one soft shadow of the whole
|
|
2208
|
+
text silhouette.)
|
|
2209
|
+
|
|
2210
|
+
\`background_color\` draws a solid background behind the text as **one band
|
|
2211
|
+
per line**, each shrink-wrapped to that line's glyphs (line width × the
|
|
2212
|
+
font ascent/descent box) — NOT the element's \`width\`/\`height\` box — so
|
|
2213
|
+
centered or ragged multi-line text gets per-line pills, and it tracks
|
|
2214
|
+
wrapping and \`font_size: "auto"\`. \`background_border_radius\` rounds each
|
|
2215
|
+
band; \`background_padding\` (a number, or \`[x, y]\`) insets it outward. It
|
|
2216
|
+
rotates, scales, and skews with the element. For a per-run highlight band
|
|
2217
|
+
use a span \`background\` (§5.2.4); for a drop shadow use a \`drop_shadow\`
|
|
2218
|
+
effect (§4.7). The same fields work on \`caption\` (one band around the
|
|
2219
|
+
caption phrase).
|
|
2220
|
+
|
|
2221
|
+
#### 5.2.1. \`text\` content
|
|
2222
|
+
|
|
2223
|
+
\`text\` (or \`spans\`, §5.2.4) provides the content. If \`spans\` is present it
|
|
2224
|
+
takes precedence over \`text\`. If neither is present the element renders
|
|
2225
|
+
nothing (and runtimes MAY skip the element entirely).
|
|
2226
|
+
|
|
2227
|
+
#### 5.2.2. \`font_size: "auto"\`
|
|
2228
|
+
|
|
2229
|
+
When \`font_size\` is the string \`"auto"\`, the runtime MUST compute a
|
|
2230
|
+
font size such that the rendered text fits inside the element's \`width\`.
|
|
2231
|
+
The exact algorithm is implementation-defined but MUST be deterministic
|
|
2232
|
+
for the same text + font + width inputs.
|
|
2233
|
+
|
|
2234
|
+
#### 5.2.3. Text mask
|
|
2235
|
+
|
|
2236
|
+
\`\`\`ts
|
|
2237
|
+
interface TextMask {
|
|
2238
|
+
type: "linear-wipe";
|
|
2239
|
+
angle?: number; // degrees, default -45
|
|
2240
|
+
progress?: number | Keyframe[]; // 0..1
|
|
2241
|
+
softness?: number; // 0..1, default 0.3
|
|
2242
|
+
}
|
|
2243
|
+
\`\`\`
|
|
2244
|
+
|
|
2245
|
+
When present, the text is rendered into an offscreen surface and
|
|
2246
|
+
multiplied by a linear-gradient alpha mask. \`progress\` controls the
|
|
2247
|
+
reveal position; \`softness\` controls the wipe edge width.
|
|
2248
|
+
|
|
2249
|
+
#### 5.2.4. Spans
|
|
2250
|
+
|
|
2251
|
+
\`spans\` carries inline-styled runs. When present it takes precedence
|
|
2252
|
+
over \`text\`. Runs lay out left-to-right; a span whose
|
|
2253
|
+
\`text\` is exactly \`"\\n"\` is a hard line break. Each span inherits the
|
|
2254
|
+
element's \`font_family\` / \`font_size\` / \`font_weight\` / \`fill_color\` /
|
|
2255
|
+
\`letter_spacing\` unless it overrides them.
|
|
2256
|
+
|
|
2257
|
+
\`\`\`ts
|
|
2258
|
+
interface TextSpan {
|
|
2259
|
+
text: string;
|
|
2260
|
+
font_family?: string;
|
|
2261
|
+
font_size?: number | string;
|
|
2262
|
+
font_weight?: number | string;
|
|
2263
|
+
font_style?: "normal" | "italic";
|
|
2264
|
+
fill_color?: string;
|
|
2265
|
+
letter_spacing?: number; // px tracking; inherits element's
|
|
2266
|
+
background_color?: string; // flat full-line-box band
|
|
2267
|
+
background?: TextSpanBackground; // stylized band; overrides above
|
|
2268
|
+
nowrap?: boolean; // atomic for word-wrap
|
|
2269
|
+
}
|
|
2270
|
+
\`\`\`
|
|
2271
|
+
|
|
2272
|
+
\`letter_spacing\` (element and span level) is pixels of tracking added
|
|
2273
|
+
after EVERY character, including the last — Chrome's model, so boxes
|
|
2274
|
+
measured in a browser reproduce exactly. \`nowrap\` marks the span atomic
|
|
2275
|
+
for word-wrap (CSS \`white-space: nowrap\` semantics): the runtime never
|
|
2276
|
+
breaks inside it. \`background\` draws a band behind the span's glyphs
|
|
2277
|
+
(\`color\`, plus optional \`height_ratio\`, \`inset_y_ratio\`, \`padding_x\`,
|
|
2278
|
+
\`skew_x\`, \`border_radius\` — see the schema for exact semantics).
|
|
2279
|
+
|
|
2280
|
+
### 5.3. \`image\` and \`video\`
|
|
2281
|
+
|
|
2282
|
+
\`\`\`ts
|
|
2283
|
+
interface ImageElement extends BaseElement {
|
|
2284
|
+
type: "image";
|
|
2285
|
+
source: string; // URL or path
|
|
2286
|
+
fit?: "cover" | "contain" | "fill" | "none"; // default "cover"
|
|
2287
|
+
border_radius?: number; // corner radius px, default 0
|
|
2288
|
+
crop_x?: number; // source crop, normalized 0..1 (see §5.3.1)
|
|
2289
|
+
crop_y?: number;
|
|
2290
|
+
crop_width?: number;
|
|
2291
|
+
crop_height?: number;
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
interface VideoElement extends BaseElement {
|
|
2295
|
+
type: "video";
|
|
2296
|
+
source: string;
|
|
2297
|
+
fit?: "cover" | "contain" | "fill" | "none"; // default "cover"
|
|
2298
|
+
crop_x?: number; // source crop, normalized 0..1 (see §5.3.1)
|
|
2299
|
+
crop_y?: number;
|
|
2300
|
+
crop_width?: number;
|
|
2301
|
+
crop_height?: number;
|
|
2302
|
+
volume?: number | Keyframe[]; // 0..100, default 100 (animatable)
|
|
2303
|
+
playback_rate?: number | Keyframe[]; // media seconds per timeline second, default 1
|
|
2304
|
+
// (schema type; the runtime requires it static — §5.3.2)
|
|
2305
|
+
trim_start?: number; // seconds into the media, default 0
|
|
2306
|
+
trim_duration?: number; // playable media window after trim_start
|
|
2307
|
+
loop?: boolean;
|
|
2308
|
+
}
|
|
2309
|
+
\`\`\`
|
|
2310
|
+
|
|
2311
|
+
Asset reference rules are defined in §8.
|
|
2312
|
+
|
|
2313
|
+
#### 5.3.1. Object fit and source crop
|
|
2314
|
+
|
|
2315
|
+
\`fit\` follows CSS \`object-fit\` against the element box:
|
|
2316
|
+
|
|
2317
|
+
| Value | Behavior |
|
|
2318
|
+
|---|---|
|
|
2319
|
+
| \`cover\` (default) | scale media to fill the box; crop the overflow, centered |
|
|
2320
|
+
| \`contain\` | scale media to fit inside the box; letterbox |
|
|
2321
|
+
| \`fill\` | stretch media to the box exactly |
|
|
2322
|
+
| \`none\` | natural media size, centered, cropped to the box |
|
|
2323
|
+
|
|
2324
|
+
**Source crop.** \`crop_x\` / \`crop_y\` / \`crop_width\` / \`crop_height\` select a normalized
|
|
2325
|
+
sub-rectangle of the **source** media (each in \`0..1\`, origin top-left)
|
|
2326
|
+
that is shown in place of the whole source. The default is the whole
|
|
2327
|
+
source — \`crop_x = 0, crop_y = 0, crop_width = 1, crop_height = 1\` — and
|
|
2328
|
+
omitting the fields is identical to that identity crop.
|
|
2329
|
+
|
|
2330
|
+
Crop applies BEFORE \`fit\`: the cropped sub-rectangle becomes the
|
|
2331
|
+
effective media, and \`fit\` then maps it into the element box exactly as
|
|
2332
|
+
in §5.3.1. The element box (its \`x\` / \`y\` / \`width\` / \`height\`) is
|
|
2333
|
+
**unchanged** — crop only chooses which part of the source fills it, so
|
|
2334
|
+
crop composes orthogonally with transform, \`border_radius\`, filters, and
|
|
2335
|
+
3D. A runtime MUST clamp the rect to the unit square (\`crop_width\` to
|
|
2336
|
+
\`1 − crop_x\`, \`crop_height\` to \`1 − crop_y\`); a zero-area crop is treated
|
|
2337
|
+
as the identity.
|
|
2338
|
+
|
|
2339
|
+
Each component MAY be keyframed (§6.3). Animating the crop origin pans
|
|
2340
|
+
across the source and animating its size zooms — a Ken Burns move with no
|
|
2341
|
+
change to the element's layout.
|
|
2342
|
+
|
|
2343
|
+
#### 5.3.2. Media time mapping
|
|
2344
|
+
|
|
2345
|
+
The playable *trim window* is:
|
|
2346
|
+
|
|
2347
|
+
\`\`\`
|
|
2348
|
+
window_start = max(0, trim_start)
|
|
2349
|
+
window_length = min(trim_duration ?? ∞, media_duration − window_start)
|
|
2350
|
+
\`\`\`
|
|
2351
|
+
|
|
2352
|
+
The media time sampled at composition time \`t\` is:
|
|
2353
|
+
|
|
2354
|
+
\`\`\`
|
|
2355
|
+
consumed = max(0, t − element.time) × playback_rate
|
|
2356
|
+
media_t = window_start + (consumed mod window_length) if loop
|
|
2357
|
+
= window_start + min(consumed, window_length − ε) otherwise
|
|
2358
|
+
\`\`\`
|
|
2359
|
+
|
|
2360
|
+
i.e. \`loop\` wraps WITHIN the trim window; without \`loop\` the last frame
|
|
2361
|
+
holds. \`playback_rate\` MUST be a static number (keyframed rates are not
|
|
2362
|
+
defined in CKP/1.0).
|
|
2363
|
+
|
|
2364
|
+
**Time remapping.** A video MAY carry \`time_remap\` — a keyframe array
|
|
2365
|
+
(§6.3 semantics: destination-keyframe easing, element-local time) whose
|
|
2366
|
+
VALUES are media times in seconds. When present, it REPLACES the
|
|
2367
|
+
mapping above entirely (\`trim_start\`, \`trim_duration\`,
|
|
2368
|
+
\`playback_rate\`, and \`loop\` are ignored):
|
|
2369
|
+
|
|
2370
|
+
\`\`\`
|
|
2371
|
+
media_t = clamp(interpolate(time_remap, t − element.time), 0, media_duration − ε)
|
|
2372
|
+
\`\`\`
|
|
2373
|
+
|
|
2374
|
+
Speed ramps are steep segments, freeze frames are flat ones, and
|
|
2375
|
+
reverse playback follows decreasing values; easings shape the ramp.
|
|
2376
|
+
Decoders quantize \`media_t\` to the media's own frames, which is
|
|
2377
|
+
conformant (§2.1 motion-blur note applies the same way).
|
|
2378
|
+
|
|
2379
|
+
**Varispeed audio.** Audio under a warped clock — a video's embedded
|
|
2380
|
+
track under \`time_remap\`, or any sound element inside a time-remapped
|
|
2381
|
+
group (§5.8.3) — plays VARISPEED, tape-style: at each instant the
|
|
2382
|
+
audio advances through the media at \`rate(t) = d(media_t)/dt\`, and
|
|
2383
|
+
pitch shifts with the rate (2× plays an octave up, slow-motion plays
|
|
2384
|
+
low). Flat segments (rate 0) are silent; decreasing segments play the
|
|
2385
|
+
media REVERSED at \`|rate|\`. The reference implementation samples the
|
|
2386
|
+
effective media-time function at 10 ms through the full warp chain,
|
|
2387
|
+
splits it into monotonic runs, and schedules each run with a rate
|
|
2388
|
+
curve (reversed runs play a sample-reversed copy of the buffer).
|
|
2389
|
+
Pitch-preserving time-stretch is NOT defined in CKP/1.0. Fade
|
|
2390
|
+
envelopes (\`audio_fade_in\`/\`audio_fade_out\`) are not applied under
|
|
2391
|
+
warps in v1.
|
|
2392
|
+
|
|
2393
|
+
#### 5.3.3. Video audio
|
|
2394
|
+
|
|
2395
|
+
If the media container carries an audio track, conforming Level 3
|
|
2396
|
+
runtimes MUST play/mix its FIRST audio track using the same timing as
|
|
2397
|
+
§5.3.2, with gain \`volume / 100\`. \`playback_rate\` resamples the audio
|
|
2398
|
+
(pitch shifts accordingly; time-stretch is not defined in CKP/1.0).
|
|
2399
|
+
Videos without an audio track are silent — not an error.
|
|
2400
|
+
|
|
2401
|
+
#### 5.3.4. Audio fades
|
|
2402
|
+
|
|
2403
|
+
\`audio_fade_in\` / \`audio_fade_out\` (on both \`audio\` and \`video\`
|
|
2404
|
+
elements, seconds, default 0) shape the gain over the element's
|
|
2405
|
+
TIMELINE window \`[0, L]\`:
|
|
2406
|
+
|
|
2407
|
+
\`\`\`
|
|
2408
|
+
g(τ) = volume/100 × min(1, τ / fade_in) × min(1, (L − τ) / fade_out)
|
|
2409
|
+
\`\`\`
|
|
2410
|
+
|
|
2411
|
+
(each factor is 1 when its fade is 0). The envelope is piecewise linear
|
|
2412
|
+
between its corner points; runtimes MUST reproduce it within normal
|
|
2413
|
+
gain-ramp accuracy, including when playback starts mid-fade (seek).
|
|
2414
|
+
|
|
2415
|
+
### 5.4. \`audio\`
|
|
2416
|
+
|
|
2417
|
+
\`\`\`ts
|
|
2418
|
+
interface AudioElement extends BaseElement {
|
|
2419
|
+
type: "audio";
|
|
2420
|
+
source: string;
|
|
2421
|
+
volume?: number | Keyframe[]; // 0..100
|
|
2422
|
+
trim_start?: number;
|
|
2423
|
+
trim_duration?: number;
|
|
2424
|
+
loop?: boolean;
|
|
2425
|
+
}
|
|
2426
|
+
\`\`\`
|
|
2427
|
+
|
|
2428
|
+
Audio elements have no visual representation. They contribute to the
|
|
2429
|
+
mixed audio track produced by export-conformant runtimes (§10.3).
|
|
2430
|
+
Preview-only runtimes MAY play them via \`HTMLAudioElement\` or similar.
|
|
2431
|
+
|
|
2432
|
+
### 5.5. \`caption\`
|
|
2433
|
+
|
|
2434
|
+
Word-timed captions with optional kinetic styling.
|
|
2435
|
+
|
|
2436
|
+
\`\`\`ts
|
|
2437
|
+
interface CaptionElement extends BaseElement {
|
|
2438
|
+
type: "caption";
|
|
2439
|
+
words: { text: string; start: number; end: number }[]; // start/end relative to element.time
|
|
2440
|
+
style?: "tiktok_bounce" | "fade_reveal" | "kinetic_typewriter" | "word_pop";
|
|
2441
|
+
// Windowing — how much of the transcript shows at once (§5.5.1). A whole
|
|
2442
|
+
// transcript otherwise renders as one block; with \`max_length\` set, the words
|
|
2443
|
+
// are split into chunks and only the chunk active at the current time shows.
|
|
2444
|
+
max_length?: number | "auto"; // number = max LETTERS per chunk;
|
|
2445
|
+
// "auto" = a few words per chunk;
|
|
2446
|
+
// absent = show all at once.
|
|
2447
|
+
|
|
2448
|
+
// Text-like styling
|
|
2449
|
+
font_family?: string;
|
|
2450
|
+
font_size?: number | string; // "auto" fits joined words to width
|
|
2451
|
+
font_weight?: number | string;
|
|
2452
|
+
fill_color?: string;
|
|
2453
|
+
highlight_color?: string;
|
|
2454
|
+
highlight_background_color?: string;
|
|
2455
|
+
text_align?: "left" | "center" | "right";
|
|
2456
|
+
}
|
|
2457
|
+
\`\`\`
|
|
2458
|
+
|
|
2459
|
+
\`words[*].start\` and \`words[*].end\` are seconds RELATIVE to the
|
|
2460
|
+
element's \`time\`. The exact kinetic behavior for each \`style\` is
|
|
2461
|
+
defined by the reference implementation; deviations MAY occur in third-
|
|
2462
|
+
party renderers but the timing MUST match.
|
|
2463
|
+
|
|
2464
|
+
#### 5.5.1. Windowing (\`max_length\`)
|
|
2465
|
+
|
|
2466
|
+
A full transcript on one element would render as a single unreadable block.
|
|
2467
|
+
\`max_length\` splits \`words\` into CHUNKS; at any time only the chunk active then
|
|
2468
|
+
is shown (within the element's box, wrapped). Chunking:
|
|
2469
|
+
|
|
2470
|
+
- **number** — grow a chunk word-by-word until adding the next word would exceed
|
|
2471
|
+
this many LETTERS (characters), then start a new chunk.
|
|
2472
|
+
- **\`"auto"\`** — chunk by a few words (and break on pauses) — the speech default.
|
|
2473
|
+
- **absent** — no windowing; the whole transcript shows at once.
|
|
2474
|
+
|
|
2475
|
+
The active chunk at element-local time \`t\` is the last chunk whose first word has
|
|
2476
|
+
started (so a chunk lingers through silent gaps until the next begins). Word
|
|
2477
|
+
kinetics (\`style\`) apply WITHIN the active chunk. Word \`start\`/\`end\` are
|
|
2478
|
+
unchanged — \`max_length\` is a display rule, not a re-timing.
|
|
2479
|
+
|
|
2480
|
+
### 5.6. \`shape\` paths (vector geometry)
|
|
2481
|
+
|
|
2482
|
+
A \`shape\` (§5.1) that carries \`paths\` renders as arbitrary vector geometry —
|
|
2483
|
+
a restricted SVG-path subset (this absorbs the former standalone \`svg\`
|
|
2484
|
+
element). Conforming runtimes MUST support viewBox-scaled paths with linear
|
|
2485
|
+
gradients, clip-to-path, stroke-dashoffset progress, and per-path opacity.
|
|
2486
|
+
|
|
2487
|
+
The path-form fields live on \`ShapeElement\` (§5.1): \`view_box\`, \`gradients\`,
|
|
2488
|
+
and \`paths\`. Their element types:
|
|
2489
|
+
|
|
2490
|
+
\`\`\`ts
|
|
2491
|
+
interface PathGradient {
|
|
2492
|
+
id: string;
|
|
2493
|
+
type: "linear";
|
|
2494
|
+
x1: number; y1: number; x2: number; y2: number; // viewBox coords
|
|
2495
|
+
stops: GradientStop[];
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
interface PathDef {
|
|
2499
|
+
d: string | Keyframe[]; // SVG path data, or d-string keyframes (§5.6.2)
|
|
2500
|
+
fill?: string; // hex or "url(#gradient-id)"
|
|
2501
|
+
stroke?: string;
|
|
2502
|
+
stroke_width?: number;
|
|
2503
|
+
stroke_progress?: number | Keyframe[]; // 0..1
|
|
2504
|
+
trim_start?: number | Keyframe[]; // §5.6.1
|
|
2505
|
+
trim_end?: number | Keyframe[];
|
|
2506
|
+
trim_offset?: number | Keyframe[];
|
|
2507
|
+
clip_path?: string; // another path that clips this one
|
|
2508
|
+
stroke_linecap?: "butt" | "round" | "square";
|
|
2509
|
+
stroke_linejoin?: "miter" | "round" | "bevel";
|
|
2510
|
+
opacity?: number; // 0..1
|
|
2511
|
+
}
|
|
2512
|
+
\`\`\`
|
|
2513
|
+
|
|
2514
|
+
\`stroke_progress\` MUST drive the standard \`stroke-dasharray\` /
|
|
2515
|
+
\`stroke-dashoffset\` reveal — a \`progress\` of \`0\` shows no stroke; \`1\`
|
|
2516
|
+
shows the entire stroke. Implementations MUST measure path length
|
|
2517
|
+
deterministically (the reference implementation uses
|
|
2518
|
+
\`SVGPathElement.getTotalLength()\`).
|
|
2519
|
+
|
|
2520
|
+
#### 5.6.1. Trim paths
|
|
2521
|
+
|
|
2522
|
+
Each path MAY carry a trim window: only the stroke between
|
|
2523
|
+
\`trim_start\` and \`trim_end\` — fractions of the path's TOTAL LENGTH,
|
|
2524
|
+
0..1 — is drawn. \`trim_offset\` rotates the window around the path,
|
|
2525
|
+
WRAPPING at the ends (an offset of 1 is a full lap), so an animated
|
|
2526
|
+
offset is the classic traveling-dash "snake" and an animated
|
|
2527
|
+
\`trim_end\` is the draw-on reveal. All three are animatable;
|
|
2528
|
+
\`stroke_progress\` remains as sugar for \`[0, progress]\` and is ignored
|
|
2529
|
+
when any trim field is present. Fill is unaffected — trimming applies
|
|
2530
|
+
to the STROKE only.
|
|
2531
|
+
|
|
2532
|
+
Reference evaluation: with window width \`w = clamp(trim_end, 0, 1) −
|
|
2533
|
+
clamp(trim_start, 0, 1)\` (nothing draws when w ≤ 0; the full stroke
|
|
2534
|
+
when w ≥ 1) and wrapped anchor \`a = (trim_start + trim_offset) mod 1\`,
|
|
2535
|
+
the stroke uses a dash pattern of \`[w·L, L − w·L]\` with dash offset
|
|
2536
|
+
\`−a·L\`, where \`L\` is the path's total length — the pattern's period
|
|
2537
|
+
equals \`L\`, so windows crossing the path's start wrap exactly.
|
|
2538
|
+
|
|
2539
|
+
#### 5.6.2. Path morphing
|
|
2540
|
+
|
|
2541
|
+
A path's \`d\` MAY be a keyframe array whose values are d-strings.
|
|
2542
|
+
Between two keyframes the path MORPHS when the pair is COMPATIBLE —
|
|
2543
|
+
identical command-letter sequences, equal numeric-argument counts, and
|
|
2544
|
+
no arc commands (\`A\`/\`a\`, whose boolean flags cannot interpolate):
|
|
2545
|
+
every numeric argument interpolates with the destination keyframe's
|
|
2546
|
+
easing. An INCOMPATIBLE pair SNAPS: the source value holds until the
|
|
2547
|
+
destination keyframe's time. No path normalization is performed — the
|
|
2548
|
+
protocol stays literal; authors export morph targets with matching
|
|
2549
|
+
command structure (the standard practice).
|
|
2550
|
+
|
|
2551
|
+
### 5.7. \`particles\`
|
|
2552
|
+
|
|
2553
|
+
A deterministic particle system with two modes: ballistic emission
|
|
2554
|
+
and target-point convergence.
|
|
2555
|
+
|
|
2556
|
+
\`\`\`ts
|
|
2557
|
+
interface ParticlesElement extends BaseElement {
|
|
2558
|
+
type: "particles";
|
|
2559
|
+
// Common
|
|
2560
|
+
size?: number; // pixels, default 12
|
|
2561
|
+
size_variation?: number; // 0..1, default 0.4
|
|
2562
|
+
particle_shape?: "square" | "circle";
|
|
2563
|
+
color?: string | string[]; // array randomizes per particle
|
|
2564
|
+
rotation_speed?: number; // deg/s
|
|
2565
|
+
lifetime?: number; // seconds per particle, default 1.5
|
|
2566
|
+
fade_at?: number; // 0..1 fraction of lifetime where fade begins, default 0.7
|
|
2567
|
+
|
|
2568
|
+
// Ballistic emission
|
|
2569
|
+
rate?: number; // particles per second
|
|
2570
|
+
velocity?: number; // initial speed px/s
|
|
2571
|
+
spread?: number; // cone in degrees, default 360
|
|
2572
|
+
direction?: number; // 0=right, 90=down, -90=up
|
|
2573
|
+
gravity?: number; // px/s², positive=down
|
|
2574
|
+
|
|
2575
|
+
// Depth (CKP/1.0, §5.7.3)
|
|
2576
|
+
z_velocity?: number; // px/s along the plane normal, default 0
|
|
2577
|
+
z_spread?: number; // uniform vz range width px/s, default 0
|
|
2578
|
+
|
|
2579
|
+
// Burst (used by both modes)
|
|
2580
|
+
burst?: boolean;
|
|
2581
|
+
burst_count?: number;
|
|
2582
|
+
|
|
2583
|
+
// Convergence (set target_points to enter convergence mode)
|
|
2584
|
+
target_points?: [number, number][]; // canvas-space targets
|
|
2585
|
+
convergence_easing?: EasingFunction;
|
|
2586
|
+
scatter_radius?: number; // disk radius around emitter
|
|
2587
|
+
}
|
|
2588
|
+
\`\`\`
|
|
2589
|
+
|
|
2590
|
+
#### 5.7.1. Determinism
|
|
2591
|
+
|
|
2592
|
+
Every particle's position, rotation, size, and color MUST be a pure
|
|
2593
|
+
function of \`(element.id, particle_index, age)\`. This means a runtime
|
|
2594
|
+
that seeks to time T MUST produce the same composition as a runtime
|
|
2595
|
+
that played continuously to T. The reference implementation seeds a
|
|
2596
|
+
PRNG with a hash of \`element.id\` and the particle index; third-party
|
|
2597
|
+
runtimes MAY use any algorithm that produces the same output as the
|
|
2598
|
+
reference.
|
|
2599
|
+
|
|
2600
|
+
#### 5.7.2. Convergence mode
|
|
2601
|
+
|
|
2602
|
+
When \`target_points\` is present and non-empty:
|
|
2603
|
+
|
|
2604
|
+
- Each particle \`n\` is assigned the target \`target_points[n % length]\`.
|
|
2605
|
+
- Each particle's start position is randomly placed within a disk of
|
|
2606
|
+
radius \`scatter_radius\` (default = \`max(canvas_width, canvas_height)\`)
|
|
2607
|
+
centered on \`(x, y)\`.
|
|
2608
|
+
- The particle's position is \`lerp(start, target, easing(age/lifetime))\`
|
|
2609
|
+
using \`convergence_easing\` (default \`"ease-out-quart"\`).
|
|
2610
|
+
|
|
2611
|
+
#### 5.7.3. Depth (CKP/1.0)
|
|
2612
|
+
|
|
2613
|
+
Per particle, \`vz = z_velocity + (r − 0.5) × z_spread\` with \`r\` the
|
|
2614
|
+
particle's uniform random draw, and its depth offset is \`vz × age\` px
|
|
2615
|
+
along the emitter plane's normal (+z toward the viewer, §4.4). The
|
|
2616
|
+
offset applies in BOTH modes (it is orthogonal to the in-plane
|
|
2617
|
+
position) and is part of the §5.7.1 determinism contract. There is no
|
|
2618
|
+
z gravity — \`gravity\` stays in-plane y. Like the \`z\` field (§4.4.2),
|
|
2619
|
+
depth has no visual effect without perspective in the chain, and
|
|
2620
|
+
particles draw in spawn order, never depth-sorted among themselves —
|
|
2621
|
+
the §4.4.3 camera sort orders whole elements, not a particle system's
|
|
2622
|
+
internal quads. With both fields absent or 0 the simulation is
|
|
2623
|
+
exactly the 2D one.
|
|
2624
|
+
|
|
2625
|
+
### 5.8. \`group\`
|
|
2626
|
+
|
|
2627
|
+
\`\`\`ts
|
|
2628
|
+
interface GroupElement extends BaseElement {
|
|
2629
|
+
type: "group";
|
|
2630
|
+
elements: Element[];
|
|
2631
|
+
clip?: boolean; // default false
|
|
2632
|
+
mask?: {
|
|
2633
|
+
mode: "alpha" | "alpha-inverted" | "luma" | "luma-inverted";
|
|
2634
|
+
elements: Element[];
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
\`\`\`
|
|
2638
|
+
|
|
2639
|
+
A positioned container. Children's \`x\`/\`y\` are coordinates in the
|
|
2640
|
+
group's LOCAL space, origin at the group's top-left box corner; a
|
|
2641
|
+
child's \`time\` is relative to the group's start; child \`layer\` and \`z\`
|
|
2642
|
+
order locally (§4.2). The group's transform (§4.4) and
|
|
2643
|
+
opacity stack multiplicatively onto children. Percentage/viewport units
|
|
2644
|
+
inside a group still resolve against the COMPOSITION canvas.
|
|
2645
|
+
|
|
2646
|
+
#### 5.8.1. Clipping
|
|
2647
|
+
|
|
2648
|
+
With \`clip: true\` (requires explicit \`width\` and \`height\`), children
|
|
2649
|
+
render into an offscreen layer the size of the group's box; pixels
|
|
2650
|
+
outside the box are discarded (CSS \`overflow: hidden\`). The group's
|
|
2651
|
+
own transform and opacity apply to the composited layer as a whole —
|
|
2652
|
+
opacity therefore applies ONCE to the flattened layer (overlapping
|
|
2653
|
+
semi-transparent children do not double-blend).
|
|
2654
|
+
|
|
2655
|
+
\`border_radius\` (px) rounds the clip box: children are masked to a
|
|
2656
|
+
rounded rectangle, matching a rounded card that clips its content (CSS
|
|
2657
|
+
\`overflow: hidden\` + \`border-radius\`). It is clamped to half the
|
|
2658
|
+
smaller box dimension and is ignored on an unclipped group. (Rounded
|
|
2659
|
+
clipping currently applies to the plain \`clip\` path; a \`mask\` group
|
|
2660
|
+
ignores \`border_radius\` since the mask layer already defines coverage.)
|
|
2661
|
+
|
|
2662
|
+
#### 5.8.2. Masks
|
|
2663
|
+
|
|
2664
|
+
The mask belongs to the group it masks — declared on the masked
|
|
2665
|
+
element, never inferred from siblings or layer adjacency. Mask
|
|
2666
|
+
\`elements\` render into a second box-sized layer using the same local
|
|
2667
|
+
coordinate space and timing rules as children, and may animate.
|
|
2668
|
+
The content layer composites through the mask layer per pixel:
|
|
2669
|
+
|
|
2670
|
+
\`\`\`
|
|
2671
|
+
factor = mask.alpha (alpha)
|
|
2672
|
+
= 1 − mask.alpha (alpha-inverted)
|
|
2673
|
+
= luminance(mask.rgb) (luma; Rec. 709 weights 0.2126/0.7152/0.0722,
|
|
2674
|
+
computed on premultiplied values)
|
|
2675
|
+
= 1 − luminance(mask.rgb) (luma-inverted)
|
|
2676
|
+
output = content × factor
|
|
2677
|
+
\`\`\`
|
|
2678
|
+
|
|
2679
|
+
\`mask\` requires explicit \`width\`/\`height\` and implies clipping (both
|
|
2680
|
+
layers are box-sized).
|
|
2681
|
+
|
|
2682
|
+
#### 5.8.3. Group time remapping
|
|
2683
|
+
|
|
2684
|
+
A group MAY carry \`time_remap\` — a keyframe array (§6.3 semantics)
|
|
2685
|
+
whose VALUES are warped local times in seconds. The group's SUBTREE
|
|
2686
|
+
runs on the warped clock:
|
|
2687
|
+
|
|
2688
|
+
\`\`\`
|
|
2689
|
+
local = t − group_start
|
|
2690
|
+
warped = max(0, interpolate(time_remap, local))
|
|
2691
|
+
\`\`\`
|
|
2692
|
+
|
|
2693
|
+
Children evaluate exactly as if the group's local time were \`warped\`:
|
|
2694
|
+
their \`time\` windows, animations, keyframes, transitions, and nested
|
|
2695
|
+
media all read the warped clock. Nested remapped groups compose (each
|
|
2696
|
+
warps its parent's clock in turn). The group's OWN animated properties
|
|
2697
|
+
(opacity, rotation, scale, position) read REAL time — the container
|
|
2698
|
+
moves on the composition's clock; only its contents are warped.
|
|
2699
|
+
|
|
2700
|
+
Flat segments freeze the subtree, steep segments speed-ramp it, and
|
|
2701
|
+
decreasing values run it backwards. Nested video decodes the frame at
|
|
2702
|
+
its warped media time (through §5.3.2's mapping). Audio inside a
|
|
2703
|
+
remapped subtree follows the varispeed rule (§5.3.2).
|
|
2704
|
+
|
|
2705
|
+
### 5.9. No nested-composition element
|
|
2706
|
+
|
|
2707
|
+
CKP deliberately has NO \`composition\` (pre-comp) element. Both things a
|
|
2708
|
+
pre-comp bundles are covered by orthogonal features on plain elements:
|
|
2709
|
+
|
|
2710
|
+
- **Reuse** is an authoring-time concern: template functions expand into
|
|
2711
|
+
plain elements before the Source is serialized (see \`@clipkit/patterns\`
|
|
2712
|
+
for the first-party library). The wire format stays fully decomposed —
|
|
2713
|
+
a runtime never resolves references or instantiates templates.
|
|
2714
|
+
- **Nested timing** is \`time_remap\` on a plain \`group\` (§5.8.3).
|
|
2715
|
+
|
|
2716
|
+
A Source containing \`type: "composition"\` is invalid under CKP/1.0.
|
|
2717
|
+
|
|
2718
|
+
---
|
|
2719
|
+
|
|
2720
|
+
## 6. Animation
|
|
2721
|
+
|
|
2722
|
+
Every animatable property may be driven in three ways: a static value,
|
|
2723
|
+
a named-preset animation, or a keyframe animation. These compose with
|
|
2724
|
+
the precedence:
|
|
2725
|
+
|
|
2726
|
+
\`\`\`
|
|
2727
|
+
keyframe_animation > named_animation > static_value
|
|
2728
|
+
\`\`\`
|
|
2729
|
+
|
|
2730
|
+
That is, if both a keyframe animation and a named animation target the
|
|
2731
|
+
same property at the same time, the keyframe wins.
|
|
2732
|
+
|
|
2733
|
+
### 6.1. Static values
|
|
2734
|
+
|
|
2735
|
+
The value as written in JSON. No interpolation; the value is used as-is
|
|
2736
|
+
for the entire duration the element is active.
|
|
2737
|
+
|
|
2738
|
+
### 6.2. Named animations
|
|
2739
|
+
|
|
2740
|
+
\`\`\`ts
|
|
2741
|
+
interface Animation {
|
|
2742
|
+
type: AnimationType;
|
|
2743
|
+
duration?: number; // seconds; defaults in §6.2.1
|
|
2744
|
+
easing?: Easing; // default "ease-out" unless noted
|
|
2745
|
+
time?: "start" | "end" | number; // start, relative to element.time
|
|
2746
|
+
|
|
2747
|
+
// Parameters read by specific types (ignored otherwise):
|
|
2748
|
+
frequency?: number; // Hz — shake (8), wiggle (2), text-wave (1.5)
|
|
2749
|
+
rotation?: number; // degrees — spin (360), wiggle amplitude (8),
|
|
2750
|
+
// text-flip start angle (90)
|
|
2751
|
+
distance?: number; // px — pan/shift (200), shake (24),
|
|
2752
|
+
// text-slide (40), text-fly (140), text-wave (12)
|
|
2753
|
+
direction?: "left" | "right" | "up" | "down"; // pan/shift/text-slide/text-fly
|
|
2754
|
+
scale?: number; // squash depth 0..1 (0.3)
|
|
2755
|
+
split?: "letter" | "word"; // text-* unit granularity (§6.5)
|
|
2756
|
+
stagger?: number; // text-* seconds between units (§6.5)
|
|
2757
|
+
axis?: "x" | "y" | "z"; // text-flip rotation axis (§6.5, CKP/1.0)
|
|
2758
|
+
}
|
|
2759
|
+
\`\`\`
|
|
2760
|
+
|
|
2761
|
+
\`time: "start"\` (or absent) resolves to local time \`0\`; \`"end"\` to
|
|
2762
|
+
\`element_duration − duration\`; a number is local seconds.
|
|
2763
|
+
|
|
2764
|
+
Normative tween recipes (deltas apply to the listed property; *relative*
|
|
2765
|
+
adds to the static value, *absolute* replaces it during the window):
|
|
2766
|
+
|
|
2767
|
+
| Type | Property | From → To | Mode | Notes |
|
|
2768
|
+
|---|---|---|---|---|
|
|
2769
|
+
| \`fade-in\` / \`fade-out\` | opacity | 0→1 / 1→0 | absolute | |
|
|
2770
|
+
| \`slide-left-in\` | x | −200→0 | relative | starts left, moves right into place |
|
|
2771
|
+
| \`slide-right-in\` | x | +200→0 | relative | |
|
|
2772
|
+
| \`slide-up-in\` | y | +200→0 | relative | starts below, rises |
|
|
2773
|
+
| \`slide-down-in\` | y | −200→0 | relative | |
|
|
2774
|
+
| \`slide-*-out\` | x/y | 0→±200 | relative | motion direction matches the name |
|
|
2775
|
+
| \`scale-in\` / \`scale-out\` | scale | 0→1 / 1→0 | absolute | |
|
|
2776
|
+
| \`rotate-in\` / \`rotate-out\` | rotation | −90→0 / 0→+90 | relative | |
|
|
2777
|
+
| \`bounce-in\` / \`bounce-out\` | scale | 0→1 / 1→0 | absolute | default easing \`ease-out-back\` / \`ease-in-back\` |
|
|
2778
|
+
| \`spin\` | rotation | 0→\`rotation\` | relative | default easing \`linear\` |
|
|
2779
|
+
| \`shake\` | x | oscillation, amplitude \`distance\`→0 | relative | \`sin(2π·frequency·t)\` × eased envelope |
|
|
2780
|
+
| \`wiggle\` | rotation | oscillation, constant amplitude \`rotation\` | relative | same formula |
|
|
2781
|
+
| \`squash\` | y_scale, x_scale | 1→1−\`scale\`→1; 1→1+0.6·\`scale\`→1 | absolute | two half-duration phases; in \`ease-in-quad\`, out \`ease-out-back\` |
|
|
2782
|
+
| \`pan\` | x or y | −\`distance\`/2→+\`distance\`/2 along \`direction\` | relative | drifts through rest position; default easing \`linear\` |
|
|
2783
|
+
| \`shift\` | x or y | 0→\`distance\` along \`direction\` | relative | **fill-forward**: the end value holds for the rest of the element's life |
|
|
2784
|
+
| \`drift\` | x, y | smooth random walk, amplitude \`distance\` (30) | relative | offsets = \`(noise1d(frequency·t, seed) − 0.5) × 2 × distance\` per axis (y uses \`seed + 7919\`); \`frequency\` default 0.5, \`seed\` default 0 |
|
|
2785
|
+
| \`breathe\` | scale | oscillation, amplitude \`scale\` (0.05) | relative | \`scale × sin(2π·frequency·t)\`, \`frequency\` default 0.4 |
|
|
2786
|
+
| \`orbit\` | x, y | circle of radius \`distance\` (40) | relative | \`x += r·sin(2πft + π/2)\`, \`y += ±r·sin(2πft)\` (\`direction: "left"\` flips y = counter-clockwise); \`frequency\` default 0.5 rev/s |
|
|
2787
|
+
| \`text-*\` | per-unit | — | — | §6.5 |
|
|
2788
|
+
|
|
2789
|
+
\`drift\`'s noise is NORMATIVE — the 1D form of §4.7's lattice noise:
|
|
2790
|
+
\`noise1d(x, seed)\` is the quintic-faded linear interpolation between
|
|
2791
|
+
\`h(⌊x⌋)\` and \`h(⌊x⌋+1)\` where \`h(i) = pcg(i XOR pcg(seed)) / (2³²−1)\`
|
|
2792
|
+
and \`pcg\` is §4.7's hash. Same seed → identical motion everywhere.
|
|
2793
|
+
|
|
2794
|
+
#### 6.2.1. Duration defaults
|
|
2795
|
+
|
|
2796
|
+
\`duration\` defaults to \`0.5\`, EXCEPT \`spin\`, \`shake\`, \`wiggle\`, \`pan\`,
|
|
2797
|
+
\`drift\`, \`breathe\`, \`orbit\` and \`text-wave\`, which default to the
|
|
2798
|
+
element's full duration when both \`time\` and \`duration\` are omitted
|
|
2799
|
+
(they read as "for the element's life"). Outside its window a tween stops contributing (the property
|
|
2800
|
+
returns to its static value), except \`shift\`'s documented fill-forward.
|
|
2801
|
+
|
|
2802
|
+
### 6.3. Keyframe animations
|
|
2803
|
+
|
|
2804
|
+
\`\`\`ts
|
|
2805
|
+
interface KeyframeAnimation {
|
|
2806
|
+
property: string; // name of the BaseElement field
|
|
2807
|
+
loop?: boolean | "ping-pong"; // repeat the pattern (see below)
|
|
2808
|
+
keyframes: Keyframe[]; // monotonically increasing times
|
|
2809
|
+
easing?: EasingFunction; // default per-keyframe
|
|
2810
|
+
}
|
|
2811
|
+
\`\`\`
|
|
2812
|
+
|
|
2813
|
+
For times before the first keyframe, the value is clamped to the first
|
|
2814
|
+
keyframe's value. After the last keyframe, clamped to the last value.
|
|
2815
|
+
Between keyframes \`i\` and \`i+1\`, the value is interpolated using the
|
|
2816
|
+
easing on keyframe \`i+1\` (or the animation's \`easing\` if not specified
|
|
2817
|
+
per-keyframe). A single-keyframe track is a constant.
|
|
2818
|
+
|
|
2819
|
+
#### 6.3.1. Color keyframes
|
|
2820
|
+
|
|
2821
|
+
When EVERY keyframe \`value\` in a track parses as a color (§3.4 — \`#…\`,
|
|
2822
|
+
\`rgb(…)\`, \`rgba(…)\`), the track is a *color track*: values interpolate
|
|
2823
|
+
componentwise in straight-alpha RGB space (alpha included), using the
|
|
2824
|
+
same easing rules. Color tracks are honored on \`fill_color\` (shape and
|
|
2825
|
+
plain text) and \`stroke_color\` (shape). Unparseable colors fall back to
|
|
2826
|
+
opaque white anywhere colors are parsed.
|
|
2827
|
+
|
|
2828
|
+
With \`loop\`, local time folds before interpolation: \`true\` wraps —
|
|
2829
|
+
\`t' = t mod span\` — and \`"ping-pong"\` reflects — \`t' = span − |((t mod
|
|
2830
|
+
2·span)) − span|\` — where \`span\` is the LAST keyframe's \`time\`. The
|
|
2831
|
+
pattern repeats for the element's whole life; without \`loop\`, time
|
|
2832
|
+
past the last keyframe holds the final value (unchanged default).
|
|
2833
|
+
Looping applies to scalar, color, and \`position\` keyframe animations.
|
|
2834
|
+
|
|
2835
|
+
### 6.4. Easing functions
|
|
2836
|
+
|
|
2837
|
+
CKP/1.0 defines 36 named easing functions plus two parametric forms.
|
|
2838
|
+
Mathematical definitions are in the reference implementation
|
|
2839
|
+
(\`packages/runtime/src/animation/easings.ts\`); the polynomial/sine/expo/
|
|
2840
|
+
circ/back families follow easings.net.
|
|
2841
|
+
|
|
2842
|
+
\`\`\`
|
|
2843
|
+
linear
|
|
2844
|
+
|
|
2845
|
+
ease, ease-in, ease-out, ease-in-out
|
|
2846
|
+
ease-in-cubic, ease-out-cubic, ease-in-out-cubic
|
|
2847
|
+
ease-in-quad, ease-out-quad, ease-in-out-quad
|
|
2848
|
+
ease-in-quart, ease-out-quart, ease-in-out-quart
|
|
2849
|
+
ease-in-quint, ease-out-quint, ease-in-out-quint
|
|
2850
|
+
ease-in-sine, ease-out-sine, ease-in-out-sine
|
|
2851
|
+
ease-in-expo, ease-out-expo, ease-in-out-expo
|
|
2852
|
+
ease-in-circ, ease-out-circ, ease-in-out-circ
|
|
2853
|
+
ease-in-back, ease-out-back, ease-in-out-back
|
|
2854
|
+
|
|
2855
|
+
elastic-in, elastic-out, elastic-in-out (decaying sinusoidal overshoot)
|
|
2856
|
+
bounce-in, bounce-out, bounce-in-out (piecewise-parabolic ball drop)
|
|
2857
|
+
|
|
2858
|
+
spring (damped harmonic oscillator: mass=1, damping=10, stiffness=100;
|
|
2859
|
+
~5% overshoot then settles. Remotion's signature feel.)
|
|
2860
|
+
\`\`\`
|
|
2861
|
+
|
|
2862
|
+
Parametric forms (string-valued):
|
|
2863
|
+
|
|
2864
|
+
- \`cubic-bezier(x1, y1, x2, y2)\` — CSS timing-function semantics;
|
|
2865
|
+
\`x1\`/\`x2\` MUST be clamped to \`[0, 1]\`, \`y1\`/\`y2\` are unbounded.
|
|
2866
|
+
- \`steps(n)\` — \`n\` equidistant steps, jump-at-end (CSS \`steps(n, end)\`).
|
|
2867
|
+
|
|
2868
|
+
Unknown easing names MUST fall back to \`linear\` (never error). Output
|
|
2869
|
+
MUST match the reference within ±0.001 at any input value in \`[0, 1]\`.
|
|
2870
|
+
|
|
2871
|
+
### 6.5. Per-unit text animations
|
|
2872
|
+
|
|
2873
|
+
The \`text-*\` animation types apply to \`text\` elements only (ignored on
|
|
2874
|
+
other element types; \`caption\` elements have their own kinetic system).
|
|
2875
|
+
The text splits into *units* and each unit runs the same animation,
|
|
2876
|
+
offset in time by \`stagger\` seconds per unit index.
|
|
2877
|
+
|
|
2878
|
+
**Unit indexing.** Letter index counts drawn glyphs (whitespace
|
|
2879
|
+
excluded); word index counts whitespace-separated runs. Both run
|
|
2880
|
+
continuously across spans and line breaks. Unit \`u\` starts at
|
|
2881
|
+
\`time + u × stagger\`.
|
|
2882
|
+
|
|
2883
|
+
**Defaults.** \`split\`: \`"word"\` for \`text-appear\`/\`text-slide\`/
|
|
2884
|
+
\`text-fly\`, \`"letter"\` for \`text-typewriter\`/\`text-wave\`/\`text-flip\`.
|
|
2885
|
+
\`stagger\`: \`0.09\` for word splits, \`0.035\` for letter splits.
|
|
2886
|
+
Per-unit \`duration\` default \`0.5\`.
|
|
2887
|
+
|
|
2888
|
+
| Type | Per-unit effect | Defaults |
|
|
2889
|
+
|---|---|---|
|
|
2890
|
+
| \`text-appear\` | opacity 0→1 over \`duration\` | easing \`ease-out-cubic\` |
|
|
2891
|
+
| \`text-slide\` | opacity 0→1 + displaced \`distance\` px opposite \`direction\`, settling at rest | \`distance\` 40, \`direction\` up, easing \`ease-out-cubic\` |
|
|
2892
|
+
| \`text-fly\` | as \`text-slide\`, farther | \`distance\` 140, easing \`ease-out-back\` |
|
|
2893
|
+
| \`text-typewriter\` | opacity steps 0→1 at the unit's start time | no fade |
|
|
2894
|
+
| \`text-wave\` | y offset \`distance × sin(2π·frequency·t − 0.6·u)\` | \`distance\` 12, \`frequency\` 1.5; full-length default (§6.2.1) |
|
|
2895
|
+
| \`text-flip\` | opacity 0→1 + 3D rotation \`rotation × (1−eased)\` degrees about \`axis\` through the unit's center, settling flat (CKP/1.0) | \`rotation\` 90, \`axis\` \`"x"\`, easing \`ease-out-cubic\` |
|
|
2896
|
+
|
|
2897
|
+
Per-unit effects fold into the glyph's tint (opacity) and an
|
|
2898
|
+
element-local offset applied BEFORE the element transform (§4.4), so
|
|
2899
|
+
kinetic type composes with scale/skew/rotation.
|
|
2900
|
+
|
|
2901
|
+
**\`text-flip\` semantics (CKP/1.0).** The rotation pivot is the unit's
|
|
2902
|
+
rest-layout center — the glyph cell's center for letter splits, the
|
|
2903
|
+
word's glyph bounding-box center for word splits — translated by the
|
|
2904
|
+
unit's current per-unit offset, so a unit sliding and flipping stays
|
|
2905
|
+
rigid. A word split rotates the word as ONE slab (its glyphs never
|
|
2906
|
+
splay). When multiple \`text-flip\` animations target an element, word
|
|
2907
|
+
rotations compose OUTSIDE letter rotations. Axis \`"x"\` flips up
|
|
2908
|
+
(rotation about the horizontal axis), \`"y"\` swings in, \`"z"\` spins
|
|
2909
|
+
in-plane. Like all 3D (§4.4.2), the depth component is orthographic
|
|
2910
|
+
without a \`camera\`; the foreshortening is \`cos θ\` exactly. An active
|
|
2911
|
+
\`text-flip\` puts the text element on the full-matrix path; elements
|
|
2912
|
+
without one are unaffected (§4.4.3 cost rules).
|
|
2913
|
+
|
|
2914
|
+
CKP/1.0 defines text animations as entrances only: \`time: "end"\` on a
|
|
2915
|
+
\`text-*\` animation MUST be ignored.
|
|
2916
|
+
|
|
2917
|
+
### 6.6. Transitions (non-feature)
|
|
2918
|
+
|
|
2919
|
+
CKP/1.0 deliberately defines NO transition primitive — every transition
|
|
2920
|
+
decomposes into existing primitives, and the document is exactly what
|
|
2921
|
+
renders:
|
|
2922
|
+
|
|
2923
|
+
- **Crossfades, pushes, zoom swaps** — two overlapping elements with
|
|
2924
|
+
paired animations (AGENTS.md §"Transitions").
|
|
2925
|
+
- **Wipes (circular, linear, stripe, soft-edged)** — the incoming slide
|
|
2926
|
+
inside a masked group (§5.8.2) whose mask elements animate: a growing
|
|
2927
|
+
ellipse, a sweeping rectangle, a luma gradient band (AGENTS.md
|
|
2928
|
+
§"Wipes").
|
|
2929
|
+
|
|
2930
|
+
An earlier draft reserved a first-class two-layer transition object for
|
|
2931
|
+
wipes; group masks made it unnecessary and it is no longer planned.
|
|
2932
|
+
|
|
2933
|
+
### 6.7. Spatial motion paths
|
|
2934
|
+
|
|
2935
|
+
A \`keyframe_animations\` entry with \`property: "position"\` moves the
|
|
2936
|
+
element along a path; it overrides the element's \`x\` and \`y\` (and
|
|
2937
|
+
any scalar \`x\`/\`y\` keyframe animations). Keyframe \`value\`s are
|
|
2938
|
+
\`[x, y]\` pairs in canvas pixels — or \`[x, y, z]\` triples for a 3D
|
|
2939
|
+
path (z in pixels, +z toward the viewer, §4.4). A 3D path
|
|
2940
|
+
additionally overrides the element's \`z\` (and any scalar \`z\`
|
|
2941
|
+
keyframe animations); a 2D path leaves \`z\` untouched. All keyframes
|
|
2942
|
+
of one position path MUST agree in dimensionality — mixing \`[x, y]\`
|
|
2943
|
+
and \`[x, y, z]\` in one animation is a validation error (no silent
|
|
2944
|
+
z = 0 promotion).
|
|
2945
|
+
|
|
2946
|
+
\`\`\`json
|
|
2947
|
+
{ "property": "position", "auto_orient": true, "keyframes": [
|
|
2948
|
+
{ "time": 0, "value": [200, 800], "out_tangent": [240, -300] },
|
|
2949
|
+
{ "time": 2, "value": [1700, 300], "in_tangent": [-200, -120],
|
|
2950
|
+
"easing": "ease-in-out" }
|
|
2951
|
+
] }
|
|
2952
|
+
\`\`\`
|
|
2953
|
+
|
|
2954
|
+
Each consecutive keyframe pair is one CUBIC BEZIER segment with
|
|
2955
|
+
control points
|
|
2956
|
+
|
|
2957
|
+
\`\`\`
|
|
2958
|
+
P0 = a.value P3 = b.value
|
|
2959
|
+
P1 = P0 + a.out_tangent P2 = P3 + b.in_tangent
|
|
2960
|
+
\`\`\`
|
|
2961
|
+
|
|
2962
|
+
where an omitted tangent defaults to the straight-line third-point
|
|
2963
|
+
(\`P1 = P0 + (P3−P0)/3\`, \`P2 = P3 − (P3−P0)/3\`) — a path with no
|
|
2964
|
+
handles is exact polyline motion. On a 3D path, tangents are
|
|
2965
|
+
\`[dx, dy]\` or \`[dx, dy, dz]\`; a 2-component tangent's \`dz\` defaults
|
|
2966
|
+
to the straight-line third-point in z (the omitted-handle rule,
|
|
2967
|
+
applied per axis). 3-component tangents on a 2D path are a
|
|
2968
|
+
validation error.
|
|
2969
|
+
|
|
2970
|
+
Travel is ARC-LENGTH parameterized (NORMATIVE): the destination
|
|
2971
|
+
keyframe's easing maps segment-local time to a fraction \`u\` of the
|
|
2972
|
+
segment's length; the bezier parameter is found on a 64-chord
|
|
2973
|
+
cumulative-length table (curve sampled at \`t = i/64\`, \`i = 0…64\`;
|
|
2974
|
+
linear interpolation between chords). With linear easing the element
|
|
2975
|
+
travels at constant speed however the handles stretch the curve's
|
|
2976
|
+
parameterization. On a 3D path the chord lengths are measured on the
|
|
2977
|
+
3D curve — constant speed means constant speed through depth too.
|
|
2978
|
+
Before the first keyframe the element holds the first point; after
|
|
2979
|
+
the last, the last point.
|
|
2980
|
+
|
|
2981
|
+
\`auto_orient: true\` adds the path's travel direction —
|
|
2982
|
+
\`atan2(dy, dx)\` of the bezier derivative at the sampled parameter, in
|
|
2983
|
+
degrees — to the element's own \`rotation\`. Orientation is STRICTLY
|
|
2984
|
+
IN-PLANE: on a 3D path the tangent's xy projection is used and \`dz\`
|
|
2985
|
+
is ignored — auto_orient never derives \`x_rotation\` or \`y_rotation\`
|
|
2986
|
+
(a path does not tilt the element's plane). A zero xy derivative
|
|
2987
|
+
(z-only travel or coincident control points) falls back to the
|
|
2988
|
+
segment chord's xy projection. Position values are numbers (pixels);
|
|
2989
|
+
length strings are not valid inside path keyframes.
|
|
2990
|
+
|
|
2991
|
+
Like the \`z\` field itself (§4.4.2), path z has no visual effect
|
|
2992
|
+
without perspective somewhere in the chain — under no camera the
|
|
2993
|
+
element renders at the path's xy projection.
|
|
2994
|
+
|
|
2995
|
+
---
|
|
2996
|
+
|
|
2997
|
+
## 7. Time, duration, sequencing
|
|
2998
|
+
|
|
2999
|
+
### 7.1. Element activity windows
|
|
3000
|
+
|
|
3001
|
+
An element is *active* at composition time \`t\` if:
|
|
3002
|
+
|
|
3003
|
+
\`\`\`
|
|
3004
|
+
start <= t <= start + duration
|
|
3005
|
+
\`\`\`
|
|
3006
|
+
|
|
3007
|
+
where:
|
|
3008
|
+
|
|
3009
|
+
- \`start = element.time\` (default \`0\`).
|
|
3010
|
+
- \`duration = element.duration\` if numeric, else the composition's
|
|
3011
|
+
remaining time if \`"auto"\` or \`"end"\`.
|
|
3012
|
+
|
|
3013
|
+
Inactive elements MUST NOT be rendered.
|
|
3014
|
+
|
|
3015
|
+
### 7.2. Local time
|
|
3016
|
+
|
|
3017
|
+
Many properties (keyframes, named-animation timing, particle simulation)
|
|
3018
|
+
operate in *local time* — seconds elapsed since the element
|
|
3019
|
+
became active. Local time \`0\` corresponds to composition time
|
|
3020
|
+
\`element.time\`.
|
|
3021
|
+
|
|
3022
|
+
For animation evaluation, local time MUST be clamped to the element's
|
|
3023
|
+
duration:
|
|
3024
|
+
|
|
3025
|
+
\`\`\`
|
|
3026
|
+
local_t = min(t − element.time, element_duration)
|
|
3027
|
+
\`\`\`
|
|
3028
|
+
|
|
3029
|
+
Rationale: the activity check (§7.1) computes the element's end as
|
|
3030
|
+
\`time + duration\` while local time computes \`t − time\`; at exact frame
|
|
3031
|
+
boundaries these two float roundings can disagree by ~1 ulp, leaving an
|
|
3032
|
+
element active at a local time fractionally PAST its duration — which
|
|
3033
|
+
would skip every end-anchored animation for one frame and flash the
|
|
3034
|
+
static value. The clamp makes the boundary frame well-defined.
|
|
3035
|
+
|
|
3036
|
+
### 7.3. The composition duration
|
|
3037
|
+
|
|
3038
|
+
If \`Source.duration\` is \`"auto"\`, the composition's effective duration
|
|
3039
|
+
is the maximum end time across all elements. Otherwise, it is the
|
|
3040
|
+
declared value.
|
|
3041
|
+
|
|
3042
|
+
Runtimes MUST produce frames from composition time \`0\` to \`duration\`
|
|
3043
|
+
inclusive of \`0\`, exclusive of \`duration\`. At 30 fps and \`duration: 5\`
|
|
3044
|
+
this produces 150 frames at times \`0, 1/30, 2/30, ..., 149/30\`.
|
|
3045
|
+
|
|
3046
|
+
---
|
|
3047
|
+
|
|
3048
|
+
## 8. Asset references
|
|
3049
|
+
|
|
3050
|
+
Asset-bearing elements (\`image\`, \`video\`, \`audio\`) carry a \`source\`
|
|
3051
|
+
string identifying the asset. The protocol does not embed binaries.
|
|
3052
|
+
|
|
3053
|
+
### 8.1. Allowed schemes
|
|
3054
|
+
|
|
3055
|
+
Conforming runtimes MUST attempt to resolve:
|
|
3056
|
+
|
|
3057
|
+
- \`https://\` URLs
|
|
3058
|
+
- \`http://\` URLs (MAY require user opt-in for mixed content)
|
|
3059
|
+
|
|
3060
|
+
Conforming runtimes MAY additionally support:
|
|
3061
|
+
|
|
3062
|
+
- \`file://\` URLs (local file references)
|
|
3063
|
+
- Absolute paths (\`/path/to/file\`)
|
|
3064
|
+
- Relative paths (\`./asset.png\`) — resolved against an
|
|
3065
|
+
implementation-defined base
|
|
3066
|
+
- \`data:\` URIs
|
|
3067
|
+
|
|
3068
|
+
Runtimes MUST fail with a clear error when a \`source\` cannot be
|
|
3069
|
+
resolved. They MUST NOT silently substitute a placeholder.
|
|
3070
|
+
|
|
3071
|
+
### 8.2. Preloading
|
|
3072
|
+
|
|
3073
|
+
Runtimes SHOULD provide a \`preload\` step that resolves all asset
|
|
3074
|
+
references before rendering begins. This is REQUIRED for export
|
|
3075
|
+
conformance (§10.3) — exports cannot tolerate runtime asset failures.
|
|
3076
|
+
|
|
3077
|
+
---
|
|
3078
|
+
|
|
3079
|
+
## 9. Output and rendering
|
|
3080
|
+
|
|
3081
|
+
### 9.1. Determinism
|
|
3082
|
+
|
|
3083
|
+
Given identical \`(Source, time)\` inputs, every conforming runtime MUST
|
|
3084
|
+
produce visually equivalent frames. "Visually equivalent" means:
|
|
3085
|
+
|
|
3086
|
+
- Same element positions, sizes, rotations, opacities to within ±0.5 px.
|
|
3087
|
+
- Same animation values to within ±0.001 for normalized properties.
|
|
3088
|
+
- Same particle state (positions, alphas, sizes) to within ±0.5 px /
|
|
3089
|
+
±0.001 alpha.
|
|
3090
|
+
- Color output MAY differ by up to ~1/255 per channel due to
|
|
3091
|
+
rasterization backends. Pixel-exact equivalence is NOT REQUIRED.
|
|
3092
|
+
|
|
3093
|
+
### 9.2. Frame timing
|
|
3094
|
+
|
|
3095
|
+
A runtime asked to produce frame \`f\` of a composition with \`frame_rate\`
|
|
3096
|
+
FPS MUST render the scene state at composition time:
|
|
3097
|
+
|
|
3098
|
+
\`\`\`
|
|
3099
|
+
t = f / frame_rate
|
|
3100
|
+
\`\`\`
|
|
3101
|
+
|
|
3102
|
+
\`f = 0\` is the first frame.
|
|
3103
|
+
|
|
3104
|
+
### 9.3. Output formats
|
|
3105
|
+
|
|
3106
|
+
| Format | Behavior |
|
|
3107
|
+
|---|---|
|
|
3108
|
+
| \`mp4\` | Encoded video. H.264 baseline + AAC audio is the recommended baseline. Higher profiles MAY be used. |
|
|
3109
|
+
| \`gif\` | Animated GIF. Audio is silently dropped. |
|
|
3110
|
+
|
|
3111
|
+
Runtimes MAY support output formats beyond these.
|
|
3112
|
+
|
|
3113
|
+
---
|
|
3114
|
+
|
|
3115
|
+
## 10. Conformance levels
|
|
3116
|
+
|
|
3117
|
+
CKP defines three nested conformance levels. An implementation MAY claim
|
|
3118
|
+
any level it actually supports.
|
|
3119
|
+
|
|
3120
|
+
### 10.1. Level 1 — Validation
|
|
3121
|
+
|
|
3122
|
+
The implementation MUST be able to parse a Source JSON object and
|
|
3123
|
+
report whether it is a valid CKP/1.0 document. Specifically:
|
|
3124
|
+
|
|
3125
|
+
- Accept any valid document per §2–8.
|
|
3126
|
+
- Reject documents with invalid \`type\` discriminators on required
|
|
3127
|
+
fields, missing REQUIRED fields, or out-of-range values.
|
|
3128
|
+
- Tolerate unknown additional fields (forward compatibility, §2.2).
|
|
3129
|
+
|
|
3130
|
+
The \`@clipkit/protocol\` package provides Level 1 validation as a
|
|
3131
|
+
reference.
|
|
3132
|
+
|
|
3133
|
+
### 10.2. Level 2 — Rendering
|
|
3134
|
+
|
|
3135
|
+
The implementation MUST also be able to produce frame images from a
|
|
3136
|
+
valid Source. Specifically:
|
|
3137
|
+
|
|
3138
|
+
- All element types (§5) MUST render.
|
|
3139
|
+
- All animations (§6) MUST animate.
|
|
3140
|
+
- Output MUST satisfy the determinism requirements (§9.1).
|
|
3141
|
+
|
|
3142
|
+
A Level 2 implementation MUST NOT silently skip element types unless
|
|
3143
|
+
the document is at a higher protocol version than the implementation
|
|
3144
|
+
supports.
|
|
3145
|
+
|
|
3146
|
+
### 10.3. Level 3 — Export
|
|
3147
|
+
|
|
3148
|
+
The implementation MUST also be able to produce encoded output:
|
|
3149
|
+
typically MP4 with mixed audio.
|
|
3150
|
+
|
|
3151
|
+
- All Level 2 requirements.
|
|
3152
|
+
- Audio elements (§5.4) MUST be mixed into the output track.
|
|
3153
|
+
- Output duration MUST match \`Source.duration\` precisely (frame-accurate).
|
|
3154
|
+
|
|
3155
|
+
The reference runtime \`@clipkit/runtime\` is a Level 3 implementation: it
|
|
3156
|
+
sums all sources to the master bus, then applies a fixed peak limiter
|
|
3157
|
+
(transparent below 0 dBFS) so a hot mix is contained rather than
|
|
3158
|
+
hard-clipped. The same limiter runs in preview, so what you hear matches
|
|
3159
|
+
the render. The exact mix algorithm otherwise remains
|
|
3160
|
+
implementation-defined (§1).
|
|
3161
|
+
|
|
3162
|
+
---
|
|
3163
|
+
|
|
3164
|
+
## 11. Versioning and extensions
|
|
3165
|
+
|
|
3166
|
+
### 11.1. The \`clipkit_version\` field
|
|
3167
|
+
|
|
3168
|
+
Documents SHOULD declare their protocol version:
|
|
3169
|
+
|
|
3170
|
+
\`\`\`json
|
|
3171
|
+
{ "clipkit_version": "1.0", "elements": [...] }
|
|
3172
|
+
\`\`\`
|
|
3173
|
+
|
|
3174
|
+
Absence is interpreted as \`"1.0"\` for the lifetime of CKP/1.x.
|
|
3175
|
+
|
|
3176
|
+
### 11.2. Version compatibility
|
|
3177
|
+
|
|
3178
|
+
Versions follow \`MAJOR.MINOR\` (semver-style without patch):
|
|
3179
|
+
|
|
3180
|
+
- **Same MINOR**: runtimes MUST render documents at the same minor
|
|
3181
|
+
version with no warning.
|
|
3182
|
+
- **Higher MINOR**: runtimes MUST attempt to render. They SHOULD warn
|
|
3183
|
+
that unknown fields may be ignored.
|
|
3184
|
+
- **Higher MAJOR**: runtimes MUST refuse to render and report the
|
|
3185
|
+
version mismatch. Major versions indicate breaking changes.
|
|
3186
|
+
- **Lower MAJOR**: runtimes MUST render if they implement the older
|
|
3187
|
+
major version. Backward compatibility within a major line is
|
|
3188
|
+
permanent.
|
|
3189
|
+
|
|
3190
|
+
### 11.3. Adding fields
|
|
3191
|
+
|
|
3192
|
+
Any minor version MAY add fields to existing element types. Unknown
|
|
3193
|
+
fields in older runtimes pass through harmlessly per §2.2.
|
|
3194
|
+
|
|
3195
|
+
### 11.4. Adding element types
|
|
3196
|
+
|
|
3197
|
+
Any minor version MAY add new element types. Runtimes that do not
|
|
3198
|
+
recognize a new \`type\` MAY skip the element with a warning.
|
|
3199
|
+
|
|
3200
|
+
### 11.5. Breaking changes
|
|
3201
|
+
|
|
3202
|
+
Removing fields, changing field types, or changing the meaning of an
|
|
3203
|
+
existing field requires a major-version bump.
|
|
3204
|
+
|
|
3205
|
+
### 11.6. Extension namespace
|
|
3206
|
+
|
|
3207
|
+
Implementations and tools MAY include vendor-specific fields prefixed
|
|
3208
|
+
with \`x_\`. The protocol reserves bare names; \`x_\` names are
|
|
3209
|
+
implementation-defined and ignored by other implementations. Names
|
|
3210
|
+
the protocol itself defines (e.g. \`x_scale\`, \`x_skew\`, \`x_rotation\`)
|
|
3211
|
+
are bare protocol names, not extensions, regardless of prefix.
|
|
3212
|
+
|
|
3213
|
+
### 11.7. Version history
|
|
3214
|
+
|
|
3215
|
+
| Version | Additions |
|
|
3216
|
+
|---|---|
|
|
3217
|
+
| 1.0 | Initial protocol, including the 3D transform model (§4.4): \`x_rotation\` / \`y_rotation\` / \`z_rotation\` (alias of \`rotation\`) / \`z\` on every element; Source-level \`camera\`; paint-order + flattening compositing rules (§4.4.3); glass under 3D via the pane-plane homography (§4.7); 3D motion paths (\`[x, y, z]\` position keyframes, §6.7); \`text-flip\` per-unit 3D reveals (§6.5); particle depth (\`z_velocity\` / \`z_spread\`, §5.7.3). These are opt-in and additive — documents that use none of them render the same as a pure-2D source. |
|
|
3218
|
+
|
|
3219
|
+
---
|
|
3220
|
+
|
|
3221
|
+
## 12. Implementation notes (non-normative)
|
|
3222
|
+
|
|
3223
|
+
These notes are advisory. They reflect lessons from the reference
|
|
3224
|
+
implementation and MAY help third-party implementers avoid pitfalls.
|
|
3225
|
+
|
|
3226
|
+
### 12.1. Premultiplied alpha
|
|
3227
|
+
|
|
3228
|
+
All blending in the reference runtime uses premultiplied alpha:
|
|
3229
|
+
textures are uploaded premultiplied, shaders output premultiplied
|
|
3230
|
+
values, and the canvas swap-chain is configured with
|
|
3231
|
+
\`alphaMode: "premultiplied"\`. This avoids the "dark halo" artifacts
|
|
3232
|
+
that appear with straight-alpha blending. Third-party runtimes are
|
|
3233
|
+
free to use any blending convention internally as long as final output
|
|
3234
|
+
matches §9.1.
|
|
3235
|
+
|
|
3236
|
+
### 12.2. Corner radii on non-square rectangles
|
|
3237
|
+
|
|
3238
|
+
If you implement rounded rectangles with a signed-distance function
|
|
3239
|
+
in shaders, the SDF MUST operate in pixel space, not in normalized UV
|
|
3240
|
+
space. UV space is anisotropic for non-square rectangles, so doing the
|
|
3241
|
+
corner math in UV stretches the arc into an ellipse. Pass the
|
|
3242
|
+
rectangle's \`(width, height)\` to the shader and convert
|
|
3243
|
+
\`uv * size\` before the SDF.
|
|
3244
|
+
|
|
3245
|
+
### 12.3. Font atlases
|
|
3246
|
+
|
|
3247
|
+
The reference runtime generates a glyph atlas per (family, size,
|
|
3248
|
+
weight). It covers ASCII (0x20–0x7E) only; characters outside this
|
|
3249
|
+
range are silently dropped. Third-party implementations MAY support
|
|
3250
|
+
larger glyph ranges; documents SHOULD avoid relying on non-ASCII text
|
|
3251
|
+
in v1.0 unless the target runtime is known to support it.
|
|
3252
|
+
|
|
3253
|
+
### 12.4. Particle PRNG
|
|
3254
|
+
|
|
3255
|
+
The reference runtime uses a \`mulberry32\` PRNG seeded by
|
|
3256
|
+
\`FNV-1a(element.id) + n * 0x9e3779b9\`. Third-party runtimes are NOT
|
|
3257
|
+
required to match this exact PRNG, only the *outputs* implied by §5.7.1.
|
|
3258
|
+
|
|
3259
|
+
---
|
|
3260
|
+
|
|
3261
|
+
## Appendix A: Reference implementation map
|
|
3262
|
+
|
|
3263
|
+
| Spec section | Reference implementation |
|
|
3264
|
+
|---|---|
|
|
3265
|
+
| §2 Source | \`packages/protocol/src/types.ts\` (Source) + \`zod.ts\` (sourceSchema) |
|
|
3266
|
+
| §3 Units | \`packages/runtime/src/compositor/unit.ts\` |
|
|
3267
|
+
| §4 Element model | \`packages/protocol/src/types.ts\` (BaseElement) |
|
|
3268
|
+
| §5.1 shape | \`packages/runtime/src/compositor/element-renderers/shape.ts\` |
|
|
3269
|
+
| §5.2 text | \`packages/runtime/src/compositor/element-renderers/text.ts\` |
|
|
3270
|
+
| §5.3–5.4 video/image/audio | \`packages/runtime/src/{compositor/element-renderers,audio}/\` |
|
|
3271
|
+
| §5.5 caption | \`packages/runtime/src/compositor/element-renderers/caption.ts\` |
|
|
3272
|
+
| §5.6 shape paths | \`packages/runtime/src/svg/svg-renderer.ts\` |
|
|
3273
|
+
| §5.7 particles | \`packages/runtime/src/compositor/element-renderers/particles.ts\` |
|
|
3274
|
+
| §6 Animation | \`packages/runtime/src/animation/\` |
|
|
3275
|
+
| §10 Conformance | \`packages/protocol/src/validate.ts\` |
|
|
3276
|
+
|
|
3277
|
+
## Appendix B: Document history
|
|
3278
|
+
|
|
3279
|
+
- *2026-05-28* — CKP/1.0 draft. Initial publication alongside the
|
|
3280
|
+
reference runtime.
|
|
3281
|
+
`;
|
|
3282
|
+
//# sourceMappingURL=agents-content.js.map
|