@clipkit/protocol 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 +156 -0
- package/dist/defaults.d.ts +16 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +122 -0
- package/dist/defaults.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +1326 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +154 -0
- package/dist/types.js.map +1 -0
- package/dist/validate.d.ts +25 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +41 -0
- package/dist/validate.js.map +1 -0
- package/dist/zod.d.ts +71291 -0
- package/dist/zod.d.ts.map +1 -0
- package/dist/zod.js +727 -0
- package/dist/zod.js.map +1 -0
- package/package.json +36 -0
package/dist/zod.js
ADDED
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ANIMATION_TYPES, CAPTION_STYLES, EASING_FUNCTIONS, OUTPUT_FORMATS, } from './types.js';
|
|
3
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// Primitives
|
|
5
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
const numberOrString = z.union([z.number(), z.string()]);
|
|
7
|
+
// Named easing, or a parametric form: cubic-bezier(x1, y1, x2, y2) with
|
|
8
|
+
// four numbers, or steps(n) with a positive integer.
|
|
9
|
+
const PARAMETRIC_EASING = /^(?:cubic-bezier\(\s*-?\d*\.?\d+\s*(?:,\s*-?\d*\.?\d+\s*){3}\)|steps\(\s*[1-9]\d*\s*\))$/;
|
|
10
|
+
const easingSchema = z.union([
|
|
11
|
+
z.enum(EASING_FUNCTIONS),
|
|
12
|
+
z
|
|
13
|
+
.string()
|
|
14
|
+
.regex(PARAMETRIC_EASING, 'expected cubic-bezier(x1, y1, x2, y2) or steps(n)'),
|
|
15
|
+
]);
|
|
16
|
+
const vec2 = z.tuple([z.number(), z.number()]);
|
|
17
|
+
const vec3 = z.tuple([z.number(), z.number(), z.number()]);
|
|
18
|
+
export const keyframeSchema = z.object({
|
|
19
|
+
time: numberOrString.describe('Keyframe time in seconds, relative to the element start.'),
|
|
20
|
+
value: z.union([z.number(), z.string(), vec2, vec3]).describe('Keyframe value: a number, a string (color/length), or a position [x,y] or [x,y,z].'),
|
|
21
|
+
easing: easingSchema.describe('Per-keyframe easing into the next keyframe (overrides the track easing).').optional(),
|
|
22
|
+
in_tangent: z.union([vec2, vec3]).describe('Bezier in-handle [dx,dy] (or [dx,dy,dz]) for spatial paths.').optional(),
|
|
23
|
+
out_tangent: z.union([vec2, vec3]).describe('Bezier out-handle [dx,dy] (or [dx,dy,dz]) for spatial paths.').optional(),
|
|
24
|
+
});
|
|
25
|
+
// Tier-A expression (CKP/1.0, §Expressions): a pure function of element-local
|
|
26
|
+
// time `t` and the element's own index/params (`i`, `n`, `dur`, `value`) — no
|
|
27
|
+
// element references, no runtime inputs. Deterministic and bakeable to keyframes.
|
|
28
|
+
//
|
|
29
|
+
// The grammar is CLOSED — an AI/tool consuming this schema must stay inside the
|
|
30
|
+
// vocabulary below. `EXPR_VOCABULARY` is the canonical machine-readable list and
|
|
31
|
+
// `EXPR_GRAMMAR_DOC` is the `.describe()` text surfaced to JSON-Schema /
|
|
32
|
+
// introspection consumers. The runtime evaluator (runtime/src/animation/expr.ts)
|
|
33
|
+
// derives its variable set from these lists and type-locks its function table to
|
|
34
|
+
// `functions`, so the schema, the docs, and the evaluator cannot drift apart.
|
|
35
|
+
export const EXPR_VOCABULARY = {
|
|
36
|
+
/** Read-only variables in scope. */
|
|
37
|
+
vars: ['t', 'dur', 'i', 'n', 'value'],
|
|
38
|
+
/** Named constants. */
|
|
39
|
+
consts: ['PI', 'TAU', 'E'],
|
|
40
|
+
/** The ONLY callable functions. Anything else is a parse error. */
|
|
41
|
+
functions: [
|
|
42
|
+
'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2', 'sinh', 'cosh', 'tanh',
|
|
43
|
+
'abs', 'sign', 'sqrt', 'cbrt', 'pow', 'exp', 'log', 'log2',
|
|
44
|
+
'floor', 'ceil', 'round', 'trunc', 'fract', 'hypot', 'min', 'max', 'mod',
|
|
45
|
+
'clamp', 'lerp', 'mix', 'step', 'smoothstep', 'linear', 'ease',
|
|
46
|
+
'noise', 'wiggle', 'random',
|
|
47
|
+
],
|
|
48
|
+
/** Operators (besides function calls). `^` is exponentiation, right-assoc. */
|
|
49
|
+
operators: ['+', '-', '*', '/', '%', '^', '<', '>', '<=', '>=', '==', '!=', '&&', '||', '!', '?:'],
|
|
50
|
+
};
|
|
51
|
+
export const EXPR_GRAMMAR_DOC = 'Tier-A expression: a numeric property given as { "expr": "<formula>" }. ' +
|
|
52
|
+
'CLOSED grammar — a pure, deterministic function of the element\'s own clock; ' +
|
|
53
|
+
'it CANNOT reference other elements or read any runtime input (no ref(), no ' +
|
|
54
|
+
'mouse/audio/valueAtTime — those are Tier-B and are permanently unsupported). ' +
|
|
55
|
+
`Variables: ${EXPR_VOCABULARY.vars.join(', ')} ` +
|
|
56
|
+
'(t = element-local seconds, dur = element duration, i = index in a generated ' +
|
|
57
|
+
'set, n = sibling count, value = the property\'s base value). ' +
|
|
58
|
+
`Constants: ${EXPR_VOCABULARY.consts.join(', ')}. ` +
|
|
59
|
+
`Functions (the only ones allowed): ${EXPR_VOCABULARY.functions.join(', ')}. ` +
|
|
60
|
+
'linear(x,x0,x1,y0,y1) and ease(x,x0,x1,y0,y1) map x∈[x0,x1]→[y0,y1] clamped ' +
|
|
61
|
+
'(ease = cubic in-out); noise(x[,seed]), wiggle(freq,amp[,seed]), random(seed) ' +
|
|
62
|
+
'are deterministic. ' +
|
|
63
|
+
`Operators: ${EXPR_VOCABULARY.operators.join(' ')} (^ = power, right-assoc; ?: ternary). ` +
|
|
64
|
+
'Any unknown identifier, function, assignment, member access, or string is a ' +
|
|
65
|
+
'parse error, and the property silently falls back to its base value.';
|
|
66
|
+
export const exprSchema = z
|
|
67
|
+
.object({ expr: z.string().min(1).describe(EXPR_GRAMMAR_DOC) })
|
|
68
|
+
.strict()
|
|
69
|
+
.describe(EXPR_GRAMMAR_DOC);
|
|
70
|
+
const numericProperty = z.union([z.number(), z.string(), z.array(keyframeSchema), exprSchema]);
|
|
71
|
+
export const animationSchema = z.object({
|
|
72
|
+
type: z.enum(ANIMATION_TYPES).describe('Named animation preset (e.g. fade-in, slide-left-in, bounce-in, spin, wiggle, text-appear).'),
|
|
73
|
+
duration: z.number().nonnegative().describe('Tween length in seconds (default 0.5 for most presets).').optional(),
|
|
74
|
+
easing: easingSchema.describe('Easing curve for the tween (default ease-out for most presets).').optional(),
|
|
75
|
+
split: z.enum(['letter', 'word']).describe('For text presets: animate per "letter" or per "word".').optional(),
|
|
76
|
+
stagger: z.number().nonnegative().describe('Delay between split units in seconds (default ~0.09 word, ~0.035 letter).').optional(),
|
|
77
|
+
time: z.union([z.literal('start'), z.literal('end'), z.number()]).describe('When the tween runs: "start", "end", or a time in seconds (default "start").').optional(),
|
|
78
|
+
frequency: z.number().positive().describe('Oscillation frequency in Hz, for oscillating presets like wiggle.').optional(),
|
|
79
|
+
rotation: z.number().describe('Rotation magnitude in degrees (preset-specific, e.g. spin 360).').optional(),
|
|
80
|
+
distance: z.number().describe('Travel distance in px (preset-specific, e.g. slide 40).').optional(),
|
|
81
|
+
direction: z.enum(['left', 'right', 'up', 'down']).describe('Travel direction for slide/fly-style presets.').optional(),
|
|
82
|
+
scale: z.number().min(0).max(1).describe('Squash/scale depth, 0-1 (default 0.3).').optional(),
|
|
83
|
+
seed: z.number().int().min(0).describe('Noise seed, integer (default 0).').optional(),
|
|
84
|
+
axis: z.enum(['x', 'y', 'z']).describe('For text-flip: the 3D rotation axis (default x).').optional(),
|
|
85
|
+
});
|
|
86
|
+
export const keyframeAnimationSchema = z
|
|
87
|
+
.object({
|
|
88
|
+
property: z.string().min(1).describe('Property to animate: "x", "y", "rotation", "scale", "opacity", etc., or "position" for an [x,y]/[x,y,z] path.'),
|
|
89
|
+
keyframes: z.array(keyframeSchema).min(1).describe('Keyframes in ascending time order (at least one).'),
|
|
90
|
+
easing: easingSchema.describe('Default easing for keyframes that do not set their own.').optional(),
|
|
91
|
+
auto_orient: z.boolean().describe('On a "position" path, rotate the element to face its travel direction (default false).').optional(),
|
|
92
|
+
loop: z.union([z.boolean(), z.literal('ping-pong')]).describe('Repeat the track: true (wrap), "ping-pong" (reflect), or omit to clamp.').optional(),
|
|
93
|
+
})
|
|
94
|
+
.superRefine((anim, ctx) => {
|
|
95
|
+
// Position paths (§6.7): every spatial keyframe agrees in
|
|
96
|
+
// dimensionality — all [x, y] or all [x, y, z]. No silent z=0
|
|
97
|
+
// promotion. Tangents may not exceed the path's dimensionality.
|
|
98
|
+
if (anim.property !== 'position')
|
|
99
|
+
return;
|
|
100
|
+
let dim = null;
|
|
101
|
+
anim.keyframes.forEach((k, i) => {
|
|
102
|
+
if (!Array.isArray(k.value))
|
|
103
|
+
return;
|
|
104
|
+
const d = k.value.length;
|
|
105
|
+
if (dim === null)
|
|
106
|
+
dim = d;
|
|
107
|
+
else if (d !== dim) {
|
|
108
|
+
ctx.addIssue({
|
|
109
|
+
code: z.ZodIssueCode.custom,
|
|
110
|
+
path: ['keyframes', i, 'value'],
|
|
111
|
+
message: 'position path keyframes must agree in dimensionality — all [x, y] or all [x, y, z] (§6.7)',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
if (dim !== 3) {
|
|
116
|
+
anim.keyframes.forEach((k, i) => {
|
|
117
|
+
for (const key of ['in_tangent', 'out_tangent']) {
|
|
118
|
+
if (k[key]?.length === 3) {
|
|
119
|
+
ctx.addIssue({
|
|
120
|
+
code: z.ZodIssueCode.custom,
|
|
121
|
+
path: ['keyframes', i, key],
|
|
122
|
+
message: '3-component tangents require a 3D position path — [x, y, z] keyframe values (§6.7)',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
130
|
+
// Stylize effects — element.effects, applied in array order (§4.7)
|
|
131
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
132
|
+
const effectParam = z.union([z.number(), z.array(keyframeSchema), exprSchema]);
|
|
133
|
+
export const effectSchema = z.discriminatedUnion('type', [
|
|
134
|
+
z.object({
|
|
135
|
+
type: z.literal('pixelate'),
|
|
136
|
+
cell_size: effectParam.describe('Cell size in canvas px (default 8, min 1). Each pixel takes its cell-center color.').optional(),
|
|
137
|
+
}),
|
|
138
|
+
z.object({
|
|
139
|
+
type: z.literal('dither'),
|
|
140
|
+
levels: effectParam.describe('Quantization levels per color channel (default 4, min 2).').optional(),
|
|
141
|
+
pixel_size: effectParam.describe('Bayer dither cell size in logical px, resolution-independent (default 2).').optional(),
|
|
142
|
+
}),
|
|
143
|
+
z.object({
|
|
144
|
+
type: z.literal('halftone'),
|
|
145
|
+
cell_size: effectParam.describe('Dot-grid cell size in canvas px (default 8, min 2).').optional(),
|
|
146
|
+
angle: effectParam.describe('Grid rotation in degrees (default 45).').optional(),
|
|
147
|
+
}),
|
|
148
|
+
z.object({
|
|
149
|
+
type: z.literal('ascii'),
|
|
150
|
+
cell_size: effectParam.describe('Glyph cell size in canvas px (default 12, min 4).').optional(),
|
|
151
|
+
}),
|
|
152
|
+
z.object({
|
|
153
|
+
type: z.literal('glass'),
|
|
154
|
+
blur_radius: effectParam.describe('Backdrop blur sigma in px (default 0 = clear glass; >0 = frosted).').optional(),
|
|
155
|
+
refraction: effectParam.describe('Lens bend strength, approx px of displacement (default 21).').optional(),
|
|
156
|
+
edge_width: effectParam.describe('Bevel z-radius; how deep the lens curvature reaches (default 40).').optional(),
|
|
157
|
+
edge_highlight: effectParam.describe('Light-rig strength; 0.35 reproduces the reference defaults (default 0.35).').optional(),
|
|
158
|
+
shadow: effectParam.describe('Drop-shadow opacity painted outside the pane (default 0.3).').optional(),
|
|
159
|
+
dispersion: effectParam.describe('Chromatic aberration along the surface normal (default 0.05).').optional(),
|
|
160
|
+
backdrop_saturation: effectParam.describe('Saturation of the sampled backdrop, 1 = unchanged (default 1).').optional(),
|
|
161
|
+
tint: z.string().describe('Color drawn over the glass; its alpha is the tint strength. Static in v1.').optional(),
|
|
162
|
+
mode: z.enum(['pill', 'dome']).describe('Lens cross-section: "pill" biconvex (default) or "dome" flat-bottom magnifier.').optional(),
|
|
163
|
+
}),
|
|
164
|
+
z.object({
|
|
165
|
+
type: z.literal('glow'),
|
|
166
|
+
radius: effectParam.describe('Blur sigma of the glow, in px (default 20).').optional(),
|
|
167
|
+
intensity: effectParam.describe('Glow brightness multiplier (default 1).').optional(),
|
|
168
|
+
color: z.string().describe('Glow color (default "#FFFFFF").').optional(),
|
|
169
|
+
}),
|
|
170
|
+
z.object({
|
|
171
|
+
type: z.literal('drop_shadow'),
|
|
172
|
+
offset_x: effectParam.describe('Shadow horizontal offset in px (default 0).').optional(),
|
|
173
|
+
offset_y: effectParam.describe('Shadow vertical offset in px (default 12).').optional(),
|
|
174
|
+
blur: effectParam.describe('Shadow blur sigma in px (default 18).').optional(),
|
|
175
|
+
color: z.string().describe('Shadow color (default "#000000").').optional(),
|
|
176
|
+
opacity: effectParam.describe('Shadow opacity, 0..1 (default 0.6).').optional(),
|
|
177
|
+
}),
|
|
178
|
+
z.object({
|
|
179
|
+
type: z.literal('stroke'),
|
|
180
|
+
width: effectParam.describe('Outline width in px, drawn outside the silhouette (default 4).').optional(),
|
|
181
|
+
color: z.string().describe('Stroke color (default "#FFFFFF").').optional(),
|
|
182
|
+
}),
|
|
183
|
+
z.object({
|
|
184
|
+
type: z.literal('chroma_key'),
|
|
185
|
+
color: z.string().describe('Key color removed from the layer (default "#00FF00").').optional(),
|
|
186
|
+
tolerance: effectParam.describe('Chroma distance below which pixels are keyed out (default 0.18).').optional(),
|
|
187
|
+
softness: effectParam.describe('Soft-edge width past tolerance (default 0.1).').optional(),
|
|
188
|
+
spill: effectParam.describe('Spill suppression of the key color (default 0.5).').optional(),
|
|
189
|
+
}),
|
|
190
|
+
z.object({
|
|
191
|
+
type: z.literal('luma_key'),
|
|
192
|
+
threshold: effectParam.describe('Luma below which pixels are removed (default 0.5).').optional(),
|
|
193
|
+
softness: effectParam.describe('Soft-edge width past threshold (default 0.1).').optional(),
|
|
194
|
+
invert: z.boolean().describe('Remove brighter pixels instead of darker (default false).').optional(),
|
|
195
|
+
}),
|
|
196
|
+
z.object({
|
|
197
|
+
type: z.literal('levels'),
|
|
198
|
+
in_black: effectParam.describe('Input black point, 0..1 (default 0).').optional(),
|
|
199
|
+
in_white: effectParam.describe('Input white point, 0..1 (default 1).').optional(),
|
|
200
|
+
gamma: effectParam.describe('Midtone gamma, >0; values >1 brighten midtones (default 1).').optional(),
|
|
201
|
+
out_black: effectParam.describe('Output black point, 0..1 (default 0).').optional(),
|
|
202
|
+
out_white: effectParam.describe('Output white point, 0..1 (default 1).').optional(),
|
|
203
|
+
}),
|
|
204
|
+
z.object({
|
|
205
|
+
type: z.literal('lut'),
|
|
206
|
+
source: z.string().min(1).describe('URL of a .cube LUT file (http(s), relative, or data: URI).'),
|
|
207
|
+
intensity: effectParam.describe('Blend toward the graded color, 0..1 (default 1).').optional(),
|
|
208
|
+
}),
|
|
209
|
+
z.object({
|
|
210
|
+
type: z.literal('fractal_noise'),
|
|
211
|
+
scale: effectParam.describe('Canvas px per noise lattice cell (default 100).').optional(),
|
|
212
|
+
evolution: effectParam.describe('Third-axis position for animating noise churn (default 0).').optional(),
|
|
213
|
+
offset_x: effectParam.describe('Noise scroll offset x in canvas px (default 0).').optional(),
|
|
214
|
+
offset_y: effectParam.describe('Noise scroll offset y in canvas px (default 0).').optional(),
|
|
215
|
+
octaves: z.number().int().min(1).max(8).describe('fBm octaves, integer 1-8, static (default 4).').optional(),
|
|
216
|
+
seed: z.number().int().min(0).describe('Noise seed, integer, static; use values <2^24 (default 0).').optional(),
|
|
217
|
+
}),
|
|
218
|
+
z.object({
|
|
219
|
+
type: z.literal('turbulent_displace'),
|
|
220
|
+
amount: effectParam.describe('Max displacement in canvas px (default 16).').optional(),
|
|
221
|
+
scale: effectParam.describe('Canvas px per noise lattice cell (default 120).').optional(),
|
|
222
|
+
evolution: effectParam.describe('Third-axis position for animating noise churn (default 0).').optional(),
|
|
223
|
+
octaves: z.number().int().min(1).max(8).describe('fBm octaves, integer 1-8, static (default 2).').optional(),
|
|
224
|
+
seed: z.number().int().min(0).describe('Noise seed, integer, static (default 0).').optional(),
|
|
225
|
+
}),
|
|
226
|
+
]);
|
|
227
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
228
|
+
// Lighting (CKP/1.0 §4.8) — PBR material on elements
|
|
229
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
230
|
+
const numOrKf = z.union([z.number(), z.array(keyframeSchema), exprSchema]);
|
|
231
|
+
const materialSchema = z
|
|
232
|
+
.object({
|
|
233
|
+
roughness: numOrKf.describe('Surface roughness, 0 (glossy) to 1 (matte); clamped to 0.02-1 (default 0.5).').optional(),
|
|
234
|
+
metalness: numOrKf.describe('Metalness, 0 (dielectric) to 1 (metal) (default 0).').optional(),
|
|
235
|
+
reflectivity: numOrKf.describe('Environment-reflection strength; needs scene lights + environment (default 1).').optional(),
|
|
236
|
+
emissive: numOrKf.describe('Self-illumination strength, multiplied into the color (default 0).').optional(),
|
|
237
|
+
normal_map: z.string().describe('URL of a tangent-space normal map for surface detail (flat texel = #8080ff).').optional(),
|
|
238
|
+
normal_scale: numOrKf.describe('Normal-map strength, 0 = flat, higher = more relief (default 1).').optional(),
|
|
239
|
+
})
|
|
240
|
+
.passthrough();
|
|
241
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
242
|
+
// Base element fields — shared by every variant
|
|
243
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
244
|
+
const baseElementFields = {
|
|
245
|
+
id: z.string().optional(),
|
|
246
|
+
name: z.string().optional(),
|
|
247
|
+
layer: z.number().int().min(1).max(1000).describe("The element's layer in the stack (1-1000), like an After Effects layer: each element has its own unique layer and LOWER numbers draw in front (layer 1 is on top). Within equal z-depth, layer is the draw order."),
|
|
248
|
+
visible: z.boolean().optional(),
|
|
249
|
+
time: numberOrString.describe('Element start time in seconds from composition start (default 0).').optional(),
|
|
250
|
+
duration: z.union([z.number(), z.string(), z.literal('auto'), z.literal('end')]).describe('How long the element lasts: seconds, "auto" (its natural content/media length), or "end" (until the composition ends).').optional(),
|
|
251
|
+
x: numericProperty.describe('Horizontal position of the anchor point, in px or a string like "50%"/"100vw" (default 0). With the default anchor (left), this is the box left edge.').optional(),
|
|
252
|
+
y: numericProperty.describe('Vertical position of the anchor point, in px or a string like "50%" (default 0). With the default anchor (top), this is the box top edge.').optional(),
|
|
253
|
+
x_anchor: numberOrString.describe('Point in the box that x positions: 0 = left, "50%" = center, "100%" = right (default 0). Default 0 makes x/y the top-left corner (CSS/SVG/Canvas model); rotation and scale still pivot the box center regardless of anchor.').optional(),
|
|
254
|
+
y_anchor: numberOrString.describe('Point in the box that y positions: 0 = top, "50%" = center, "100%" = bottom (default 0).').optional(),
|
|
255
|
+
width: numericProperty.describe('Box width in px or a string like "50%"/"100vw".').optional(),
|
|
256
|
+
height: numericProperty.describe('Box height in px or a string like "50%"/"100vh".').optional(),
|
|
257
|
+
aspect_ratio: z.number().positive().optional(),
|
|
258
|
+
rotation: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('In-plane rotation in degrees about the box center (default 0).').optional(),
|
|
259
|
+
// CKP/1.0 3D transform fields (§4.4). `z_rotation` is the same slot
|
|
260
|
+
// as `rotation` — both authored on one element is rejected by the
|
|
261
|
+
// source-level cross-field check below.
|
|
262
|
+
z_rotation: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Same slot as rotation (in-plane degrees); author one, not both (default 0).').optional(),
|
|
263
|
+
x_rotation: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Rotation about the local x axis in degrees; tips the top edge away under a camera (default 0).').optional(),
|
|
264
|
+
y_rotation: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Rotation about the local y axis in degrees; turns the right edge away under a camera (default 0).').optional(),
|
|
265
|
+
z: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Depth in px toward (+) / away from (-) the viewer; orders elements and drives perspective under a camera (default 0).').optional(),
|
|
266
|
+
scale: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Uniform scale factor, multiplied with x_scale/y_scale (default 1).').optional(),
|
|
267
|
+
x_scale: z.union([z.number(), z.string(), z.array(keyframeSchema), exprSchema]).describe('Horizontal scale factor, number or "150%" (default 1).').optional(),
|
|
268
|
+
y_scale: z.union([z.number(), z.string(), z.array(keyframeSchema), exprSchema]).describe('Vertical scale factor, number or "150%" (default 1).').optional(),
|
|
269
|
+
x_skew: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Horizontal shear in degrees (CSS skewX); positive moves the bottom edge right (default 0).').optional(),
|
|
270
|
+
y_skew: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Vertical shear in degrees (CSS skewY); positive moves the right edge down (default 0).').optional(),
|
|
271
|
+
opacity: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Opacity from 0 (transparent) to 1 (opaque) (default 1).').optional(),
|
|
272
|
+
blend_mode: z.enum(['normal', 'multiply', 'screen', 'add', 'overlay', 'hard-light', 'soft-light']).optional(),
|
|
273
|
+
blur_radius: z.union([z.number().nonnegative(), z.array(keyframeSchema), exprSchema]).describe('Gaussian blur sigma in px applied to the element (default 0 = none).').optional(),
|
|
274
|
+
brightness: z.union([z.number().nonnegative(), z.array(keyframeSchema), exprSchema]).describe('Brightness multiplier, 1 = unchanged, >1 brightens (default 1).').optional(),
|
|
275
|
+
contrast: z.union([z.number().nonnegative(), z.array(keyframeSchema), exprSchema]).describe('Contrast multiplier around mid-gray, 1 = unchanged (default 1).').optional(),
|
|
276
|
+
saturation: z.union([z.number().nonnegative(), z.array(keyframeSchema), exprSchema]).describe('Saturation multiplier, 1 = unchanged, 0 = grayscale (default 1).').optional(),
|
|
277
|
+
hue_rotate: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Hue rotation in degrees (default 0).').optional(),
|
|
278
|
+
effects: z.array(effectSchema).describe('Stylize/keying effects applied in array order (e.g. glass, drop_shadow, chroma_key, glow); they stack.').optional(),
|
|
279
|
+
material: materialSchema.describe('PBR material (roughness/metalness/etc.); only visible with scene lights.').optional(),
|
|
280
|
+
animations: z.array(animationSchema).describe('Named animation presets applied to this element (e.g. fade-in, slide-left-in); see the animation type list.').optional(),
|
|
281
|
+
keyframe_animations: z.array(keyframeAnimationSchema).describe('Explicit per-property keyframe tracks (property + keyframes[]); use for custom motion and position paths.').optional(),
|
|
282
|
+
};
|
|
283
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
284
|
+
// Element variants
|
|
285
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
286
|
+
export const videoElementSchema = z
|
|
287
|
+
.object({
|
|
288
|
+
...baseElementFields,
|
|
289
|
+
type: z.literal('video'),
|
|
290
|
+
source: z.string().min(1),
|
|
291
|
+
volume: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Volume in percent, 0..100 (default 100).').optional(),
|
|
292
|
+
playback_rate: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Timeline seconds per media second (default 1); <1 = slow-mo, >1 = speed-up.').optional(),
|
|
293
|
+
trim_start: z.number().nonnegative().describe('Media in-point in seconds (default 0).').optional(),
|
|
294
|
+
trim_duration: z.number().nonnegative().describe('Length of the played window in seconds (default = remainder after trim_start).').optional(),
|
|
295
|
+
loop: z.boolean().describe('Restart at the trim in-point when the window ends (default false).').optional(),
|
|
296
|
+
time_remap: z.array(keyframeSchema).describe('Keyframes whose values are media times in seconds; replaces trim/playback_rate for warped playback.').optional(),
|
|
297
|
+
audio_fade_in: z.number().nonnegative().describe('Audio fade-in length in seconds (default 0).').optional(),
|
|
298
|
+
audio_fade_out: z.number().nonnegative().describe('Audio fade-out length in seconds (default 0).').optional(),
|
|
299
|
+
fit: z.enum(['cover', 'contain', 'fill', 'none']).describe('How the media fills the box (CSS object-fit): cover (default), contain, fill, or none.').optional(),
|
|
300
|
+
// Source crop — normalized sub-rectangle of the media (0..1, origin
|
|
301
|
+
// top-left), applied before `fit`. Default 0,0,1,1 (whole source).
|
|
302
|
+
crop_x: z.number().min(0).max(1).describe('Source crop origin x, normalized 0..1, applied before fit (default 0).').optional(),
|
|
303
|
+
crop_y: z.number().min(0).max(1).describe('Source crop origin y, normalized 0..1 (default 0).').optional(),
|
|
304
|
+
crop_width: z.number().min(0).max(1).describe('Source crop width, normalized 0..1 (default 1 = whole source).').optional(),
|
|
305
|
+
crop_height: z.number().min(0).max(1).describe('Source crop height, normalized 0..1 (default 1).').optional(),
|
|
306
|
+
})
|
|
307
|
+
.passthrough();
|
|
308
|
+
export const imageElementSchema = z
|
|
309
|
+
.object({
|
|
310
|
+
...baseElementFields,
|
|
311
|
+
type: z.literal('image'),
|
|
312
|
+
source: z.string().min(1),
|
|
313
|
+
fit: z.enum(['cover', 'contain', 'fill', 'none']).describe('How the image fills the box (CSS object-fit): cover (default), contain, fill, or none.').optional(),
|
|
314
|
+
border_radius: z.number().nonnegative().describe('Corner radius in px (default 0).').optional(),
|
|
315
|
+
// Source crop — normalized sub-rectangle of the media (0..1, origin
|
|
316
|
+
// top-left), applied before `fit`. Default 0,0,1,1 (whole source).
|
|
317
|
+
crop_x: z.number().min(0).max(1).describe('Source crop origin x, normalized 0..1, applied before fit (default 0).').optional(),
|
|
318
|
+
crop_y: z.number().min(0).max(1).describe('Source crop origin y, normalized 0..1 (default 0).').optional(),
|
|
319
|
+
crop_width: z.number().min(0).max(1).describe('Source crop width, normalized 0..1 (default 1 = whole source).').optional(),
|
|
320
|
+
crop_height: z.number().min(0).max(1).describe('Source crop height, normalized 0..1 (default 1).').optional(),
|
|
321
|
+
})
|
|
322
|
+
.passthrough();
|
|
323
|
+
export const textMaskSchema = z.object({
|
|
324
|
+
type: z.literal('linear-wipe'),
|
|
325
|
+
angle: z.number().describe('Wipe direction in degrees (default -45); 0 = left-to-right, 90 = top-to-bottom.').optional(),
|
|
326
|
+
progress: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Reveal amount, 0 (hidden) to 1 (fully shown); animatable (default 1 = fully shown).').optional(),
|
|
327
|
+
softness: z.number().min(0).max(1).describe('Softness of the wipe edge, 0..1 (default 0.3).').optional(),
|
|
328
|
+
});
|
|
329
|
+
const textShadowSchema = z
|
|
330
|
+
.object({
|
|
331
|
+
color: z.string(),
|
|
332
|
+
offset_x: z.number().describe('Shadow horizontal offset in px (default 0).').optional(),
|
|
333
|
+
offset_y: z.number().describe('Shadow vertical offset in px, positive = down (default 0).').optional(),
|
|
334
|
+
blur: z.number().nonnegative().describe('Shadow blur sigma in px (default 0 = crisp).').optional(),
|
|
335
|
+
opacity: z.number().min(0).max(1).describe('Shadow opacity, 0..1 (default 1).').optional(),
|
|
336
|
+
})
|
|
337
|
+
.passthrough();
|
|
338
|
+
const textShadowField = z.union([textShadowSchema, z.array(textShadowSchema)]).describe('Per-glyph drop shadow: one object, or an array rendered back-to-front.').optional();
|
|
339
|
+
const textSpanBackgroundSchema = z
|
|
340
|
+
.object({
|
|
341
|
+
color: z.string(),
|
|
342
|
+
height_ratio: z.number().describe('Band height as a fraction of font size (default 1 = full line box).').optional(),
|
|
343
|
+
inset_y_ratio: z.number().describe('Vertical offset of the band as a fraction of font size (default 0).').optional(),
|
|
344
|
+
padding_x: z.number().describe('Horizontal padding around the span glyphs in px (default 0).').optional(),
|
|
345
|
+
skew_x: z.number().describe('Horizontal skew of the band in degrees (default 0).').optional(),
|
|
346
|
+
border_radius: z.number().nonnegative().describe('Band corner radius in px (default 0).').optional(),
|
|
347
|
+
opacity: z.number().min(0).max(1).describe('Band opacity, 0..1 (default 1).').optional(),
|
|
348
|
+
})
|
|
349
|
+
.passthrough();
|
|
350
|
+
export const textSpanSchema = z
|
|
351
|
+
.object({
|
|
352
|
+
text: z.string(),
|
|
353
|
+
font_weight: numberOrString.optional(),
|
|
354
|
+
font_style: z.enum(['normal', 'italic']).optional(),
|
|
355
|
+
font_family: z.string().optional(),
|
|
356
|
+
font_size: numberOrString.optional(),
|
|
357
|
+
fill_color: z.string().optional(),
|
|
358
|
+
letter_spacing: z.number().optional(),
|
|
359
|
+
background_color: z.string().describe('Flat band behind this span; overridden by background.').optional(),
|
|
360
|
+
background: textSpanBackgroundSchema.describe('Styled background band behind this span (height/inset/padding/skew/radius).').optional(),
|
|
361
|
+
nowrap: z.boolean().describe('Prevent line-breaking inside this span (default false).').optional(),
|
|
362
|
+
})
|
|
363
|
+
.passthrough();
|
|
364
|
+
export const textElementSchema = z
|
|
365
|
+
.object({
|
|
366
|
+
...baseElementFields,
|
|
367
|
+
type: z.literal('text'),
|
|
368
|
+
text: z.string().describe('The text content; use this OR spans, not both.').optional(),
|
|
369
|
+
spans: z.array(textSpanSchema).describe('Rich-text runs with per-span styling; alternative to a single text string.').optional(),
|
|
370
|
+
font_family: z.string().optional(),
|
|
371
|
+
font_size: numberOrString.describe('Font size in px, a string, or "auto" to fit the box.').optional(),
|
|
372
|
+
font_size_minimum: numberOrString.describe('Lower bound in px when font_size is "auto" (default 8).').optional(),
|
|
373
|
+
font_size_maximum: numberOrString.describe('Upper bound in px when font_size is "auto" (default 400).').optional(),
|
|
374
|
+
font_weight: numberOrString.optional(),
|
|
375
|
+
font_style: z.enum(['normal', 'italic']).optional(),
|
|
376
|
+
fill_color: z.string().describe('Text color (default "#ffffff").').optional(),
|
|
377
|
+
stroke_color: z.string().describe('Glyph outline color; pair with stroke_width.').optional(),
|
|
378
|
+
stroke_width: z.number().describe('Glyph outline width in px (default 0).').optional(),
|
|
379
|
+
text_transform: z.enum(['none', 'uppercase', 'lowercase', 'capitalize']).optional(),
|
|
380
|
+
text_wrap: z.boolean().describe('Soft-wrap within the box width (default true); false forces a single line.').optional(),
|
|
381
|
+
text_align: z.enum(['left', 'center', 'right']).describe('Horizontal text alignment (default "left").').optional(),
|
|
382
|
+
vertical_align: z.enum(['top', 'middle', 'bottom']).describe('Vertical alignment within the box (default "top").').optional(),
|
|
383
|
+
x_padding: numberOrString.describe('Horizontal inset in px around the text (default 0).').optional(),
|
|
384
|
+
y_padding: numberOrString.describe('Vertical inset in px around the text (default 0).').optional(),
|
|
385
|
+
x_alignment: numberOrString.describe('Fine horizontal alignment as a 0..1 fraction; overrides text_align.').optional(),
|
|
386
|
+
y_alignment: numberOrString.describe('Fine vertical alignment as a 0..1 fraction; overrides vertical_align.').optional(),
|
|
387
|
+
line_height: z.number().describe('Line spacing as a multiple of font size (default 1).').optional(),
|
|
388
|
+
letter_spacing: z.number().describe('Tracking in px added after each glyph (default 0).').optional(),
|
|
389
|
+
background_color: z.string().describe('Background band behind the text, shrink-wrapped per line (default none).').optional(),
|
|
390
|
+
background_border_radius: z.number().describe('Corner radius in px of the background band (default 0).').optional(),
|
|
391
|
+
background_padding: z.union([z.number(), z.tuple([z.number(), z.number()])]).describe('Background band padding in px: a number, or [x, y] (default [0, 0]).').optional(),
|
|
392
|
+
text_shadow: textShadowField,
|
|
393
|
+
mask: textMaskSchema.describe('Linear-wipe reveal of the text (angle/progress/softness).').optional(),
|
|
394
|
+
})
|
|
395
|
+
.passthrough();
|
|
396
|
+
// ── Gradients ──────────────────────────────────────────────────────────────
|
|
397
|
+
export const gradientStopSchema = z.object({
|
|
398
|
+
offset: z.number().min(0).max(1).describe('Position along the gradient, 0 (start) to 1 (end).'),
|
|
399
|
+
color: z.string(),
|
|
400
|
+
});
|
|
401
|
+
// ── Lighting: scene lights + environment (CKP/1.0 §4.8) ─────────────────────
|
|
402
|
+
const lightSchema = z.discriminatedUnion('type', [
|
|
403
|
+
z.object({
|
|
404
|
+
type: z.literal('ambient'),
|
|
405
|
+
color: z.string().describe('Ambient light color (default "#FFFFFF").').optional(),
|
|
406
|
+
intensity: numOrKf.describe('Ambient brightness multiplier (default 1).').optional(),
|
|
407
|
+
}).passthrough(),
|
|
408
|
+
z.object({
|
|
409
|
+
type: z.literal('directional'),
|
|
410
|
+
azimuth: numOrKf.describe('Compass direction of the light in degrees (default 0).').optional(),
|
|
411
|
+
elevation: numOrKf.describe('Height of the light above the screen plane in degrees (default 45).').optional(),
|
|
412
|
+
color: z.string().describe('Directional light color (default "#FFFFFF").').optional(),
|
|
413
|
+
intensity: numOrKf.describe('Directional brightness multiplier (default 1).').optional(),
|
|
414
|
+
}).passthrough(),
|
|
415
|
+
]);
|
|
416
|
+
const environmentSchema = z.union([
|
|
417
|
+
z.object({
|
|
418
|
+
type: z.literal('gradient'),
|
|
419
|
+
stops: z.array(gradientStopSchema).min(2).max(6).describe('Sky gradient stops (2-6), sampled by the reflection ray vertical component.'),
|
|
420
|
+
}).passthrough(),
|
|
421
|
+
z.object({
|
|
422
|
+
type: z.literal('image'),
|
|
423
|
+
src: z.string().describe('Equirectangular environment image URL, used for material reflections.'),
|
|
424
|
+
}).passthrough(),
|
|
425
|
+
]);
|
|
426
|
+
const bloomSchema = z
|
|
427
|
+
.object({
|
|
428
|
+
threshold: numOrKf.describe('Luma above which pixels bloom, 0-1 (default 0.75).').optional(),
|
|
429
|
+
knee: numOrKf.describe('Soft-knee width around the threshold (default 0.1).').optional(),
|
|
430
|
+
intensity: numOrKf.describe('Bloom strength multiplier (default 1).').optional(),
|
|
431
|
+
radius: numOrKf.describe('Bloom blur sigma in px (default 24).').optional(),
|
|
432
|
+
})
|
|
433
|
+
.passthrough();
|
|
434
|
+
export const linearGradientSchema = z.object({
|
|
435
|
+
type: z.literal('linear'),
|
|
436
|
+
angle: z.number().describe('Gradient direction in degrees (CSS convention; default 180 = top-to-bottom).').optional(),
|
|
437
|
+
stops: z.array(gradientStopSchema).min(2).max(4).describe('Color stops, 2-4.'),
|
|
438
|
+
});
|
|
439
|
+
export const radialGradientSchema = z.object({
|
|
440
|
+
type: z.literal('radial'),
|
|
441
|
+
cx: z.number().describe('Center x as a fraction of the box, 0..1 (default 0.5).').optional(),
|
|
442
|
+
cy: z.number().describe('Center y as a fraction of the box, 0..1 (default 0.5).').optional(),
|
|
443
|
+
radius: z.number().positive().describe('Radius as a fraction of the box, 0..1 (default 0.5).').optional(),
|
|
444
|
+
stops: z.array(gradientStopSchema).min(2).max(4).describe('Color stops, 2-4.'),
|
|
445
|
+
});
|
|
446
|
+
export const gradientSchema = z.discriminatedUnion('type', [
|
|
447
|
+
linearGradientSchema,
|
|
448
|
+
radialGradientSchema,
|
|
449
|
+
]);
|
|
450
|
+
const boxShadowSchema = z
|
|
451
|
+
.object({
|
|
452
|
+
color: z.string(),
|
|
453
|
+
offset_x: z.number().describe('Shadow horizontal offset in px (default 0).').optional(),
|
|
454
|
+
offset_y: z.number().describe('Shadow vertical offset in px (default 12).').optional(),
|
|
455
|
+
blur: z.number().nonnegative().describe('Shadow blur sigma in px (default 18).').optional(),
|
|
456
|
+
})
|
|
457
|
+
.passthrough();
|
|
458
|
+
// ── Path geometry (used by `shape` when it carries `paths`) ──────────────────
|
|
459
|
+
export const pathGradientSchema = z.object({
|
|
460
|
+
id: z.string().min(1).describe('Identifier referenced from a path fill/stroke as url(#id).'),
|
|
461
|
+
type: z.literal('linear'),
|
|
462
|
+
x1: z.number().describe('Gradient line start x in viewBox coords; the line runs (x1,y1) to (x2,y2).'),
|
|
463
|
+
y1: z.number(),
|
|
464
|
+
x2: z.number(),
|
|
465
|
+
y2: z.number(),
|
|
466
|
+
stops: z.array(gradientStopSchema).min(2).max(4).describe('Color stops, 2-4.'),
|
|
467
|
+
});
|
|
468
|
+
export const pathDefSchema = z.object({
|
|
469
|
+
d: z.union([z.string().min(1), z.array(keyframeSchema)]).describe('SVG path data; an array of keyframes morphs between path shapes.'),
|
|
470
|
+
fill: z.string().describe('Fill color, or url(#id) referencing a gradient (default none).').optional(),
|
|
471
|
+
stroke: z.string().describe('Stroke color, or url(#id); needs stroke_width > 0.').optional(),
|
|
472
|
+
stroke_width: z.number().positive().describe('Stroke width in px.').optional(),
|
|
473
|
+
stroke_progress: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Reveal fraction of the stroke length, 0..1 (default 1); shorthand for a draw-on trim.').optional(),
|
|
474
|
+
trim_start: effectParam.describe('Stroke start as a fraction of path length, 0..1 (default 0).').optional(),
|
|
475
|
+
trim_end: effectParam.describe('Stroke end as a fraction of path length, 0..1 (default 1).').optional(),
|
|
476
|
+
trim_offset: effectParam.describe('Rotate the visible trim window around the path, 0..1 wrapping (default 0).').optional(),
|
|
477
|
+
clip_path: z.string().describe('SVG path d-string that clips this path (intersection).').optional(),
|
|
478
|
+
stroke_linecap: z.enum(['butt', 'round', 'square']).describe('Line-end style (default "butt").').optional(),
|
|
479
|
+
stroke_linejoin: z.enum(['miter', 'round', 'bevel']).describe('Line-join style (default "miter").').optional(),
|
|
480
|
+
opacity: z.number().min(0).max(1).describe('Path opacity, 0..1 (default 1).').optional(),
|
|
481
|
+
});
|
|
482
|
+
// A `shape` is EITHER a primitive (SDF rectangle/ellipse) OR a vector path
|
|
483
|
+
// (`paths`). The renderer dispatches on `paths`; when present the primitive
|
|
484
|
+
// fields are ignored. (Absorbs the former `svg` element.)
|
|
485
|
+
export const shapeElementSchema = z
|
|
486
|
+
.object({
|
|
487
|
+
...baseElementFields,
|
|
488
|
+
type: z.literal('shape'),
|
|
489
|
+
// primitive form (SDF)
|
|
490
|
+
shape: z.enum(['rectangle', 'ellipse']).describe('Primitive kind: "rectangle" (default) or "ellipse". Ignored when paths is present.').optional(),
|
|
491
|
+
fill_color: z.string().describe('Solid fill color, hex or CSS name (default "#ffffff"); overridden by gradient.').optional(),
|
|
492
|
+
gradient: gradientSchema.describe('Linear or radial gradient fill (overrides fill_color).').optional(),
|
|
493
|
+
stroke_color: z.string().describe('Outline color, hex or CSS name; pair with stroke_width > 0.').optional(),
|
|
494
|
+
stroke_width: z.number().describe('Outline width in px, drawn outside the shape (default 0 = no stroke).').optional(),
|
|
495
|
+
border_radius: z.number().describe('Corner radius in px for a rectangle (default 0).').optional(),
|
|
496
|
+
shadow: boxShadowSchema.describe('Drop shadow cast by the shape (color, offset, blur).').optional(),
|
|
497
|
+
// path form (rasterized) — presence selects this representation
|
|
498
|
+
paths: z.array(pathDefSchema).min(1).describe('Vector path form: an array of SVG paths. Its presence switches the shape from primitive to vector and ignores shape/fill_color/etc.').optional(),
|
|
499
|
+
view_box: z.tuple([z.number(), z.number(), z.number(), z.number()]).describe('SVG viewBox [x, y, width, height] for the paths coordinate space (default [0, 0, 100, 100]).').optional(),
|
|
500
|
+
gradients: z.array(pathGradientSchema).describe('Named gradients referenced from path fill/stroke as url(#id).').optional(),
|
|
501
|
+
})
|
|
502
|
+
.passthrough();
|
|
503
|
+
export const audioElementSchema = z
|
|
504
|
+
.object({
|
|
505
|
+
...baseElementFields,
|
|
506
|
+
type: z.literal('audio'),
|
|
507
|
+
source: z.string().min(1),
|
|
508
|
+
volume: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Volume in percent, 0..100 (default 100).').optional(),
|
|
509
|
+
trim_start: z.number().nonnegative().describe('Media in-point in seconds (default 0).').optional(),
|
|
510
|
+
trim_duration: z.number().nonnegative().describe('Length of the played window in seconds (default = remainder after trim_start).').optional(),
|
|
511
|
+
loop: z.boolean().describe('Restart at the trim in-point when the window ends (default false).').optional(),
|
|
512
|
+
audio_fade_in: z.number().nonnegative().describe('Fade-in length in seconds (default 0).').optional(),
|
|
513
|
+
audio_fade_out: z.number().nonnegative().describe('Fade-out length in seconds (default 0).').optional(),
|
|
514
|
+
})
|
|
515
|
+
.passthrough();
|
|
516
|
+
// Group nests elements. We don't recurse through Zod here — the resulting
|
|
517
|
+
// inferred type is too large for tsc declaration emit, and the renderer
|
|
518
|
+
// traverses + validates nested elements at render time anyway. Promote to
|
|
519
|
+
// recursive validation in v1.x if a real consumer needs it.
|
|
520
|
+
export const groupElementSchema = z
|
|
521
|
+
.object({
|
|
522
|
+
...baseElementFields,
|
|
523
|
+
type: z.literal('group'),
|
|
524
|
+
elements: z.array(z.unknown()).min(1).describe('Child elements; the group transform composes onto all of them (couple elements without duplicating motion onto each).'),
|
|
525
|
+
time_remap: z.array(keyframeSchema).describe('Keyframes that warp the group subtree clock (values are warped seconds).').optional(),
|
|
526
|
+
clip: z.boolean().describe('Clip children to the group box (default false).').optional(),
|
|
527
|
+
// Rounds a clipped group's box (rounded card clipping its content).
|
|
528
|
+
border_radius: z.number().nonnegative().describe('Corner radius in px of the clipped group box (default 0).').optional(),
|
|
529
|
+
mask: z
|
|
530
|
+
.object({
|
|
531
|
+
mode: z.enum(['alpha', 'alpha-inverted', 'luma', 'luma-inverted']).describe('How the mask layer drives content opacity: alpha or luma, optionally inverted.'),
|
|
532
|
+
elements: z.array(z.unknown()).min(1).describe('Elements that compose the mask layer.'),
|
|
533
|
+
})
|
|
534
|
+
.describe('Mask the group with another set of elements (alpha or luma).')
|
|
535
|
+
.optional(),
|
|
536
|
+
})
|
|
537
|
+
.passthrough();
|
|
538
|
+
export const captionWordSchema = z.object({
|
|
539
|
+
text: z.string(),
|
|
540
|
+
start: z.number().nonnegative(),
|
|
541
|
+
end: z.number().nonnegative(),
|
|
542
|
+
});
|
|
543
|
+
export const captionElementSchema = z
|
|
544
|
+
.object({
|
|
545
|
+
...baseElementFields,
|
|
546
|
+
type: z.literal('caption'),
|
|
547
|
+
words: z.array(captionWordSchema).min(1).describe('Word timings: array of { text, start, end } in seconds; drives the karaoke-style highlight.'),
|
|
548
|
+
style: z.enum(CAPTION_STYLES).describe('Kinetic caption preset (e.g. tiktok_bounce, fade_reveal, word_pop).').optional(),
|
|
549
|
+
// Windowing: max letters per chunk (number) or auto word-chunking ('auto').
|
|
550
|
+
max_length: z.union([z.number().int().positive(), z.literal('auto')]).describe('On-screen chunking: max letters per chunk (number) or "auto" (a few words); omit to show all words at once.').optional(),
|
|
551
|
+
font_family: z.string().optional(),
|
|
552
|
+
font_size: numberOrString.describe('Font size in px, a string, or "auto" to fit (default "auto").').optional(),
|
|
553
|
+
font_weight: numberOrString.optional(),
|
|
554
|
+
font_style: z.enum(['normal', 'italic']).optional(),
|
|
555
|
+
fill_color: z.string().describe('Color of inactive (not-yet-spoken) words (default "#ffffff").').optional(),
|
|
556
|
+
stroke_color: z.string().describe('Glyph outline color; pair with stroke_width.').optional(),
|
|
557
|
+
stroke_width: z.number().describe('Glyph outline width in px (default 0).').optional(),
|
|
558
|
+
text_align: z.enum(['left', 'center', 'right']).describe('Horizontal alignment (default "left").').optional(),
|
|
559
|
+
line_height: z.number().describe('Line spacing as a multiple of font size (default 1.2).').optional(),
|
|
560
|
+
letter_spacing: z.number().describe('Tracking in px added after each glyph (default 0).').optional(),
|
|
561
|
+
background_color: z.string().describe('Background band behind the caption (default none).').optional(),
|
|
562
|
+
background_border_radius: z.number().describe('Corner radius in px of the background band (default 0).').optional(),
|
|
563
|
+
background_padding: z.union([z.number(), z.tuple([z.number(), z.number()])]).describe('Background band padding in px: a number, or [x, y] (default [0, 0]).').optional(),
|
|
564
|
+
text_shadow: textShadowField,
|
|
565
|
+
highlight_color: z.string().describe('Color of the active (currently-spoken) word (default "#ffd60a").').optional(),
|
|
566
|
+
highlight_background_color: z.string().describe('Background color behind the active word (default none).').optional(),
|
|
567
|
+
})
|
|
568
|
+
.passthrough();
|
|
569
|
+
// ── Particles ──────────────────────────────────────────────────────────────
|
|
570
|
+
export const particlesElementSchema = z
|
|
571
|
+
.object({
|
|
572
|
+
...baseElementFields,
|
|
573
|
+
type: z.literal('particles'),
|
|
574
|
+
rate: z.number().positive().describe('Particles emitted per second (default 60).').optional(),
|
|
575
|
+
lifetime: z.number().positive().describe('Seconds each particle lives (default 1.5).').optional(),
|
|
576
|
+
velocity: z.number().nonnegative().describe('Initial particle speed in px/s (default 300).').optional(),
|
|
577
|
+
spread: z.number().min(0).max(360).describe('Emission cone width in degrees, 0-360 (default 360 = all directions).').optional(),
|
|
578
|
+
direction: z.number().describe('Emission direction in degrees: 0 = right, 90 = down, -90 = up (default -90).').optional(),
|
|
579
|
+
gravity: z.number().describe('Downward acceleration in px/s^2 (default 600).').optional(),
|
|
580
|
+
color: z.union([z.string(), z.array(z.string()).min(1)]).describe('Particle color, or an array of colors picked per particle.').optional(),
|
|
581
|
+
size: z.number().positive().describe('Particle size in px (default 12).').optional(),
|
|
582
|
+
size_variation: z.number().min(0).max(1).describe('Random size variation, 0-1 (default 0.4).').optional(),
|
|
583
|
+
particle_shape: z.enum(['square', 'circle']).describe('Particle shape (default "square").').optional(),
|
|
584
|
+
rotation_speed: z.number().describe('Spin rate in degrees/s (default 360).').optional(),
|
|
585
|
+
burst: z.boolean().describe('Emit all particles at once instead of continuously (default false).').optional(),
|
|
586
|
+
burst_count: z.number().int().min(1).max(2000).describe('Particles emitted in burst mode (default 80).').optional(),
|
|
587
|
+
fade_at: z.number().min(0).max(1).describe('Lifetime fraction where fade-out begins, 0-1 (default 0.7).').optional(),
|
|
588
|
+
z_velocity: z.number().describe('Depth speed in px/s along the plane normal (default 0).').optional(),
|
|
589
|
+
z_spread: z.number().nonnegative().describe('Random depth-speed range in px/s (default 0).').optional(),
|
|
590
|
+
target_points: z.array(z.tuple([z.number(), z.number()])).describe('Canvas-space [x, y] targets the particles converge toward.').optional(),
|
|
591
|
+
convergence_easing: easingSchema.describe('Easing for convergence toward target_points.').optional(),
|
|
592
|
+
scatter_radius: z.number().nonnegative().describe('Spawn-disk radius in px (default = the larger canvas dimension).').optional(),
|
|
593
|
+
})
|
|
594
|
+
.passthrough();
|
|
595
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
596
|
+
// Discriminated union & Source root
|
|
597
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
598
|
+
// Explicitly typed for the same reason as sourceSchema below — with the
|
|
599
|
+
// effects union aboard, the inferred type exceeds what tsc will
|
|
600
|
+
// serialize into declarations. The boundary type remains `Element`.
|
|
601
|
+
export const elementSchema = z.discriminatedUnion('type', [
|
|
602
|
+
videoElementSchema,
|
|
603
|
+
imageElementSchema,
|
|
604
|
+
textElementSchema,
|
|
605
|
+
shapeElementSchema,
|
|
606
|
+
audioElementSchema,
|
|
607
|
+
groupElementSchema,
|
|
608
|
+
captionElementSchema,
|
|
609
|
+
particlesElementSchema,
|
|
610
|
+
]);
|
|
611
|
+
// Explicitly typed as ZodTypeAny: the inferred type of the discriminated-
|
|
612
|
+
// union-of-7-passthrough-objects-inside-an-array is too large for tsc to
|
|
613
|
+
// serialize when emitting declarations. The runtime boundary type is `Source`,
|
|
614
|
+
// enforced at the `validate()` callsite via cast.
|
|
615
|
+
const fontFaceSchema = z
|
|
616
|
+
.object({
|
|
617
|
+
family: z.string().min(1).describe('Family name that text/caption elements reference via font_family.'),
|
|
618
|
+
weight: z.union([z.number(), z.string()]).describe('CSS font-weight this face covers, e.g. 400, "bold", or a range "100 900" (default "normal").').optional(),
|
|
619
|
+
style: z.enum(['normal', 'italic']).optional(),
|
|
620
|
+
src: z.string().min(1).describe('Font file URL (http(s), relative, or data: URI).'),
|
|
621
|
+
unicode_range: z.string().describe('CSS unicode-range this face covers, for subsetting.').optional(),
|
|
622
|
+
})
|
|
623
|
+
.passthrough();
|
|
624
|
+
const cameraSchema = z
|
|
625
|
+
.object({
|
|
626
|
+
perspective: z.union([z.number().positive(), z.array(keyframeSchema), exprSchema]).describe('Focal distance in px (>0); smaller = stronger perspective foreshortening.'),
|
|
627
|
+
origin_x: z.union([z.number(), z.string()]).describe('Vanishing-point x, px or string (default canvas center).').optional(),
|
|
628
|
+
origin_y: z.union([z.number(), z.string()]).describe('Vanishing-point y, px or string (default canvas center).').optional(),
|
|
629
|
+
// CKP/1.0 movable pose (§4.4.2) — all default 0; identity pose ⇒ V=I.
|
|
630
|
+
x: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Camera dolly x in px (default 0).').optional(),
|
|
631
|
+
y: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Camera dolly y in px (default 0).').optional(),
|
|
632
|
+
z: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Camera dolly toward the scene in px; +z = closer (default 0).').optional(),
|
|
633
|
+
x_rotation: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Camera pitch in degrees (default 0).').optional(),
|
|
634
|
+
y_rotation: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Camera yaw in degrees (default 0).').optional(),
|
|
635
|
+
z_rotation: z.union([z.number(), z.array(keyframeSchema), exprSchema]).describe('Camera roll in degrees (default 0).').optional(),
|
|
636
|
+
// Compositing order under the camera (§4.4.3). Default 'depth'.
|
|
637
|
+
sort: z.enum(['depth', 'paint']).describe('Compositing order: "depth" (2.5D by z, default) or "paint" (fixed layer order, layer 1 on top).').optional(),
|
|
638
|
+
})
|
|
639
|
+
.passthrough();
|
|
640
|
+
// ── CKP/1.0 cross-field rules (§4.4) ──
|
|
641
|
+
// `rotation` and `z_rotation` are one slot — both authored is an error.
|
|
642
|
+
// (An earlier 1.1 draft also forbade glass under un-flattened 3D; the
|
|
643
|
+
// runtime now projects glass through the pane's plane homography, so
|
|
644
|
+
// glass×3D is legal — §4.7.)
|
|
645
|
+
function checkElements(elements, path, ctx) {
|
|
646
|
+
// Uniqueness (the AE one-element-per-layer invariant): every element
|
|
647
|
+
// in a container owns a distinct `layer`. Duplicates are a HARD error
|
|
648
|
+
// — sources are corrected at author time, never repaired on load.
|
|
649
|
+
// Reported on each colliding element's `layer`.
|
|
650
|
+
const layerIndices = new Map();
|
|
651
|
+
elements.forEach((raw, i) => {
|
|
652
|
+
if (typeof raw !== 'object' || raw === null)
|
|
653
|
+
return;
|
|
654
|
+
const layer = raw.layer;
|
|
655
|
+
if (typeof layer === 'number') {
|
|
656
|
+
const seen = layerIndices.get(layer);
|
|
657
|
+
if (seen)
|
|
658
|
+
seen.push(i);
|
|
659
|
+
else
|
|
660
|
+
layerIndices.set(layer, [i]);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
for (const [layer, indices] of layerIndices) {
|
|
664
|
+
if (indices.length < 2)
|
|
665
|
+
continue;
|
|
666
|
+
for (const i of indices) {
|
|
667
|
+
ctx.addIssue({
|
|
668
|
+
code: z.ZodIssueCode.custom,
|
|
669
|
+
path: [...path, i, 'layer'],
|
|
670
|
+
message: `duplicate layer ${layer} — each element in a container needs a unique layer (layer 1 = top); renumber the colliding elements`,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
elements.forEach((raw, i) => {
|
|
675
|
+
if (typeof raw !== 'object' || raw === null)
|
|
676
|
+
return;
|
|
677
|
+
const el = raw;
|
|
678
|
+
const elPath = [...path, i];
|
|
679
|
+
if (el.rotation !== undefined && el.z_rotation !== undefined) {
|
|
680
|
+
ctx.addIssue({
|
|
681
|
+
code: z.ZodIssueCode.custom,
|
|
682
|
+
path: [...elPath, 'z_rotation'],
|
|
683
|
+
message: '`rotation` and `z_rotation` are the same slot — author one, not both (§4.4)',
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
if (el.type === 'group' && Array.isArray(el.elements)) {
|
|
687
|
+
checkElements(el.elements, [...elPath, 'elements'], ctx);
|
|
688
|
+
}
|
|
689
|
+
const mask = el.mask;
|
|
690
|
+
if (el.type === 'group' && mask && Array.isArray(mask.elements)) {
|
|
691
|
+
checkElements(mask.elements, [...elPath, 'mask', 'elements'], ctx);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
export const sourceSchema = z
|
|
696
|
+
.object({
|
|
697
|
+
// Clipkit Protocol version. Absence defaults to '1.0'. Unknown values
|
|
698
|
+
// validate but downstream runtimes are expected to warn — see
|
|
699
|
+
// PROTOCOL.md §11.
|
|
700
|
+
clipkit_version: z.string().describe('Protocol version (default "1.0").').optional(),
|
|
701
|
+
output_format: z.enum(OUTPUT_FORMATS).describe('Output format: "mp4" (default) or "gif".').optional(),
|
|
702
|
+
width: z.number().int().positive().describe('Canvas width in px (default 1920).').optional(),
|
|
703
|
+
height: z.number().int().positive().describe('Canvas height in px (default 1080).').optional(),
|
|
704
|
+
duration: z.union([z.number().nonnegative(), z.literal('auto')]).describe('Total duration in seconds, or "auto" to fit the longest element (default "auto").').optional(),
|
|
705
|
+
frame_rate: z.number().positive().describe('Frames per second (default 30).').optional(),
|
|
706
|
+
background_color: z.string().describe('Canvas background color (default opaque black "#000000").').optional(),
|
|
707
|
+
fonts: z.array(fontFaceSchema).describe('Custom font faces to register before rendering.').optional(),
|
|
708
|
+
motion_blur: z
|
|
709
|
+
.object({
|
|
710
|
+
samples: z.number().int().min(1).max(32).describe('Sub-frame samples, 1-32 (default 8).').optional(),
|
|
711
|
+
shutter: z.number().gt(0).max(1).describe('Shutter as a fraction of the frame interval, 0..1 (default 0.5).').optional(),
|
|
712
|
+
})
|
|
713
|
+
.passthrough()
|
|
714
|
+
.optional(),
|
|
715
|
+
camera: cameraSchema.describe('Scene camera (perspective + pose); omit for flat 2D.').optional(),
|
|
716
|
+
lights: z.array(lightSchema).describe('Scene lights for PBR materials; omit for unlit.').optional(),
|
|
717
|
+
environment: environmentSchema.describe('Environment map for material reflections.').optional(),
|
|
718
|
+
bloom: bloomSchema.describe('Post-process bloom (glow on bright areas).').optional(),
|
|
719
|
+
elements: z.array(elementSchema).min(1).describe('The scene content (at least one element); drawn by z-depth then layer order (layer 1 on top).'),
|
|
720
|
+
})
|
|
721
|
+
.passthrough()
|
|
722
|
+
.superRefine((src, ctx) => {
|
|
723
|
+
if (Array.isArray(src.elements)) {
|
|
724
|
+
checkElements(src.elements, ['elements'], ctx);
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
//# sourceMappingURL=zod.js.map
|