@vosjs/cli 0.53.0 → 0.53.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vosjs/cli",
3
- "version": "0.53.0",
3
+ "version": "0.53.1",
4
4
  "description": "The vos CLI: record the real product from a scripted browser flow, auto-zoom from the cursor track, cut as data in doc.json, render deterministic video and stills, deliver a release's media per destination spec, and sync with vos.so. One binary, every verb, MIT.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -49,14 +49,14 @@
49
49
  "dependencies": {
50
50
  "mediabunny": "^1.55.7",
51
51
  "playwright": "^1.49.0",
52
+ "@vosjs/core": "^0.25.1",
52
53
  "@vosjs/editor": "^1.4.0",
53
- "@vosjs/core": "^0.25.0",
54
- "@vosjs/elements": "^0.8.2",
55
54
  "@vosjs/render-core": "^0.3.7",
56
55
  "@vosjs/shared": "^0.4.1",
56
+ "@vosjs/elements": "^0.8.2",
57
57
  "@vosjs/studio-core": "^0.33.0",
58
- "@vosjs/tween": "^0.8.2",
59
- "@vosjs/timeline": "^0.4.1"
58
+ "@vosjs/timeline": "^0.4.1",
59
+ "@vosjs/tween": "^0.8.2"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/node": "^22",
package/skills/VERSION CHANGED
@@ -1 +1 @@
1
- 0.7.1
1
+ 0.9.0
@@ -23,7 +23,11 @@ Two ways to get this wrong, both seen in real ports:
23
23
  Read `references/intro-port.mjs` first: the Remotion showreel's intro scene,
24
24
  ported by these rules and checked against Remotion's own render. It is the
25
25
  shape every port takes: real functions, stringified into a `config.json`
26
- (`node intro-port.mjs` writes one beside it).
26
+ (`node intro-port.mjs` writes one beside it). Every field a config, an
27
+ element, `ctx` and an ease accept is in `references/elements-and-context.md`,
28
+ copied from the published declarations with the facts they leave out
29
+ (which props re-raster, what `vos check` says about eases): read it there,
30
+ not in `node_modules`.
27
31
 
28
32
  ## The rules (the port grammar)
29
33
 
@@ -72,18 +76,64 @@ shape every port takes: real functions, stringified into a `config.json`
72
76
  translateY }` in design pixels, never a `tl.set` on `props.x/y`, for any
73
77
  text whose content or colour changes live: every re-raster lays the
74
78
  element out again from its config and drops a tweened position.
75
- - **Bind a live text to its full words.** Text that `onFrame` writes each
76
- frame (a scramble, a counter) starts as `{ "$data": "subtitle" }`, never
77
- `' '`: a still capture (and so the vos.so thumbnail) misses what `onFrame`
78
- writes, and then shows the bound words instead of nothing.
79
+ - **Text that `onFrame` writes is missing from a still.** A scramble or a
80
+ counter written into `props.content` each frame shows in `vos render`
81
+ output (a frame late) and is BLANK in `vos still` and in the vos.so
82
+ thumbnail. Check it in a rendered frame, and set the program's cover
83
+ (`vos push --still <t>`, cli 0.53+) at a moment where that text is not the
84
+ point.
79
85
  - **Transform origin is the centre.** Remotion's `transformOrigin: 'right
80
86
  center'` with `scaleX` becomes a centre scale plus an `x` tween that keeps
81
87
  the right edge still: `{ scaleX: 0, x: x0 + (width / 2) * k }`.
82
88
 
