@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.
|
|
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/
|
|
59
|
-
"@vosjs/
|
|
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.
|
|
1
|
+
0.9.0
|
package/skills/vos-port/SKILL.md
CHANGED
|
@@ -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
|
-
- **
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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 |
|