89
+ ## Layering: where a painter goes (measured)
90
+
91
+ A frame draws in this order, and a painter joins it at the place it names:
92
+
93
+ 1. **The 3D scene** (`ctx.scene`, `ctx.camera`) first, `scene.background`
94
+ under all of it. A ground or a 3D object lives here, under every element.
95
+ 2. **Then the elements**, one overlay scene per distinct element `zIndex`
96
+ (default 100), lowest first, depth cleared between them, all drawn with
97
+ `ctx.overlayCamera`: orthographic, in RENDER pixels, centred on the frame
98
+ (x right, y up in three.js terms).
99
+ 3. **Inside an overlay scene, later wins**: element *i* of `config.elements`
100
+ has `renderOrder = zIndex + i × 0.01` (a split unit adds `0.001` per
101
+ unit). Order elements in the array the way the source stacks them.
102
+
103
+ A painter that must sit BETWEEN elements is a plane in `ctx.overlayScene`
104
+ (the lowest-zIndex overlay scene, so keep every element at the default
105
+ `zIndex`) with a `renderOrder` between its neighbours:
106
+
107
+ ```js
108
+ // createContent: a painter over element 1 and under element 2
109
+ const T = ctx.THREE
110
+ const canvas = document.createElement('canvas')
111
+ const tex = new T.CanvasTexture(canvas)
112
+ tex.colorSpace = T.SRGBColorSpace // or every colour renders lighter
113
+ const mesh = new T.Mesh(
114
+ new T.PlaneGeometry(1, 1),
115
+ new T.MeshBasicMaterial({ map: tex, transparent: true, depthTest: false, depthWrite: false }),
116
+ )
117
+ mesh.scale.set(ctx.resolution.width, ctx.resolution.height, 1) // full frame, render px
118
+ mesh.renderOrder = 100 + 1 * 0.01 + 0.005
119
+ mesh.frustumCulled = false
120
+ ctx.overlayScene.add(mesh)
121
+ // onFrame: paint `canvas` from ctx.time and ctx.data, then tex.needsUpdate = true
122
+ ```
123
+
124
+ Keep the slot numbers in `data` beside the scene table (`layers: { card:
125
+ 100.015, grain: 100.995 }`), computed from element indices in the build
126
+ script, so adding an element never silently reorders a painter. A painter
127
+ over everything (grain, a vignette) takes the highest slot; a CSS blend mode
128
+ is a custom `blending` on its material, stated in the push note as an
129
+ approximation.
130
+
83
131
  ## The mapping
84
132
 
85
133
  - Remotion: `references/remotion.md`.
86
134
  - HyperFrames: `references/hyperframes.md`.
135
+ - The target's types (elements, `ctx`, the timeline, eases):
136
+ `references/elements-and-context.md`.
87
137
  - A hand-rolled page (a single HTML file, its own canvas engine): read it as
88
138
  source with the same tables. What the page draws with DOM becomes
89
139
  elements; what it paints in a canvas is a painter, kept to the procedural
@@ -0,0 +1,591 @@
1
+ <!-- Generated by scripts/build-element-reference.mjs from @vosjs/core 0.25.1, @vosjs/elements 0.8.2 and @vosjs/timeline 0.4.1. Do not edit by hand: re-run the script. -->
2
+
3
+ # Elements, the context and eases: the declarations
4
+
5
+ What a program's config and functions accept, copied from the published
6
+ type declarations so a port never has to read `node_modules`. The notes
7
+ between the blocks are the facts the declarations do not say; the units
8
+ are in SKILL.md's "coordinates, measured" and the draw order in its
9
+ "Layering".
10
+
11
+ ## The config
12
+
13
+ A `config.json` is this shape, functions as strings. `elements` is typed
14
+ loosely here; its members are the element types below.
15
+
16
+ ```ts
17
+ interface FontFaceDecl {
18
+ /** Family name as used in `font.family` / canvas font strings. */
19
+ family: string;
20
+ /** Font file URL (woff2/woff/ttf). Must be reachable from render pages. */
21
+ url: string;
22
+ /** CSS weight the file carries (default 'normal'). One decl per weight. */
23
+ weight?: number | string;
24
+ style?: 'normal' | 'italic';
25
+ }
26
+
27
+ /**
28
+ * JSON-serializable version of VosConfig.
29
+ * Functions are stored as strings that can be embedded in the compiled template.
30
+ *
31
+ * Note: Elements are typed loosely as Record<string, unknown>[] for JSON
32
+ * transport compatibility. The actual ElementConfig type validation happens
33
+ * at compile time in compileVosConfig.
34
+ */
35
+ interface VosConfigJson {
36
+ /**
37
+ * Which schema era this config was written against.
38
+ *
39
+ * Required, because this is the CANONICAL shape: what is stored, served
40
+ * and read back later, when nobody can tell the era from the file itself.
41
+ * `migrateConfig` stamps it, so the type you hand a storer always has one.
42
+ * The authoring shape that may omit it is `AuthoredVosConfigJson`.
43
+ */
44
+ version: number;
45
+ /**
46
+ * Total duration of one animation cycle in seconds.
47
+ */
48
+ duration: number;
49
+ /** Scene configuration (background, fog) */
50
+ scene?: SceneConfig;
51
+ /** Camera configuration */
52
+ camera: CameraConfig;
53
+ /** Post-processing effects */
54
+ postprocessing?: PostprocessingEffect[];
55
+ /** Declare per-layer effect types for addon imports */
56
+ perLayerEffects?: PostprocessingEffect[];
57
+ /** Enable per-frame render group rebuild when zIndex changes at runtime */
58
+ dynamicLayers?: boolean;
59
+ /** 2D Elements rendered as textured planes (loosely typed for JSON transport) */
60
+ elements?: Record<string, unknown>[];
61
+ /** Declarative world-space 3D objects (primitives / GLB). */
62
+ objects?: Record<string, unknown>[];
63
+ /**
64
+ * Webfont faces to register and load BEFORE anything rasterizes text.
65
+ * Canvas text silently falls back to a default font when a family isn't
66
+ * loaded — headless render environments have near-zero system fonts, so any
67
+ * non-generic family used by text elements (or setup-drawn canvases) should
68
+ * be declared here with a self-hosted URL. Loading is awaited capped and
69
+ * fail-open: a dead URL degrades to fallback stacks, never a hung page.
70
+ */
71
+ fonts?: FontFaceDecl[];
72
+ /**
73
+ * Arbitrary input data made available to functions as `ctx.data`.
74
+ * The shape is the author's/app's, not vos's — vos passes it through verbatim.
75
+ * Overridable at runtime via `initVos(container, deps)` `deps.data` (so a live
76
+ * editor can update data without recompiling); `config.data` is the baked default.
77
+ * @example { cursor: [{ t: 0, x: 10, y: 20, type: 'down' }] }
78
+ */
79
+ data?: Record<string, unknown>;
80
+ /**
81
+ * Async setup hook as a string.
82
+ * @example "(ctx) => { const loader = new ctx.loaders.FontLoader(); ... }"
83
+ */
84
+ setup?: string;
85
+ /**
86
+ * Create scene content function as a string.
87
+ * @example "(ctx, setupData) => { const { THREE, scene } = ctx; ... }"
88
+ */
89
+ createContent: string;
90
+ /**
91
+ * Create GSAP timeline function as a string.
92
+ * @example "(ctx, content, duration) => { const tl = ctx.gsap.timeline(); ... }"
93
+ */
94
+ createTimeline: string;
95
+ /**
96
+ * Optional per-frame update function as a string.
97
+ * @example "(ctx, content, deltaTime) => { content.refs.uniforms.iTime.value += deltaTime; }"
98
+ */
99
+ onFrame?: string;
100
+ /**
101
+ * Evaluate the program at `f(t)`, as a string: `(t, data) => number`, a
102
+ * pure function of the OUTPUT time and `ctx.data`. Slow motion, ramps,
103
+ * reverse, freeze frames, ping-pong loops. See `VosConfig.retime`.
104
+ */
105
+ retime?: string;
106
+ /**
107
+ * The program stack: more programs on this context, run after the main one
108
+ * in array order, each with its own `ctx.data` and error boundary. A HUD, a
109
+ * subtitle pass, a watermark, an overlay a remixer adds without touching the
110
+ * main program's code. No timeline of their own — one master clock.
111
+ */
112
+ stack?: ProgramEntryJson[];
113
+ }
114
+ ```
115
+
116
+ ## Elements (`config.elements[]`)
117
+
118
+ `content`, `font.family` and `font.color` of a text element are the three
119
+ fields that take a `{ "$data": "<key>" }` binding, and the only ones that
120
+ re-resolve on a data edit; every other field is read once at build.
121
+ `font.size`, `letterSpacing`, `transform.translateX/Y`, `stroke.width`
122
+ and `shadow.blur` are design pixels (a 1080-high frame). There is no
123
+ `mask`, `clip`, `blend` or group field on any element: SKILL.md's
124
+ "honest gaps" says what to do instead.
125
+
126
+ ```ts
127
+ /**
128
+ * `{$data: key}` binding — the value resolves from the host's data object at
129
+ * render time and re-resolves on setData (a pure data edit, no recompile).
130
+ */
131
+ interface DataRef {
132
+ $data: string;
133
+ }
134
+
135
+ /**
136
+ * Base interface for all element types
137
+ */
138
+ interface BaseElement {
139
+ /** Unique identifier for referencing in createTimeline */
140
+ id?: string;
141
+ type: 'text' | 'image' | 'svg' | 'video' | 'audio';
142
+ /** Position on screen */
143
+ position: ElementPosition;
144
+ /** Transform origin */
145
+ anchor?: Anchor;
146
+ /** Layer order (higher = on top, default: 100) */
147
+ zIndex?: number;
148
+ /** Opacity 0-1 */
149
+ opacity?: number;
150
+ /** 3D transform */
151
+ transform?: Transform;
152
+ }
153
+
154
+ /**
155
+ * Position can be pixels, percentages, or preset strings
156
+ */
157
+ type ElementPosition = {
158
+ x: number;
159
+ y: number;
160
+ } | {
161
+ x: string;
162
+ y: string;
163
+ } | PositionPreset;
164
+
165
+ type PositionPreset = 'center' | 'top-left' | 'top-center' | 'top-right' | 'center-left' | 'center-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
166
+
167
+ type Anchor = 'center' | 'top-left' | 'top' | 'top-right' | 'left' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right';
168
+
169
+ /**
170
+ * 3D transform properties
171
+ */
172
+ interface Transform {
173
+ translateX?: number;
174
+ translateY?: number;
175
+ translateZ?: number;
176
+ rotateX?: number;
177
+ rotateY?: number;
178
+ rotateZ?: number;
179
+ rotation?: number;
180
+ scale?: number;
181
+ scaleX?: number;
182
+ scaleY?: number;
183
+ perspective?: number;
184
+ origin?: {
185
+ x: string;
186
+ y: string;
187
+ };
188
+ }
189
+
190
+ interface TextElement extends BaseElement {
191
+ type: 'text';
192
+ /** Text content (supports \n for multiline), or a `{$data}` binding */
193
+ content: string | DataRef;
194
+ font?: {
195
+ family?: string | DataRef;
196
+ size?: number;
197
+ weight?: number | string;
198
+ style?: 'normal' | 'italic';
199
+ color?: string | DataRef;
200
+ letterSpacing?: number;
201
+ lineHeight?: number;
202
+ align?: 'left' | 'center' | 'right';
203
+ };
204
+ stroke?: {
205
+ color: string;
206
+ width: number;
207
+ };
208
+ shadow?: {
209
+ color: string;
210
+ blur: number;
211
+ offsetX?: number;
212
+ offsetY?: number;
213
+ };
214
+ /**
215
+ * Split text into segments for per-character/word/line animation.
216
+ * When split is defined, the element exposes a `segments` array
217
+ * of ElementProps for animating individual parts.
218
+ */
219
+ split?: {
220
+ /** Type of split: 'chars', 'words', or 'lines' */
221
+ type: 'chars' | 'words' | 'lines';
222
+ };
223
+ }
224
+
225
+ interface ImageElement extends BaseElement {
226
+ type: 'image';
227
+ /** URL, base64, or imported asset path */
228
+ src: string;
229
+ size?: {
230
+ width?: number | 'auto';
231
+ height?: number | 'auto';
232
+ fit?: 'contain' | 'cover' | 'fill';
233
+ };
234
+ filters?: {
235
+ brightness?: number;
236
+ contrast?: number;
237
+ saturate?: number;
238
+ blur?: number;
239
+ hueRotate?: number;
240
+ grayscale?: number;
241
+ };
242
+ borderRadius?: number;
243
+ }
244
+
245
+ interface SVGElement extends BaseElement {
246
+ type: 'svg';
247
+ /** SVG string, URL, or imported asset */
248
+ src: string;
249
+ size?: {
250
+ width?: number | 'auto';
251
+ height?: number | 'auto';
252
+ };
253
+ /** Override colors in SVG */
254
+ colors?: Record<string, string>;
255
+ }
256
+
257
+ interface VideoElement extends BaseElement {
258
+ type: 'video';
259
+ src: string;
260
+ size?: {
261
+ width?: number | 'auto';
262
+ height?: number | 'auto';
263
+ fit?: 'contain' | 'cover' | 'fill';
264
+ };
265
+ loop?: boolean;
266
+ muted?: boolean;
267
+ playbackRate?: number;
268
+ startTime?: number;
269
+ /**
270
+ * Decode strategy:
271
+ * - 'html5' (default): HTMLVideoElement + VideoTexture (legacy; not frame-accurate)
272
+ * - 'webcodecs': frame-accurate WebCodecs decode (deterministic export/scrub)
273
+ * - 'auto': webcodecs when available, else html5
274
+ */
275
+ frameSource?: 'auto' | 'webcodecs' | 'html5';
276
+ }
277
+
278
+ /**
279
+ * Non-visual element that plays an audio file synced to the master clock.
280
+ * Drive it like an html5 video: set `playing` and/or animate `currentTime`
281
+ * in createTimeline; playback honors the global pause/seek state, and
282
+ * `props.gain` (0-1) is animatable for fades. Renders no pixels — position,
283
+ * anchor and transform are accepted for BaseElement compatibility but ignored.
284
+ */
285
+ interface AudioElement extends Omit<BaseElement, 'position'> {
286
+ type: 'audio';
287
+ /** Audio file URL (anything the browser's media stack decodes) */
288
+ src: string;
289
+ /** Ignored (audio renders no pixels) */
290
+ position?: ElementPosition;
291
+ /** Initial volume 0-1 (default: 1); animatable via props.gain */
292
+ gain?: number;
293
+ loop?: boolean;
294
+ /** Offset into the source when playback begins, seconds (default: 0) */
295
+ startTime?: number;
296
+ /**
297
+ * A gain envelope over OUTPUT time: `[t, gain]` points, linear between
298
+ * them, held flat outside, multiplied with `props.gain`. Fades, ducking, a
299
+ * bed that swells under a title, as data the offline renderer replays
300
+ * (`@vosjs/core/audio`) and live playback follows frame by frame.
301
+ */
302
+ gainEnvelope?: Array<[t: number, gain: number]>;
303
+ }
304
+ ```
305
+
306
+ ## An element at runtime (`ctx.elements.get(id)`)
307
+
308
+ What `createTimeline` and `onFrame` animate: `el.props` (and
309
+ `el.segments[i]` under `split`). `props.x` / `props.y` are RENDER pixels
310
+ from the frame centre with `y` down, and `rotation` is degrees
311
+ counter-clockwise, unlike the config's design-pixel `transform`.
312
+
313
+ Writing any of `content`, `fontSize`, `fontFamily`, `fontWeight`, `fontStyle`, `letterSpacing`, `color`, `strokeColor`, `strokeWidth` on a text element's `props`
314
+ re-rasters it (the runtime's own list), so a scramble or a counter writes
315
+ `el.props.content` in `onFrame` and a colour change sets `el.props.color`.
316
+ The tween dialect animates numbers only: tween `fontSize` and
317
+ `letterSpacing`, SET the strings. `props.zIndex` is settable too.
318
+
319
+ Every such write queues a re-raster, even of an unchanged value, so guard
320
+ a per-frame write: `if (el.props.content !== next) el.props.content = next`.
321
+ A `split` text element does not re-raster at all: these writes are ignored
322
+ on it, so text that changes and text that animates per letter are two
323
+ elements.
324
+
325
+ ```ts
326
+ /**
327
+ * GSAP-animatable properties exposed on each element instance
328
+ */
329
+ interface ElementProps {
330
+ x: number;
331
+ y: number;
332
+ z: number;
333
+ opacity: number;
334
+ scale: number;
335
+ scaleX: number;
336
+ scaleY: number;
337
+ rotation: number;
338
+ rotationX: number;
339
+ rotationY: number;
340
+ /** Layer order inside the element's overlay scene (writes `renderOrder`) */
341
+ zIndex?: number;
342
+ content?: string;
343
+ fontSize?: number;
344
+ fontFamily?: string;
345
+ fontWeight?: number | string;
346
+ fontStyle?: 'normal' | 'italic';
347
+ letterSpacing?: number;
348
+ color?: string;
349
+ strokeColor?: string;
350
+ strokeWidth?: number;
351
+ /** Current playback position in seconds (animatable with GSAP) */
352
+ currentTime?: number;
353
+ /** Video duration in seconds (read-only) */
354
+ readonly duration?: number;
355
+ /** Whether the video is playing (controls native playback) */
356
+ playing?: boolean;
357
+ /** Start offset for video playback */
358
+ startOffset?: number;
359
+ /** Volume 0-1 (audio elements; animatable for fades) */
360
+ gain?: number;
361
+ }
362
+
363
+ /**
364
+ * Runtime element instance with animatable props and methods
365
+ */
366
+ interface ElementInstance {
367
+ /** Original configuration */
368
+ config: ElementConfig;
369
+ /** Three.js mesh (textured plane) */
370
+ mesh: THREE.Mesh;
371
+ /** DOM node for SplitText (text elements only) - typed as unknown for portability */
372
+ node: unknown;
373
+ /** GSAP-animatable properties */
374
+ props: ElementProps;
375
+ /**
376
+ * Split text segments (only available when split config is defined).
377
+ * Each segment has its own ElementProps for individual animation.
378
+ */
379
+ segments?: ElementProps[];
380
+ /** Update element content (text or image src) */
381
+ setContent: (content: string) => void;
382
+ /**
383
+ * Re-resolve `{$data}`-bound props against fresh data (called by the
384
+ * compiled module's setData). Returns true when a change was picked up.
385
+ */
386
+ updateData?: (data: Record<string, unknown> | null | undefined) => boolean;
387
+ /**
388
+ * Re-raster with unchanged values — the late-webfont hook (a data-carried
389
+ * face landing after first paint re-draws over the fallback stack).
390
+ */
391
+ refreshRaster?: () => boolean;
392
+ /** Re-rasterize canvas-backed textures for a new output resolution */
393
+ updateResolution?: (resolution: unknown) => boolean;
394
+ /** Remove element from scene */
395
+ destroy: () => void;
396
+ }
397
+ ```
398
+
399
+ ## The context (`ctx`)
400
+
401
+ `setup(ctx)` gets a `SetupContext` and may return assets;
402
+ `createContent(ctx, setupData)` returns a `ContentResult`;
403
+ `createTimeline(ctx, content, duration)` returns the timeline;
404
+ `onFrame(ctx, content, deltaTime)` runs every frame, `ctx.time` being the
405
+ program's time in seconds. `ctx.data` is read-only: a knob changes it from outside,
406
+ and `onFrame` reads the new value on the next frame. Read `ctx.data` on
407
+ every frame, never a copy taken in `createContent`.
408
+
409
+ ```ts
410
+ /**
411
+ * Resolution configuration passed to animations
412
+ */
413
+ interface Resolution {
414
+ width: number;
415
+ height: number;
416
+ pixelRatio: number;
417
+ /** Physical drawing buffer width (width × pixelRatio) - for shader uniforms */
418
+ drawingBufferWidth: number;
419
+ /** Physical drawing buffer height (height × pixelRatio) - for shader uniforms */
420
+ drawingBufferHeight: number;
421
+ }
422
+
423
+ /**
424
+ * Loaders registry - common Three.js loaders
425
+ */
426
+ interface LoadersRegistry {
427
+ FontLoader: any;
428
+ TextureLoader: any;
429
+ GLTFLoader: any;
430
+ HDRLoader: any;
431
+ CubeTextureLoader: any;
432
+ }
433
+
434
+ /**
435
+ * Utilities registry - common Three.js utilities
436
+ */
437
+ interface UtilsRegistry {
438
+ MeshSurfaceSampler: any;
439
+ BufferGeometryUtils: any;
440
+ TextGeometry: any;
441
+ }
442
+
443
+ /**
444
+ * Context available during async setup phase
445
+ */
446
+ interface SetupContext {
447
+ THREE: typeof THREE;
448
+ resolution: Resolution;
449
+ loaders: LoadersRegistry;
450
+ utils: UtilsRegistry;
451
+ /**
452
+ * Read-only input data exposed to all functions as `ctx.data`.
453
+ * Sourced from `config.data`, overridable at runtime by `initVos` `deps.data`.
454
+ * Always defined (defaults to `{}`). Shape is the author's/app's, not vos's.
455
+ */
456
+ data: Readonly<Record<string, unknown>>;
457
+ }
458
+
459
+ /**
460
+ * Context available during animation creation
461
+ */
462
+ interface VosContext extends SetupContext {
463
+ gsap: typeof gsap;
464
+ scene: THREE.Scene;
465
+ camera: THREE.Camera;
466
+ renderer: THREE.WebGLRenderer;
467
+ /** Dedicated scene for 2D overlay elements (rendered on top of main scene) */
468
+ overlayScene: THREE.Scene;
469
+ /** Orthographic camera for 2D overlay (pixel-space: 1 unit = 1 pixel) */
470
+ overlayCamera: THREE.OrthographicCamera;
471
+ composer?: unknown;
472
+ /** Element instances for timeline animations */
473
+ elements: Map<string, ElementInstance>;
474
+ /** Current playback time in seconds (available in onFrame) */
475
+ time: number;
476
+ /** Playback progress 0-1 (available in onFrame) */
477
+ progress: number;
478
+ /**
479
+ * The OUTPUT time in seconds: what the transport shows and the capture
480
+ * counts. Equal to `time` unless the config carries a `retime`, in which
481
+ * case `time` is `retime(outputTime, data)` — the program's own time —
482
+ * while `outputTime` keeps counting the output. Stack entries read
483
+ * `ctx.time` as output time (they are output-anchored by contract).
484
+ */
485
+ outputTime: number;
486
+ }
487
+
488
+ /**
489
+ * Result from createContent function
490
+ */
491
+ interface ContentResult {
492
+ /** Objects added to scene */
493
+ objects: THREE.Object3D[];
494
+ /** Named references for timeline animations */
495
+ refs?: Record<string, unknown>;
496
+ /** Cleanup function for content-specific resources */
497
+ dispose?: () => void;
498
+ }
499
+ ```
500
+
501
+ ## The timeline (what `createTimeline` returns)
502
+
503
+ Author it as GSAP: `const tl = ctx.gsap.timeline({ paused: true })`, tweens
504
+ on `el.props`, `tl.addLabel(name, t)` per scene, return `tl`. This is the
505
+ whole surface the engine calls on it.
506
+
507
+ ```ts
508
+ /**
509
+ * Structural master-clock interface the engine seeks each frame.
510
+ *
511
+ * This is the ONLY surface the runtime uses from the timeline object returned by
512
+ * `createTimeline` (pause/seek/play + transport queries + carrier retiming). It is
513
+ * satisfied structurally by `gsap.core.Timeline` today, so authoring against real
514
+ * GSAP is unchanged — but the public API no longer hard-depends on the `gsap` type,
515
+ * which lets an alternate deterministic backend provide a conformant timeline later
516
+ * without a breaking change. Method shorthand is intentional (bivariant params) so
517
+ * GSAP's overloaded signatures remain structurally assignable.
518
+ */
519
+ interface VosTimeline {
520
+ /** Pause playback (frame-stepped export pauses before the first frame). */
521
+ pause(): unknown;
522
+ /** Resume playback. */
523
+ play(): unknown;
524
+ /** Seek to `time` seconds. `suppressEvents=false` fires onUpdate callbacks. */
525
+ seek(time: number, suppressEvents?: boolean): unknown;
526
+ /** Rebuild/clear children (used by the vosCarrier duration-retime path). */
527
+ clear(): unknown;
528
+ /** Set playback rate. */
529
+ timeScale(value: number): unknown;
530
+ /** Current playhead in seconds. */
531
+ time(): number;
532
+ /** Normalized progress 0..1. */
533
+ progress(): number;
534
+ /**
535
+ * Optional (the vos tween backend): re-apply a tween-timing overlay over the
536
+ * recorded tweens, from the recording every time. The bridge's
537
+ * SET_TWEEN_EDITS rides it; absent on a gsap timeline.
538
+ */
539
+ applyEdits?(edits: readonly Record<string, unknown>[]): unknown;
540
+ /** Configured duration in seconds. */
541
+ duration(): number;
542
+ /** Total duration including repeats (seconds). */
543
+ totalDuration(): number;
544
+ /** Attach/read a lifecycle callback (engine uses onUpdate). */
545
+ eventCallback(type: string, callback?: (...args: any[]) => void): unknown;
546
+ /** Opaque author-attached marker (e.g. `{ vosCarrier: true }`). */
547
+ data?: unknown;
548
+ }
549
+ ```
550
+
551
+ ## Eases
552
+
553
+ An `ease` string anywhere GSAP takes one. Beyond the names below the
554
+ engine accepts the parameterized forms `back.out(1.7)`,
555
+ `elastic.out(1, 0.3)`, `steps(5)`, and `css-bezier(x1, y1, x2, y2)`,
556
+ which is CSS's `cubic-bezier` and Remotion's `Easing.bezier` with the same
557
+ four numbers (`cubic-bezier` itself is not a name). A bare family
558
+ (`'power2'`) means `.out`. **An unknown name is not an error: it plays
559
+ linear.** `vos check` warns `unknown-ease` for a literal `ease: '…'` it
560
+ cannot resolve; through `@vosjs/core` 0.25.0 that warning also fired on
561
+ `css-bezier`, wrongly, so ignore it there. An ease held in a variable is
562
+ not checked: the side-by-side is where it shows.
563
+
564
+ ```ts
565
+ /**
566
+ * Value types for deterministic timeline math.
567
+ *
568
+ * These are meant to be EMBEDDED inside an app's own document schema — @vosjs/timeline
569
+ * is an evaluation library, not a document format. Everything is JSON-serializable
570
+ * (eases are registry names, never functions) so the same values travel through
571
+ * `ctx.data` into a running vos program and evaluate identically on both sides.
572
+ */
573
+ type EaseFamily = 'power1' | 'power2' | 'power3' | 'power4' | 'sine' | 'expo' | 'circ' | 'back' | 'elastic' | 'bounce';
574
+
575
+ type EaseDirection = 'in' | 'out' | 'inOut';
576
+
577
+ /**
578
+ * Serializable easing name. The vocabulary (and the curves) match GSAP's so one
579
+ * ease language spans freeform function-strings and declarative keyframes.
580
+ */
581
+ type EaseName = 'none' | 'linear' | `${EaseFamily}.${EaseDirection}`;
582
+
583
+ /**
584
+ * Resolve an ease by name, including parameterized forms (`back.out(1.7)`,
585
+ * `elastic.out(1, 0.3)`, `steps(5)`) and GSAP's bare-family default
586
+ * (`'power2'` → `power2.out`). Unknown names fall back to linear — evaluation
587
+ * must never throw per-frame inside a running program; authoring layers are
588
+ * expected to validate names at edit time instead.
589
+ */
590
+ function resolveEase(name: string | undefined): EaseFn;
591
+ ```
@@ -21,6 +21,7 @@ units (render pixels, y down, degrees counter-clockwise).
21
21
  | a sub-composition or scene `div` with a time window | `tl.addLabel(name, t)` and its elements tweened in and out inside the window |
22
22
  | an `onUpdate` proxy clock that draws (`renderAll(t)`) | `onFrame(ctx)`, reading `ctx.time`, for exactly those procedural parts |
23
23
  | GSAP ease names (`power3.out`, `back.out(1.7)`, `expo.inOut`) | the same names |
24
+ | a CSS `cubic-bezier(a, b, c, d)` timing | `ease: 'css-bezier(a, b, c, d)'` (exact; spelled `cubic-bezier` it plays linear) |
24
25
  | CSS `--chrome` set by the timeline | a `data` value read in `onFrame`, or a tween on the element that shows it |
25
26
  | faces resolved by the runtime from CSS families | `fonts: [{ family, weight, url }]` from the catalog |
26
27
  | `mix-blend-mode`, `clip-path`, `overflow: hidden` reveals | GAPS: the painter, or an opacity approximation, said |