@vosjs/studio-core 0.1.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 +21 -0
- package/README.md +69 -0
- package/dist/index.d.ts +2973 -0
- package/dist/index.js +7014 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2973 @@
|
|
|
1
|
+
import { Segment, KeyframeTrack } from '@vosjs/timeline';
|
|
2
|
+
export { Segment } from '@vosjs/timeline';
|
|
3
|
+
import { TimelineEdit } from '@vosjs/shared/timelineEdits';
|
|
4
|
+
import { LaneAdapter } from '@vosjs/editor';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Zoom/pan camera styles — named strategy
|
|
8
|
+
* presets covering the whole auto-zoom pipeline: how the planner turns clicks
|
|
9
|
+
* into spans (cluster vs session merging, level clamps, follow default) AND
|
|
10
|
+
* how the lowering animates the camera (ramp durations, eases, connected-pan
|
|
11
|
+
* gap, dead-zone follow tuning). One name → one coherent feel.
|
|
12
|
+
*
|
|
13
|
+
* The presets are grounded in a measured comparison of the four shipping
|
|
14
|
+
* strategies (frame-by-frame optical-flow tracking of real exports + source
|
|
15
|
+
* audits of Recordly/OpenScreen + Cursorful bundle/behavior research):
|
|
16
|
+
*
|
|
17
|
+
* - Cursorful ("glide"): ONE modest zoom (~1.5×) per activity session —
|
|
18
|
+
* 2+ clicks within a rolling window keep the zoom alive — and the camera
|
|
19
|
+
* TRAVELS by panning between focus points while zoomed. Measured zoom ramp
|
|
20
|
+
* fits css-bezier(0.26, 0, 0.16, 1): near-zero initial velocity, soft
|
|
21
|
+
* landing, zero overshoot. Pan durations scale with distance.
|
|
22
|
+
* - Screen Studio "Focused" / Recordly ("focus"): a zoom block per click
|
|
23
|
+
* cluster at ~1.8×, spring-settled ramps (~1.5 s in / ~1 s out), direct
|
|
24
|
+
* pans across gaps ≤ ~1.35 s, dead-zone cursor follow. The measured
|
|
25
|
+
* OpenScreen/Recordly ramp (their bezier filtered through the spring) fits
|
|
26
|
+
* css-bezier(0.28, 0.03, 0.09, 1) — that curve IS the family feel.
|
|
27
|
+
* - Screen Studio "Smooth" ("cinema"): the same block model, slower and
|
|
28
|
+
* more fluid — for content that is watched, not read.
|
|
29
|
+
* - "snappy": the studio's original fast cycles, with the defect fixed — the raw
|
|
30
|
+
* css-bezier(0.16, 1, 0.3, 1) arrival had an initial velocity 6.25× the
|
|
31
|
+
* ramp average (the "jump cut" complaint); competitors always filter that
|
|
32
|
+
* curve through a spring. This preset keeps the pace but caps the onset.
|
|
33
|
+
* - "cut": Screen Studio's "instant zoom" option — hard cut-in, no glide.
|
|
34
|
+
*
|
|
35
|
+
* Style changes are live SET_DATA (the zoom track is data); regenerating
|
|
36
|
+
* auto spans on a style switch re-plans with the style's planner params while
|
|
37
|
+
* preserving user-touched ('manual') spans, per the wand contract.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
type ZoomStyleName = 'glide' | 'focus' | 'cinema' | 'snappy' | 'cut' | 'none' | 'keynote' | 'drift';
|
|
41
|
+
/**
|
|
42
|
+
* The tilt half of a camera style: the Dynamic-tilt
|
|
43
|
+
* intensity the style ships with, plus optional overrides on the tilt track's
|
|
44
|
+
* motion constants. A style pick stamps `doc.tiltStyle` with `intensity` and
|
|
45
|
+
* re-plans auto tilt spans alongside the auto zooms — one name, one coherent
|
|
46
|
+
* camera sentence (zoom AND lean).
|
|
47
|
+
*/
|
|
48
|
+
interface TiltPersonality {
|
|
49
|
+
/** Dynamic-tilt intensity this style ships with ('off' = flat card). */
|
|
50
|
+
intensity: TiltStyleName;
|
|
51
|
+
/** tilt ramp overrides (seconds); absent = the TILT_RAMP_* constants. */
|
|
52
|
+
rampIn?: number;
|
|
53
|
+
rampOut?: number;
|
|
54
|
+
/** output-time gap ≤ this → swing pose-to-pose (absent = TILT_CHAIN_GAP). */
|
|
55
|
+
chainGap?: number;
|
|
56
|
+
/** connected-swing duration (absent = TILT_PAN). */
|
|
57
|
+
pan?: number;
|
|
58
|
+
}
|
|
59
|
+
interface ZoomStyleParams {
|
|
60
|
+
/** false = the planner emits nothing (the 'none' style — manual zooms only). */
|
|
61
|
+
autoZoom: boolean;
|
|
62
|
+
/** clicks within this many seconds merge into one span (session merge). */
|
|
63
|
+
clusterGap: number;
|
|
64
|
+
/** minimum clicks for a cluster to earn a zoom (Cursorful's ≥2 rule). */
|
|
65
|
+
minClusterClicks: number;
|
|
66
|
+
/** zoom so the clicked element fills ~this fraction of the frame. */
|
|
67
|
+
targetFill: number;
|
|
68
|
+
minLevel: number;
|
|
69
|
+
maxLevel: number;
|
|
70
|
+
/** span lead-in before the first click / hold after the last (seconds). */
|
|
71
|
+
lead: number;
|
|
72
|
+
hold: number;
|
|
73
|
+
/** planner emits spans with focusMode 'auto' (cursor-follow camera). */
|
|
74
|
+
followByDefault: boolean;
|
|
75
|
+
/** false = typing sessions plan no spans (clicks/dwells still do). */
|
|
76
|
+
typingZoom: boolean;
|
|
77
|
+
/** max silence between pings before the typing session ends (seconds). */
|
|
78
|
+
typingGap: number;
|
|
79
|
+
/** hold after the last keystroke — the read-what-you-typed beat (seconds). */
|
|
80
|
+
typingHold: number;
|
|
81
|
+
/**
|
|
82
|
+
* Level FLOOR for typing spans, above the style's minLevel: a wide field
|
|
83
|
+
* (URL bar, dialog search input) fit-clamps to this instead — typing is the
|
|
84
|
+
* moment being narrated, so it reads a notch punchier than a wide click.
|
|
85
|
+
*/
|
|
86
|
+
typingMinLevel: number;
|
|
87
|
+
/** zoom-in ramp duration; arrival lands rampInOverlap into the span. */
|
|
88
|
+
rampIn: number;
|
|
89
|
+
rampInOverlap: number;
|
|
90
|
+
rampOut: number;
|
|
91
|
+
/** output-time gap ≤ this → pan straight to the next span (no zoom-out). */
|
|
92
|
+
chainGap: number;
|
|
93
|
+
/** connected-pan duration. */
|
|
94
|
+
pan: number;
|
|
95
|
+
/** arrival ease (zoom-in AND zoom-out). */
|
|
96
|
+
ease: string;
|
|
97
|
+
/** connected-pan + follow-recenter ease. */
|
|
98
|
+
panEase: string;
|
|
99
|
+
/** recenter when the cursor exits this central fraction of the crop. */
|
|
100
|
+
followSafeRatio: number;
|
|
101
|
+
/** seconds the camera takes to glide to a recentered focus. */
|
|
102
|
+
followRecenter: number;
|
|
103
|
+
/**
|
|
104
|
+
* Recenter targets the cursor this many seconds AHEAD of the exit moment
|
|
105
|
+
* (Cursorful's look-ahead: the camera leads the pointer instead of chasing
|
|
106
|
+
* a stale position). Sampled from the real track — still deterministic.
|
|
107
|
+
*/
|
|
108
|
+
followLookahead: number;
|
|
109
|
+
/** The style's tilt personality — see TiltPersonality. */
|
|
110
|
+
tilt: TiltPersonality;
|
|
111
|
+
}
|
|
112
|
+
declare const ZOOM_STYLES: Record<ZoomStyleName, ZoomStyleParams>;
|
|
113
|
+
/** The default camera style for new projects (the Cursorful-family strategy). */
|
|
114
|
+
declare const DEFAULT_ZOOM_STYLE: ZoomStyleName;
|
|
115
|
+
/**
|
|
116
|
+
* Resolve a style name (+ optional per-doc overrides, `doc.zoomParams`) into a
|
|
117
|
+
* full parameter bundle. Overrides are the "Custom" seam: agents/doc.json can
|
|
118
|
+
* tune individual params on top of a named preset; the studio shows Custom
|
|
119
|
+
* while any override is present. Unknown names from hand-edited doc.json fall
|
|
120
|
+
* back to the default style.
|
|
121
|
+
*/
|
|
122
|
+
declare function resolveZoomStyle(name?: ZoomStyleName, overrides?: Partial<ZoomStyleParams>): ZoomStyleParams;
|
|
123
|
+
/** Picker order + copy for the studio's Camera style control. */
|
|
124
|
+
declare const ZOOM_STYLE_OPTIONS: {
|
|
125
|
+
name: ZoomStyleName;
|
|
126
|
+
label: string;
|
|
127
|
+
hint: string;
|
|
128
|
+
}[];
|
|
129
|
+
|
|
130
|
+
interface SpeedParams {
|
|
131
|
+
/** Seconds of no input at all before a stretch counts as idle. */
|
|
132
|
+
idleMin: number;
|
|
133
|
+
/** Rate applied to idle stretches. */
|
|
134
|
+
idleRate: number;
|
|
135
|
+
/** Seconds a typing session must last to earn a span. */
|
|
136
|
+
typingMin: number;
|
|
137
|
+
/** Rate applied to typing passages. */
|
|
138
|
+
typingRate: number;
|
|
139
|
+
/** Seconds a scroll run must last to earn a span. */
|
|
140
|
+
scrollMin: number;
|
|
141
|
+
/** Rate applied to scroll runs. */
|
|
142
|
+
scrollRate: number;
|
|
143
|
+
}
|
|
144
|
+
/** Conservative defaults: only stretches nobody wants to watch in real time. */
|
|
145
|
+
declare const DEFAULT_SPEED_PARAMS: SpeedParams;
|
|
146
|
+
/**
|
|
147
|
+
* An idle gap whose measured frame activity (the digest's per-second
|
|
148
|
+
* changed-pixel fraction) averages above this is the video PLAYING — the
|
|
149
|
+
* recording's own playback, a render in progress — not idle. Speeding it up
|
|
150
|
+
* compresses the payoff. Five real takes (2026-08-25) each had one; the
|
|
151
|
+
* cursor track alone cannot tell, so this needs the activity witness, and
|
|
152
|
+
* without one (the studio's ingest) the gap still plans as idle.
|
|
153
|
+
*/
|
|
154
|
+
declare const PLAYBACK_ACTIVITY = 0.1;
|
|
155
|
+
declare function planAutoSpeed(track: readonly CursorEvent[], opts: {
|
|
156
|
+
durationMs: number;
|
|
157
|
+
params?: Partial<SpeedParams>;
|
|
158
|
+
/** Per-SOURCE-second motion bins (0..1) when a digest measured them. */
|
|
159
|
+
activity?: readonly number[] | null;
|
|
160
|
+
}): SpeedSpan[];
|
|
161
|
+
/**
|
|
162
|
+
* Scroll runs as [start, last] seconds — the same grouping the speed planner
|
|
163
|
+
* proposes 2× over, exported for the take digest.
|
|
164
|
+
*/
|
|
165
|
+
declare function scrollRuns(track: readonly CursorEvent[], minLen?: number): [number, number][];
|
|
166
|
+
/**
|
|
167
|
+
* Idle gaps as [start, end] seconds: no event of ANY kind for ≥ idleMin,
|
|
168
|
+
* head and tail included — the digest's `idle` moments and the speed
|
|
169
|
+
* planner's 4× candidates come from this one derivation.
|
|
170
|
+
*/
|
|
171
|
+
declare function idleGaps(track: readonly CursorEvent[], durationS: number, idleMin?: number): [number, number][];
|
|
172
|
+
/** Mean activity over [a, b) source seconds exceeds PLAYBACK_ACTIVITY. */
|
|
173
|
+
declare function isPlayback(activity: readonly number[] | null | undefined, a: number, b: number): boolean;
|
|
174
|
+
|
|
175
|
+
/** A single input event captured in the page, relative to the recording's t0. */
|
|
176
|
+
interface CursorEvent {
|
|
177
|
+
/** ms since t0 (recording start). */
|
|
178
|
+
t: number;
|
|
179
|
+
/** viewport CSS px. */
|
|
180
|
+
x: number;
|
|
181
|
+
y: number;
|
|
182
|
+
/**
|
|
183
|
+
* Screen-coordinate CSS px (MouseEvent.screenX/Y) — the mapping anchor for
|
|
184
|
+
* window/monitor captures, where the viewport is only part of the frame.
|
|
185
|
+
* Carrying both spaces per event also gives an exact per-event viewport→screen
|
|
186
|
+
* offset (sx−x, sy−y) for transforming element rects. Absent on old tracks.
|
|
187
|
+
*/
|
|
188
|
+
sx?: number;
|
|
189
|
+
sy?: number;
|
|
190
|
+
/**
|
|
191
|
+
* `key` is a typing-ACTIVITY ping (throttled): when and where typing is
|
|
192
|
+
* happening, never what is typed — the payload must not carry key identity
|
|
193
|
+
* or input contents at any layer. Position/rect follow the `focus`
|
|
194
|
+
* convention: the focused editable element's center + bounds.
|
|
195
|
+
*/
|
|
196
|
+
type: 'move' | 'down' | 'up' | 'scroll' | 'focus' | 'key';
|
|
197
|
+
/** pointer button for down/up (0=left). */
|
|
198
|
+
button?: 0 | 1 | 2;
|
|
199
|
+
/** target element bounds at event time — enables element-aware auto-zoom. */
|
|
200
|
+
rect?: Rect;
|
|
201
|
+
}
|
|
202
|
+
type CursorTrack = CursorEvent[];
|
|
203
|
+
interface Rect {
|
|
204
|
+
x: number;
|
|
205
|
+
y: number;
|
|
206
|
+
w: number;
|
|
207
|
+
h: number;
|
|
208
|
+
}
|
|
209
|
+
/** Recording metadata needed to map captured pixels ↔ cursor coords ↔ time. */
|
|
210
|
+
interface RecordingMeta {
|
|
211
|
+
/** device pixel ratio at capture. */
|
|
212
|
+
dpr: number;
|
|
213
|
+
/** page/browser zoom at capture. */
|
|
214
|
+
zoom: number;
|
|
215
|
+
/** wall-clock origin (Date.now at first frame). */
|
|
216
|
+
t0: number;
|
|
217
|
+
durationMs: number;
|
|
218
|
+
/** captured pixel dimensions. */
|
|
219
|
+
width: number;
|
|
220
|
+
height: number;
|
|
221
|
+
fps: number;
|
|
222
|
+
/**
|
|
223
|
+
* The recording's OWN file carries an audio track — unmute preview, mux into
|
|
224
|
+
* export. After the AT split that track is SYSTEM/tab audio only (it rides
|
|
225
|
+
* the same stream as the video, sample-aligned by construction); on takes
|
|
226
|
+
* recorded before the split it is the legacy record-time mic+system mix.
|
|
227
|
+
*/
|
|
228
|
+
hasAudio?: boolean;
|
|
229
|
+
/**
|
|
230
|
+
* A separately-recorded microphone sidecar exists. The mic never
|
|
231
|
+
* enters the video file — it records through its own audio-only
|
|
232
|
+
* MediaRecorder so the studio can gain/mute/duck it independently.
|
|
233
|
+
*/
|
|
234
|
+
hasMic?: boolean;
|
|
235
|
+
/**
|
|
236
|
+
* Recorder start skew: wall-clock ms between the main recorder's start and
|
|
237
|
+
* the mic/cam sidecar recorders' starts (positive = sidecar started later).
|
|
238
|
+
* Lets the consume path trim/pad a sidecar head instead of assuming t0
|
|
239
|
+
* equality. Absent on takes without the matching sidecar.
|
|
240
|
+
*/
|
|
241
|
+
micT0DeltaMs?: number;
|
|
242
|
+
camT0DeltaMs?: number;
|
|
243
|
+
/**
|
|
244
|
+
* Encoded frame dimensions in device px, from the capture track's settings.
|
|
245
|
+
* `width`/`height` are the CSS-px viewport (the CursorEvent coordinate space);
|
|
246
|
+
* these are the actual video pixels — same aspect when capture is constrained
|
|
247
|
+
* to the tab size, but keep both spaces so mapping never assumes it.
|
|
248
|
+
*/
|
|
249
|
+
captureWidth?: number;
|
|
250
|
+
captureHeight?: number;
|
|
251
|
+
/**
|
|
252
|
+
* The tab viewport changed size mid-take. Capture resolution is fixed for the
|
|
253
|
+
* whole take, so Chrome letterboxes the resized content — surface a notice.
|
|
254
|
+
*/
|
|
255
|
+
resizedDuringTake?: boolean;
|
|
256
|
+
/**
|
|
257
|
+
* Page URL/title at record start (seeds the browser-bar mock's address pill).
|
|
258
|
+
* Query/hash are stripped at capture for privacy.
|
|
259
|
+
*/
|
|
260
|
+
pageUrl?: string;
|
|
261
|
+
pageTitle?: string;
|
|
262
|
+
/**
|
|
263
|
+
* What the frame contains. Absent = 'tab' (back-compat). Non-tab surfaces use
|
|
264
|
+
* CursorEvent.sx/sy + the geometry rects below to map cursor → capture px
|
|
265
|
+
* (see normalizeCaptureSpace); tab-only studio features (browser-bar mock,
|
|
266
|
+
* letterbox notice) are gated off for them.
|
|
267
|
+
*/
|
|
268
|
+
captureSurface?: 'tab' | 'window' | 'monitor';
|
|
269
|
+
/**
|
|
270
|
+
* Target-tab browser-window bounds at record start, screen-coord CSS px
|
|
271
|
+
* (window.screenX/Y + outerWidth/Height). Anchor for 'window' captures.
|
|
272
|
+
*/
|
|
273
|
+
windowRect?: Rect;
|
|
274
|
+
/**
|
|
275
|
+
* FULL bounds of the display hosting the target window at record start,
|
|
276
|
+
* screen-coord CSS px (chrome.system.display bounds — the true origin,
|
|
277
|
+
* including the macOS menu bar; page availLeft/Top only as a fallback).
|
|
278
|
+
* Anchor for 'monitor' captures; wrong-display shares surface as low
|
|
279
|
+
* coverage and fall back to no auto-zoom.
|
|
280
|
+
*/
|
|
281
|
+
screenRect?: Rect;
|
|
282
|
+
/**
|
|
283
|
+
* The target window moved or resized during a 'window' take — the single
|
|
284
|
+
* windowRect anchor can't map the whole track, so the studio drops the
|
|
285
|
+
* cursor rather than rendering it at stale positions.
|
|
286
|
+
*/
|
|
287
|
+
windowMovedDuringTake?: boolean;
|
|
288
|
+
/**
|
|
289
|
+
* Viewport CSS-px size (innerWidth/Height) of the recorded tab at record
|
|
290
|
+
* start. On 'window' takes this + windowRect + the cursor events' screen
|
|
291
|
+
* coords derive the viewport crop that removes the real browser chrome from
|
|
292
|
+
* the footage (deriveViewportCrop) so the synthetic browser bar applies.
|
|
293
|
+
*/
|
|
294
|
+
viewport?: {
|
|
295
|
+
w: number;
|
|
296
|
+
h: number;
|
|
297
|
+
};
|
|
298
|
+
/**
|
|
299
|
+
* The tab viewport changed size mid-take on a display take (resize, devtools
|
|
300
|
+
* dock, zoom) — the static viewport crop can't map the whole take, so crop
|
|
301
|
+
* derivation fails closed.
|
|
302
|
+
*/
|
|
303
|
+
viewportChangedDuringTake?: boolean;
|
|
304
|
+
/**
|
|
305
|
+
* Fraction of a 'window' take during which the target tab's browser window
|
|
306
|
+
* was the FOCUSED window (chrome.windows.onFocusChanged, pause-gated).
|
|
307
|
+
* The wrong-window tell that geometry can't provide: cursor events come from
|
|
308
|
+
* the recorded tab and its window geometry is self-consistent, so sharing a
|
|
309
|
+
* DIFFERENT window (Finder, another app — even one with identical bounds)
|
|
310
|
+
* still maps events "in frame". But driving that other window means focusing
|
|
311
|
+
* it — a low fraction ⇒ the footage isn't the browser window, so cursor
|
|
312
|
+
* effects and the viewport crop must fail closed (WINDOW_FOCUS_MIN).
|
|
313
|
+
*/
|
|
314
|
+
windowFocusedFrac?: number;
|
|
315
|
+
/** Recorder OS (chrome.runtime.getPlatformInfo) — seeds the browser-bar style. */
|
|
316
|
+
platform?: 'mac' | 'windows' | 'linux';
|
|
317
|
+
/**
|
|
318
|
+
* Which recorder produced the artifact. CLI takes synthesize the cursor
|
|
319
|
+
* track from automation (exact coords, fresh rects, coverage 1 by
|
|
320
|
+
* construction) and encode WebM; absent means the extension.
|
|
321
|
+
*/
|
|
322
|
+
producer?: 'extension' | 'cli';
|
|
323
|
+
/**
|
|
324
|
+
* The step timeline: when each actions.json step ran, in SOURCE
|
|
325
|
+
* seconds. This is what makes a cut re-anchorable across re-records — a
|
|
326
|
+
* span anchored to a step re-times to wherever that step landed in the
|
|
327
|
+
* new recording (`vos plan --reuse`). CLI takes only; a human recording
|
|
328
|
+
* has no script and carries none.
|
|
329
|
+
*/
|
|
330
|
+
steps?: StepSpan[];
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* A span's tie to an actions.json step: metadata for `vos plan
|
|
334
|
+
* --reuse`, which re-times the span onto a NEW recording of the same script
|
|
335
|
+
* by resolving the step in the new `meta.steps`. NEVER read by lowering —
|
|
336
|
+
* seconds stay the wire truth (`in`/`out` are always authoritative), so a
|
|
337
|
+
* human recording with no steps renders identically with or without one.
|
|
338
|
+
*/
|
|
339
|
+
interface StepAnchor {
|
|
340
|
+
/** The step: its `id` from actions.json when it has one, else its index. */
|
|
341
|
+
step: string | number;
|
|
342
|
+
/** Which edge of the step the span's `in` is measured from. Default 'start'. */
|
|
343
|
+
at?: 'start' | 'end';
|
|
344
|
+
/** Seconds from that edge to the span's `in` (negative = before it). */
|
|
345
|
+
offset?: number;
|
|
346
|
+
}
|
|
347
|
+
/** One executed actions.json step's extent in the recording. */
|
|
348
|
+
interface StepSpan {
|
|
349
|
+
/** index into actions.steps at record time. */
|
|
350
|
+
step: number;
|
|
351
|
+
/** the step's own id from actions.json, when it names one — an id lets a
|
|
352
|
+
* step move or be reordered without breaking anchors (absent = the index
|
|
353
|
+
* is the identity). */
|
|
354
|
+
id?: string;
|
|
355
|
+
do: string;
|
|
356
|
+
selector?: string;
|
|
357
|
+
/** SOURCE seconds the gesture occupied, [tStart, tEnd]. */
|
|
358
|
+
tStart: number;
|
|
359
|
+
tEnd: number;
|
|
360
|
+
/** the selector never became visible — the gesture did not run. */
|
|
361
|
+
skipped?: boolean;
|
|
362
|
+
}
|
|
363
|
+
/** Everything the capture extension hands off to the studio. */
|
|
364
|
+
interface RecordingArtifact {
|
|
365
|
+
/** OPFS key / object URL for the recorded video. */
|
|
366
|
+
videoKey: string;
|
|
367
|
+
cursor: CursorTrack;
|
|
368
|
+
/** object URL for the separately-recorded mic sidecar (the mic/system split). */
|
|
369
|
+
audioKey?: string;
|
|
370
|
+
/** object URL for a separately-recorded webcam track (drawn as an editable bubble). */
|
|
371
|
+
camKey?: string;
|
|
372
|
+
meta: RecordingMeta;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Per-span transition speed — how fast the camera/bubble/card moves
|
|
376
|
+
* into and out of a span's state, as NAMED steps (the category convention:
|
|
377
|
+
* Screen Studio's speed words, Descript's one knob — never a curve editor).
|
|
378
|
+
* Multipliers on the lane's own ramp constants, so 'smooth' (absent) is
|
|
379
|
+
* byte-identical to the pre-feature motion and each lane keeps its feel.
|
|
380
|
+
* 'instant' is a hard cut: the ramp collapses to the track emitter's 1ms
|
|
381
|
+
* collision nudge.
|
|
382
|
+
*/
|
|
383
|
+
type TransitionSpeed = 'instant' | 'fast' | 'smooth' | 'slow';
|
|
384
|
+
declare const TRANSITION_SPEED_MULT: Record<TransitionSpeed, number>;
|
|
385
|
+
/** A span's ramp multiplier (absent = 'smooth' = 1, the exact legacy motion). */
|
|
386
|
+
declare function transitionMult(t: TransitionSpeed | undefined): number;
|
|
387
|
+
/**
|
|
388
|
+
* A speed-change region over a SOURCE-time span (seconds). Footage-anchored
|
|
389
|
+
* like zoom keyframes — it follows its content through trims/splits, and a
|
|
390
|
+
* span whose footage is fully cut away simply has no effect (and comes back
|
|
391
|
+
* if the trim is undone). Non-overlapping (the lane clamps). The lowering
|
|
392
|
+
* intersects spans with `segments` via @vosjs/timeline `splitBySpeed` into
|
|
393
|
+
* rated segments; playback, export, and lane display all evaluate those.
|
|
394
|
+
*/
|
|
395
|
+
interface SpeedSpan {
|
|
396
|
+
/** Stable identity for selection/editing in the timeline UI. */
|
|
397
|
+
id: string;
|
|
398
|
+
in: number;
|
|
399
|
+
out: number;
|
|
400
|
+
/** Re-record tie to an actions.json step; `in`/`out` stay the truth. */
|
|
401
|
+
anchor?: StepAnchor;
|
|
402
|
+
/** Playback rate (> 0): 2 = twice as fast, 0.5 = half speed. */
|
|
403
|
+
rate: number;
|
|
404
|
+
/**
|
|
405
|
+
* The auto-zoom wand contract: 'auto' = planner suggestion
|
|
406
|
+
* (planAutoSpeed — typing/scroll/idle), replaced by a re-plan; 'manual' =
|
|
407
|
+
* user/agent work, always preserved. Absent = manual (spans predating the contract).
|
|
408
|
+
*/
|
|
409
|
+
source?: 'auto' | 'manual';
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Speed-rate bounds. 16 is also Chromium's HTMLMediaElement.playbackRate
|
|
413
|
+
* ceiling, so preview (native playback) and export (offline resample) can
|
|
414
|
+
* honor the same range.
|
|
415
|
+
*/
|
|
416
|
+
declare const SPEED_RATE_MIN = 0.1;
|
|
417
|
+
declare const SPEED_RATE_MAX = 16;
|
|
418
|
+
/**
|
|
419
|
+
* Minimum speed-span length in OUTPUT seconds (the lane converts through the
|
|
420
|
+
* span's own rate: a 2× span may not shrink below 0.5s of source). A source
|
|
421
|
+
* floor shrank with the rate — 0.1s of source at 5× was 20ms of screen, a
|
|
422
|
+
* sliver nobody could grab again.
|
|
423
|
+
*/
|
|
424
|
+
declare const SPEED_SPAN_MIN = 0.25;
|
|
425
|
+
/** Clamp + quantize a speed rate for storage (2 decimals, like "1.75×"). */
|
|
426
|
+
declare function clampSpeedRate(rate: number): number;
|
|
427
|
+
/**
|
|
428
|
+
* A zoom region over a SOURCE-time span (seconds) — one adjustable clip on the
|
|
429
|
+
* zoom lane. Footage-anchored like SpeedSpan/CamStyle.window: it follows its
|
|
430
|
+
* content through trims/splits (a span whose footage is fully cut away renders
|
|
431
|
+
* nothing, and comes back if the trim is undone; a partially-cut span keeps its
|
|
432
|
+
* kept extent). Non-overlapping (the lane clamps). The camera ramps in around
|
|
433
|
+
* `in`, holds `[level, cx, cy]` until `out`, then ramps back to 1× — or pans
|
|
434
|
+
* straight to the next span when the gap is short (see `zoomTrackFromDoc`).
|
|
435
|
+
*/
|
|
436
|
+
interface ZoomSpan {
|
|
437
|
+
/** Stable identity for selection/editing (`z{n}` planner, `u{n}` user). */
|
|
438
|
+
id: string;
|
|
439
|
+
in: number;
|
|
440
|
+
out: number;
|
|
441
|
+
/** Re-record tie to an actions.json step; `in`/`out` stay the truth. */
|
|
442
|
+
anchor?: StepAnchor;
|
|
443
|
+
/** zoom level (1 = no zoom), ZOOM_LEVEL_MIN..ZOOM_LEVEL_MAX, 2 decimals. */
|
|
444
|
+
level: number;
|
|
445
|
+
/** focus point in normalized [0..1] video-frame coords. */
|
|
446
|
+
cx: number;
|
|
447
|
+
cy: number;
|
|
448
|
+
/** arrival ease (@vosjs/timeline EASINGS name). Absent = the default ramp ease. */
|
|
449
|
+
ease?: string;
|
|
450
|
+
/**
|
|
451
|
+
* Transition speed for THIS span's ramps (in, out, and the pan arriving
|
|
452
|
+
* here from a chained neighbor). Absent = 'smooth', the camera style's
|
|
453
|
+
* stock motion; 'instant' is a hard cut.
|
|
454
|
+
*/
|
|
455
|
+
transition?: TransitionSpeed;
|
|
456
|
+
/**
|
|
457
|
+
* 'auto' = the camera follows the cursor through the span (dead-zone
|
|
458
|
+
* recenter, baked deterministically at lowering — see followFocusEvents);
|
|
459
|
+
* absent/'manual' = the fixed cx/cy focus.
|
|
460
|
+
*/
|
|
461
|
+
focusMode?: 'manual' | 'auto';
|
|
462
|
+
/**
|
|
463
|
+
* 'auto' = planner suggestion — regenerate replaces these freely, never
|
|
464
|
+
* 'manual' ones. Any edit gesture promotes the span to 'manual' (OpenScreen's
|
|
465
|
+
* contract: suggestions are disposable, user work is sacred).
|
|
466
|
+
*/
|
|
467
|
+
source?: 'auto' | 'manual';
|
|
468
|
+
}
|
|
469
|
+
/** Zoom-level preset chips (OpenScreen-style picker). */
|
|
470
|
+
declare const ZOOM_LEVELS: readonly [1.25, 1.5, 1.8, 2.2, 3.5, 5];
|
|
471
|
+
declare const ZOOM_LEVEL_MIN = 1;
|
|
472
|
+
declare const ZOOM_LEVEL_MAX = 5;
|
|
473
|
+
/** Default level for new/user-created zooms. */
|
|
474
|
+
declare const DEFAULT_ZOOM_LEVEL = 1.8;
|
|
475
|
+
/**
|
|
476
|
+
* Minimum zoom-span length in OUTPUT seconds (the lane clamps resizes,
|
|
477
|
+
* converting through the rate in force so the floor is what the eye sees).
|
|
478
|
+
*/
|
|
479
|
+
declare const ZOOM_SPAN_MIN = 0.3;
|
|
480
|
+
/** Clamp + quantize a zoom level for storage (2 decimals, like "1.8×"). */
|
|
481
|
+
declare function clampZoomLevel(level: number): number;
|
|
482
|
+
/**
|
|
483
|
+
* A tilt region over a SOURCE-time span (seconds) — one adjustable clip on the
|
|
484
|
+
* tilt lane. Footage-anchored
|
|
485
|
+
* like ZoomSpan/SpeedSpan: it follows its content through trims/splits and
|
|
486
|
+
* speed changes (a span whose footage is fully cut away renders nothing, and
|
|
487
|
+
* comes back if the trim is undone). Non-overlapping (the lane clamps). While
|
|
488
|
+
* active the card leans to this pose; between spans it returns to the RESTING
|
|
489
|
+
* FLAT rest pose (there is no static card tilt) — expanded at lowering into an
|
|
490
|
+
* OUTPUT-time [rx, ry] degree keyframe track (see tiltTrackFromDoc).
|
|
491
|
+
*/
|
|
492
|
+
interface TiltSpan {
|
|
493
|
+
/** Stable identity for selection/editing (`t{n}` planner, `u{n}` user). */
|
|
494
|
+
id: string;
|
|
495
|
+
in: number;
|
|
496
|
+
out: number;
|
|
497
|
+
/** Re-record tie to an actions.json step; `in`/`out` stay the truth. */
|
|
498
|
+
anchor?: StepAnchor;
|
|
499
|
+
/**
|
|
500
|
+
* Pose in DEGREES (the CardTilt convention): rx leans the card back/forward,
|
|
501
|
+
* ry swings it left/right. Gentle values read best (±5..18°).
|
|
502
|
+
*/
|
|
503
|
+
rx: number;
|
|
504
|
+
ry: number;
|
|
505
|
+
/** arrival ease (@vosjs/timeline EASINGS name). Absent = the house tilt ease. */
|
|
506
|
+
ease?: string;
|
|
507
|
+
/**
|
|
508
|
+
* Transition speed for this span's ramps. Absent = 'smooth' (the stock
|
|
509
|
+
* tilt motion); 'instant' snaps the card to the pose.
|
|
510
|
+
*/
|
|
511
|
+
transition?: TransitionSpeed;
|
|
512
|
+
/**
|
|
513
|
+
* 'auto' = Dynamic-tilt wand suggestion — regenerate replaces these freely,
|
|
514
|
+
* never 'manual' ones. Any edit gesture promotes the span to 'manual' (the
|
|
515
|
+
* auto-zoom wand contract).
|
|
516
|
+
*/
|
|
517
|
+
source?: 'auto' | 'manual';
|
|
518
|
+
}
|
|
519
|
+
/** Hard tilt bound in degrees (schema/lint); UI sliders stay within ±20. */
|
|
520
|
+
declare const TILT_DEG_MAX = 45;
|
|
521
|
+
/** UI slider bound — matches the Card panel's static (rest) tilt sliders. */
|
|
522
|
+
declare const TILT_UI_DEG_MAX = 20;
|
|
523
|
+
/**
|
|
524
|
+
* Minimum tilt-span length in OUTPUT seconds (the lane clamps resizes). Bigger
|
|
525
|
+
* than ZOOM_SPAN_MIN because tilt ramps are longer — a pose that can't settle
|
|
526
|
+
* isn't a pose.
|
|
527
|
+
*/
|
|
528
|
+
declare const TILT_SPAN_MIN = 0.8;
|
|
529
|
+
/** Default pose for user-created spans: a medium three-quarter "showcase" lean. */
|
|
530
|
+
declare const DEFAULT_TILT_POSE: {
|
|
531
|
+
rx: number;
|
|
532
|
+
ry: number;
|
|
533
|
+
};
|
|
534
|
+
/** Clamp + quantize a tilt angle for storage (1 decimal, degrees). */
|
|
535
|
+
declare function clampTiltDeg(deg: number): number;
|
|
536
|
+
/**
|
|
537
|
+
* Dynamic-tilt wand intensity ladder (the category convention — FocuSee ships
|
|
538
|
+
* Subtle/Default/Strong): the max degrees planAutoTilt will lean per axis.
|
|
539
|
+
*/
|
|
540
|
+
type TiltStyleName = 'off' | 'subtle' | 'medium' | 'strong';
|
|
541
|
+
declare const TILT_INTENSITY_MAX: Record<Exclude<TiltStyleName, 'off'>, number>;
|
|
542
|
+
interface CursorStyle {
|
|
543
|
+
/**
|
|
544
|
+
* Draw the cursor dot. Off still keeps the track: auto-zoom cursor-follow
|
|
545
|
+
* and click effects are independent of whether the dot is painted. Absent
|
|
546
|
+
* reads as visible (pre-toggle docs).
|
|
547
|
+
*/
|
|
548
|
+
visible: boolean;
|
|
549
|
+
/** 0..1 smoothing strength (lerp factor; higher = smoother/laggier). */
|
|
550
|
+
smoothing: number;
|
|
551
|
+
/** rendered cursor size in px. */
|
|
552
|
+
size: number;
|
|
553
|
+
style: 'default' | 'dot' | 'ring';
|
|
554
|
+
hideWhenIdle: boolean;
|
|
555
|
+
clickFx: ClickFxStyle;
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Click-effect styling. Clicks are extracted
|
|
559
|
+
* at lowering (OUTPUT-anchored — see extractClicks) and drawn by ON_FRAME as a
|
|
560
|
+
* pure function of t; every field here is a live SET_DATA edit.
|
|
561
|
+
*/
|
|
562
|
+
interface ClickFxStyle {
|
|
563
|
+
/** ring drawn at the click point ('highlight' glows the clicked element's rect). */
|
|
564
|
+
style: 'none' | 'ripple' | 'pulse' | 'highlight';
|
|
565
|
+
/** cursor press dip on real down→up spans — independent of ring style. */
|
|
566
|
+
press: boolean;
|
|
567
|
+
intensity: 'subtle' | 'medium' | 'strong';
|
|
568
|
+
/** resolved hex for rings/glow; 'auto' = neutral white-over-dark-rim. */
|
|
569
|
+
color: string | 'auto';
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Named intensity levels → resolved multipliers (size/alpha `k`, duration
|
|
573
|
+
* `dur`), baked into ctx.data at lowering so ON_FRAME needs no registry —
|
|
574
|
+
* the same pattern as MINIMAL_BAR_THEMES resolving to concrete colors.
|
|
575
|
+
*/
|
|
576
|
+
declare const CLICK_FX_INTENSITY: Record<ClickFxStyle['intensity'], {
|
|
577
|
+
k: number;
|
|
578
|
+
dur: number;
|
|
579
|
+
}>;
|
|
580
|
+
/** Webcam bubble overlay — an editable layer composited over the frame. */
|
|
581
|
+
interface CamStyle {
|
|
582
|
+
visible: boolean;
|
|
583
|
+
/**
|
|
584
|
+
* Free placement: the bubble CENTER as frame fractions (the overlay
|
|
585
|
+
* and zoom cx/cy convention, so positions survive aspect switches). When
|
|
586
|
+
* present they WIN over `position`; clearing them snaps back to the corner.
|
|
587
|
+
*/
|
|
588
|
+
x?: number;
|
|
589
|
+
y?: number;
|
|
590
|
+
/** corner the bubble is anchored to when x/y are absent. */
|
|
591
|
+
position: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
|
|
592
|
+
/** bubble diameter as a fraction of frame height (0..1). */
|
|
593
|
+
size: number;
|
|
594
|
+
shape: 'circle' | 'rounded';
|
|
595
|
+
/**
|
|
596
|
+
* Corner radius in design px for the 'rounded' shape (absent = 18, the
|
|
597
|
+
* house look; a circle ignores it). Scales with the canvas like every
|
|
598
|
+
* frame-owned control.
|
|
599
|
+
*/
|
|
600
|
+
radius?: number;
|
|
601
|
+
/**
|
|
602
|
+
* Ring stroke over the bubble edge. Absent = the house ring (3px white at
|
|
603
|
+
* 0.9 alpha — the pre-existing paint); `width: 0` = no ring.
|
|
604
|
+
*/
|
|
605
|
+
border?: {
|
|
606
|
+
width: number;
|
|
607
|
+
color: string;
|
|
608
|
+
};
|
|
609
|
+
/** Bubble shadow. Absent = 'soft', the pre-existing paint. */
|
|
610
|
+
shadow?: 'none' | 'soft' | 'strong';
|
|
611
|
+
/** mirror horizontally (selfie view). */
|
|
612
|
+
mirror: boolean;
|
|
613
|
+
/**
|
|
614
|
+
* Show the bubble only during this SOURCE-time span (trimmed on the cam
|
|
615
|
+
* timeline lane; anchored to footage like zoom keyframes). Absent = always.
|
|
616
|
+
*/
|
|
617
|
+
window?: Segment;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* A cam pose region over a SOURCE-time span (seconds) — one adjustable clip on
|
|
621
|
+
* the cam-move lane (MO track: animated cam layouts, the Screen Studio
|
|
622
|
+
* signature). Footage-anchored like ZoomSpan/TiltSpan: it follows its content
|
|
623
|
+
* through trims/splits and speed changes (a span whose footage is fully cut
|
|
624
|
+
* away renders nothing, and comes back if the trim is undone). Non-overlapping
|
|
625
|
+
* (the lane clamps). While active the bubble holds this pose; outside spans it
|
|
626
|
+
* rests at the doc's cam style (doc.cam IS the rest pose); spans close together
|
|
627
|
+
* in output time morph pose-to-pose without returning to rest. Absent pose
|
|
628
|
+
* fields inherit the rest pose, so a span may move without resizing. Expanded
|
|
629
|
+
* at lowering into an OUTPUT-time [x, y, size] fraction keyframe track
|
|
630
|
+
* (camTrackFromDoc) — pure f(t), no springs.
|
|
631
|
+
*/
|
|
632
|
+
interface CamPoseSpan {
|
|
633
|
+
/** Stable identity for selection/editing (`m{n}` user-created). */
|
|
634
|
+
id: string;
|
|
635
|
+
in: number;
|
|
636
|
+
out: number;
|
|
637
|
+
/**
|
|
638
|
+
* Bubble CENTER as frame fractions [0..1] (the cam.x/y and zoom cx/cy
|
|
639
|
+
* convention — aspect-stable). Absent = the rest pose's center.
|
|
640
|
+
*/
|
|
641
|
+
x?: number;
|
|
642
|
+
y?: number;
|
|
643
|
+
/** Bubble diameter as a fraction of frame height. Absent = the rest size. */
|
|
644
|
+
size?: number;
|
|
645
|
+
/** arrival ease (@vosjs/timeline EASINGS name). Absent = the house cam ease. */
|
|
646
|
+
ease?: string;
|
|
647
|
+
/**
|
|
648
|
+
* Transition speed for this span's morphs. Absent = 'smooth' (~0.65s);
|
|
649
|
+
* 'instant' jump-cuts the bubble to its pose — the Screen Studio layout-cut.
|
|
650
|
+
*/
|
|
651
|
+
transition?: TransitionSpeed;
|
|
652
|
+
/**
|
|
653
|
+
* Reserved for a future auto planner (the wand contract: 'auto' spans are
|
|
654
|
+
* disposable suggestions). Every studio gesture writes 'manual'.
|
|
655
|
+
*/
|
|
656
|
+
source?: 'auto' | 'manual';
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Minimum cam-move span length in OUTPUT seconds (the lane clamps resizes).
|
|
660
|
+
* Between zoom's 0.3 and tilt's 0.8: a bubble move settles faster than a card
|
|
661
|
+
* pose but still needs its ~0.65s ramp to read as a move, not a jump.
|
|
662
|
+
*/
|
|
663
|
+
declare const CAM_SPAN_MIN = 0.5;
|
|
664
|
+
/** Bubble-size band for pose spans (fractions of frame height; UI + lint). */
|
|
665
|
+
declare const CAM_SIZE_MIN = 0.08;
|
|
666
|
+
declare const CAM_SIZE_MAX = 0.6;
|
|
667
|
+
/** Clamp + quantize a pose size for storage (3 decimals, frame fraction). */
|
|
668
|
+
declare function clampCamSize(size: number): number;
|
|
669
|
+
/** Clamp + quantize a pose center coordinate for storage (3 decimals, [0..1]). */
|
|
670
|
+
declare function clampCamFrac(v: number): number;
|
|
671
|
+
/**
|
|
672
|
+
* Default pose for user-created cam-move spans: front-and-center, large —
|
|
673
|
+
* the "talk to camera" moment that is the feature's reason to exist (the
|
|
674
|
+
* DEFAULT_TILT_POSE philosophy: a new span shows a visible, editable move,
|
|
675
|
+
* never a no-op).
|
|
676
|
+
*/
|
|
677
|
+
declare const DEFAULT_CAM_POSE: {
|
|
678
|
+
x: number;
|
|
679
|
+
y: number;
|
|
680
|
+
size: number;
|
|
681
|
+
};
|
|
682
|
+
/**
|
|
683
|
+
* Mock browser chrome drawn as a strip above the video inside the frame card.
|
|
684
|
+
* Drawn by the compositor (never captured) — the editor-frame pattern every
|
|
685
|
+
* shot/recorder tool uses. All values travel in `ctx.data.frame` (live T2 edits).
|
|
686
|
+
*/
|
|
687
|
+
interface BrowserBarStyle {
|
|
688
|
+
kind: 'none' | 'mac-light' | 'mac-dark' | 'windows-light' | 'windows-dark' | 'minimal';
|
|
689
|
+
/** address-pill text (editable; seeded from the recorded page's URL). */
|
|
690
|
+
url: string;
|
|
691
|
+
showUrl: boolean;
|
|
692
|
+
/** traffic lights (mac) / window buttons (windows). */
|
|
693
|
+
showControls: boolean;
|
|
694
|
+
/** bar height in design px (1080-based, same space as padding/radius). */
|
|
695
|
+
height: number;
|
|
696
|
+
/**
|
|
697
|
+
* Minimal-bar color theme — RESOLVED colors (not a palette id) so ON_FRAME
|
|
698
|
+
* needs no registry lookup (interpreter rule: everything in ctx.data is
|
|
699
|
+
* self-contained). Absent = the built-in graphite look. Pick from
|
|
700
|
+
* MINIMAL_BAR_THEMES in the inspector.
|
|
701
|
+
*/
|
|
702
|
+
theme?: MinimalBarTheme;
|
|
703
|
+
}
|
|
704
|
+
/** Resolved minimal-bar colors. `light` flips the hairline to dark-on-light. */
|
|
705
|
+
interface MinimalBarTheme {
|
|
706
|
+
id: string;
|
|
707
|
+
bar: string;
|
|
708
|
+
pill: string;
|
|
709
|
+
text: string;
|
|
710
|
+
light?: boolean;
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Curated minimal-bar palette (Cursorful-style 12 swatches: 6 dark, 6 light).
|
|
714
|
+
* The first entry matches the built-in default (theme absent).
|
|
715
|
+
*/
|
|
716
|
+
declare const MINIMAL_BAR_THEMES: MinimalBarTheme[];
|
|
717
|
+
/**
|
|
718
|
+
* A music/SFX clip. OUTPUT-anchored (`start` is final-cut seconds): music and
|
|
719
|
+
* effects are authored against the cut that remains — unlike zoom keyframes /
|
|
720
|
+
* cam window, they do NOT follow footage through trims. The mic track stays
|
|
721
|
+
* source-anchored via the export's segment splice.
|
|
722
|
+
*/
|
|
723
|
+
interface AudioClip {
|
|
724
|
+
id: string;
|
|
725
|
+
/** blob URL (or asset URL) of the audio file. */
|
|
726
|
+
key: string;
|
|
727
|
+
/** display name (file name or library track title). */
|
|
728
|
+
name: string;
|
|
729
|
+
/** placement on the OUTPUT timeline, seconds. */
|
|
730
|
+
start: number;
|
|
731
|
+
/** kept span within the source file, seconds (trim). */
|
|
732
|
+
in: number;
|
|
733
|
+
out: number;
|
|
734
|
+
/** full source-file length, seconds — the trim ceiling (set on add). */
|
|
735
|
+
duration: number;
|
|
736
|
+
/** linear gain 0..1. */
|
|
737
|
+
gain: number;
|
|
738
|
+
/** fade durations, seconds. */
|
|
739
|
+
fadeIn: number;
|
|
740
|
+
fadeOut: number;
|
|
741
|
+
/** loop the [in,out) span to fill `loopLen` output seconds. */
|
|
742
|
+
loop?: boolean;
|
|
743
|
+
/** placed output length when looping (≥ span; defaults to the span). */
|
|
744
|
+
loopLen?: number;
|
|
745
|
+
/** duck this clip under the mic while speech is detected. */
|
|
746
|
+
duck?: boolean;
|
|
747
|
+
}
|
|
748
|
+
/** Effective placed length of a clip on the output timeline, seconds. */
|
|
749
|
+
declare function clipLength(clip: Pick<AudioClip, 'in' | 'out' | 'loop' | 'loopLen'>): number;
|
|
750
|
+
/**
|
|
751
|
+
* Media layer drawn over the CSS `background` and under the card
|
|
752
|
+
* The flagship option is a vos rendered
|
|
753
|
+
* to a seamless loop (`vosId` provenance kept for re-bakes + a future live
|
|
754
|
+
* tier). Video time is OUTPUT-anchored modulo the loop (`bgT = t % duration`)
|
|
755
|
+
* — trims/speed never retime ambience. Fail-open: while the media loads (or if
|
|
756
|
+
* it can't), the CSS background underneath still paints — never a black frame.
|
|
757
|
+
*/
|
|
758
|
+
interface BackgroundMedia {
|
|
759
|
+
kind: 'video' | 'image';
|
|
760
|
+
/**
|
|
761
|
+
* Source URL — blob URL (session), /api/assets/{id}/file (saved vos),
|
|
762
|
+
* https://assets.vos.so/... (pre-baked official), or take-dir relative path
|
|
763
|
+
* (CLI). Rides the same resolution plumbing as source.videoKey.
|
|
764
|
+
*/
|
|
765
|
+
key: string;
|
|
766
|
+
/** Loop length in seconds (video; the bake duration). */
|
|
767
|
+
duration?: number;
|
|
768
|
+
/** Provenance: the vos this media was rendered from. */
|
|
769
|
+
vosId?: string;
|
|
770
|
+
versionId?: string;
|
|
771
|
+
/** Poster/thumbnail URL (picker display + reduced-motion; not drawn by the layer). */
|
|
772
|
+
poster?: string;
|
|
773
|
+
/** Black scrim over the media, 0..1 — the one legibility dial. */
|
|
774
|
+
dim: number;
|
|
775
|
+
/** Blur radius in design px — softens the media behind the card. */
|
|
776
|
+
blur?: number;
|
|
777
|
+
}
|
|
778
|
+
interface FrameStyle {
|
|
779
|
+
/** CSS background (gradient/color): always painted — the media underlay/fallback. */
|
|
780
|
+
background: string;
|
|
781
|
+
/** Optional media layer (vos loop / image) drawn over the CSS background. */
|
|
782
|
+
backgroundMedia?: BackgroundMedia | null;
|
|
783
|
+
padding: number;
|
|
784
|
+
radius: number;
|
|
785
|
+
/** shadow strength 0..1. */
|
|
786
|
+
shadow: number;
|
|
787
|
+
/** stroke around the card, 0..1 alpha (0 = off). The switch AND the opacity. */
|
|
788
|
+
border: number;
|
|
789
|
+
/**
|
|
790
|
+
* Stroke width in design px (scales with the canvas, like radius), drawn
|
|
791
|
+
* OUTWARD from the card's edge (a CSS outline) so it never covers footage.
|
|
792
|
+
* Absent = FRAME_BORDER_WIDTH_DEFAULT, the hairline every take shipped with.
|
|
793
|
+
*/
|
|
794
|
+
borderWidth?: number;
|
|
795
|
+
/**
|
|
796
|
+
* Stroke colour, any CSS colour string. Absent = FRAME_BORDER_COLOR_DEFAULT.
|
|
797
|
+
* `border` is the alpha it is drawn at, so an opaque colour is correct here.
|
|
798
|
+
*/
|
|
799
|
+
borderColor?: string;
|
|
800
|
+
/**
|
|
801
|
+
* How footage meets an off-ratio frame. 'contain' (default) fits the
|
|
802
|
+
* whole card inside the padded area, letterboxing onto the background;
|
|
803
|
+
* 'cover' makes the padded area the card and cover-fills it with footage,
|
|
804
|
+
* cropped around `focus` — what a 440x280 store tile or a 2.5:1 marquee
|
|
805
|
+
* demands ("fill the region"). Absent = contain; every existing doc is
|
|
806
|
+
* byte-identical.
|
|
807
|
+
*/
|
|
808
|
+
fit?: 'contain' | 'cover';
|
|
809
|
+
/**
|
|
810
|
+
* Cover-crop anchor, normalized video-frame fractions (the zoom cx/cy
|
|
811
|
+
* convention): which point of the footage stays visible when `fit:'cover'`
|
|
812
|
+
* crops. Absent = center. Ignored under contain.
|
|
813
|
+
*/
|
|
814
|
+
focus?: {
|
|
815
|
+
cx: number;
|
|
816
|
+
cy: number;
|
|
817
|
+
};
|
|
818
|
+
aspectRatio: string;
|
|
819
|
+
browserBar: BrowserBarStyle;
|
|
820
|
+
/**
|
|
821
|
+
* Background parallax 0..1: the background media counter-pans subtly
|
|
822
|
+
* as the zoom camera moves (depth cue). 0/absent = static.
|
|
823
|
+
*/
|
|
824
|
+
parallax?: number;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Overlay clips (compositor v2: "elements-shaped data"). Screen-
|
|
828
|
+
* space clips drawn on the OVERLAY layer (above the card, never tilts, outside
|
|
829
|
+
* the zoom transform). **OUTPUT-anchored** — trims/speed never retime a title.
|
|
830
|
+
* The first slice ships `kind: 'text'`; image/video kinds are the next slice and extend this
|
|
831
|
+
* union without changing the anchoring or transform model.
|
|
832
|
+
*/
|
|
833
|
+
type OverlayKind = 'text' | 'image' | 'video';
|
|
834
|
+
/** Named house text styles — resolved to concrete font/size/color at lowering. */
|
|
835
|
+
type TextOverlayPreset = 'title' | 'caption' | 'label';
|
|
836
|
+
/** Enter/exit transition presets — pure f(t), evaluated in ON_FRAME. */
|
|
837
|
+
type OverlayTransition = 'none' | 'fade' | 'rise';
|
|
838
|
+
/** Text animation vocabulary — entrance presets evaluated per unit. */
|
|
839
|
+
type TextFxKind = 'fade' | 'rise' | 'pop' | 'blur' | 'typewriter';
|
|
840
|
+
type TextFxUnit = 'block' | 'line' | 'word' | 'char';
|
|
841
|
+
type TextFxDirection = 'forward' | 'reverse' | 'center';
|
|
842
|
+
/**
|
|
843
|
+
* Text entrance animation. When present it OWNS the entrance — the
|
|
844
|
+
* clip's `enter` string is ignored (a spec with `unit: 'block'` is the
|
|
845
|
+
* superset of the legacy presets); `exit` stays clip-level. Segmentation is
|
|
846
|
+
* baked at lowering (deterministic doc-derived data), per-unit progress is
|
|
847
|
+
* evaluated in ON_FRAME — pure f(t), so scrub/seek/chunk cold-seeks agree.
|
|
848
|
+
*/
|
|
849
|
+
interface TextFxSpec {
|
|
850
|
+
fx: TextFxKind;
|
|
851
|
+
/** What animates as one thing (default 'block' — the whole text). */
|
|
852
|
+
unit?: TextFxUnit;
|
|
853
|
+
/** Unit start order (default 'forward'; 'center' ripples outward). */
|
|
854
|
+
direction?: TextFxDirection;
|
|
855
|
+
/**
|
|
856
|
+
* Seconds between unit starts. Defaults: typewriter 0.05, other kinds
|
|
857
|
+
* 0.06 when unit ≠ block, else 0. Clamped at lowering so the whole
|
|
858
|
+
* entrance fits the clip.
|
|
859
|
+
*/
|
|
860
|
+
stagger?: number;
|
|
861
|
+
/** Per-unit seconds (default OVERLAY_TRANSITION_DUR); typewriter ignores it. */
|
|
862
|
+
duration?: number;
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* A pose keyframe on an overlay/object clip (element motion).
|
|
866
|
+
* `at` is CLIP-LOCAL OUTPUT seconds (0 = the clip's start), so poses ride
|
|
867
|
+
* along when the clip moves. Values interpolate across the gap between poses
|
|
868
|
+
* (ease-into per pose, the KeyframeTrack convention); a hold is two identical
|
|
869
|
+
* poses. The clip's base transform is the value before the first pose, and
|
|
870
|
+
* absent fields inherit it — a pose may move without resizing. Baked at
|
|
871
|
+
* lowering into a clip-local keyframe track, sampled in ON_FRAME as pure
|
|
872
|
+
* f(t): scrub, export and chunked server renders agree by construction.
|
|
873
|
+
*/
|
|
874
|
+
interface MotionPose {
|
|
875
|
+
/** Clip-local OUTPUT seconds. */
|
|
876
|
+
at: number;
|
|
877
|
+
/** Anchor center as frame fractions (the transform.x/y convention). */
|
|
878
|
+
x?: number;
|
|
879
|
+
y?: number;
|
|
880
|
+
/** Scale multiplier (the transform.scale convention). */
|
|
881
|
+
scale?: number;
|
|
882
|
+
/** Degrees (the transform.rotation convention). */
|
|
883
|
+
rotation?: number;
|
|
884
|
+
/** Opacity MULTIPLIER 0..1 on the clip's own alpha (default 1). */
|
|
885
|
+
opacity?: number;
|
|
886
|
+
/** Arrival ease (@vosjs/timeline EASINGS name). Absent = the house motion ease. */
|
|
887
|
+
ease?: string;
|
|
888
|
+
}
|
|
889
|
+
/** Default pose-to-pose ease: a symmetric in-out (continuous motion between
|
|
890
|
+
* poses, not a settle — the CapCut/keyframe convention). */
|
|
891
|
+
declare const MOTION_EASE = "power2.inOut";
|
|
892
|
+
interface OverlayTransform {
|
|
893
|
+
/**
|
|
894
|
+
* Anchor CENTER as FRACTIONS of the output frame [0..1] (the zoom cx/cy
|
|
895
|
+
* convention): 0.5/0.5 = frame center at ANY aspect ratio — positions
|
|
896
|
+
* survive aspect switches (design px did not: the space's width changes
|
|
897
|
+
* with the aspect, pushing clips off-frame).
|
|
898
|
+
*/
|
|
899
|
+
x: number;
|
|
900
|
+
y: number;
|
|
901
|
+
/** Uniform scale multiplier on the preset size. */
|
|
902
|
+
scale: number;
|
|
903
|
+
/** Rotation in degrees (screen-space, about the anchor). */
|
|
904
|
+
rotation: number;
|
|
905
|
+
}
|
|
906
|
+
interface OverlayClipBase {
|
|
907
|
+
/** Stable identity for selection/editing in the timeline UI. */
|
|
908
|
+
id: string;
|
|
909
|
+
/** OUTPUT-time span (seconds). */
|
|
910
|
+
start: number;
|
|
911
|
+
duration: number;
|
|
912
|
+
transform: OverlayTransform;
|
|
913
|
+
/** Absent = 'rise' for enter, 'fade' for exit (the house default motion). */
|
|
914
|
+
enter?: OverlayTransition;
|
|
915
|
+
exit?: OverlayTransition;
|
|
916
|
+
/**
|
|
917
|
+
* Pose keyframes (see MotionPose): the clip's transform animated
|
|
918
|
+
* over clip-local time, rendered as diamonds on the clip. Optional: absent
|
|
919
|
+
* lowers byte-identically (no track in data).
|
|
920
|
+
*/
|
|
921
|
+
motion?: MotionPose[];
|
|
922
|
+
}
|
|
923
|
+
interface TextOverlayClip extends OverlayClipBase {
|
|
924
|
+
kind: 'text';
|
|
925
|
+
/** Text content; '\n' breaks lines. */
|
|
926
|
+
text: string;
|
|
927
|
+
preset: TextOverlayPreset;
|
|
928
|
+
/** Font size override in design px (preset default when absent). */
|
|
929
|
+
size?: number;
|
|
930
|
+
/** CSS color override (preset default when absent). */
|
|
931
|
+
color?: string;
|
|
932
|
+
/**
|
|
933
|
+
* Font family override — a catalog family name (GET /api/fonts). Unknown
|
|
934
|
+
* names fail open: used verbatim with the preset stack as fallback.
|
|
935
|
+
*/
|
|
936
|
+
family?: string;
|
|
937
|
+
/** Weight override — snapped to the nearest weight the catalog hosts. */
|
|
938
|
+
weight?: number;
|
|
939
|
+
/** Synthesized oblique (no italic files are hosted). */
|
|
940
|
+
italic?: boolean;
|
|
941
|
+
/** Multi-line alignment within the block (default center). */
|
|
942
|
+
align?: 'left' | 'center' | 'right';
|
|
943
|
+
/** Letter spacing in design px at the resolved size (default 0). */
|
|
944
|
+
letterSpacing?: number;
|
|
945
|
+
/** Line height multiplier (default OVERLAY_LINE_HEIGHT). */
|
|
946
|
+
lineHeight?: number;
|
|
947
|
+
/** Text outline, drawn under the fill. */
|
|
948
|
+
stroke?: TextOverlayStroke;
|
|
949
|
+
/** Background pill behind the text block (absent = none). */
|
|
950
|
+
box?: TextOverlayBox;
|
|
951
|
+
/** Entrance animation. Absent = the legacy `enter` transition. */
|
|
952
|
+
fx?: TextFxSpec;
|
|
953
|
+
/**
|
|
954
|
+
* Wrap width as a FRACTION of the frame width [0.1..1] (the transform.x
|
|
955
|
+
* convention — aspect-stable). Absent = no wrapping (lines break only on
|
|
956
|
+
* explicit \n). Wrapping is greedy over word tokens at measured widths; a
|
|
957
|
+
* single token wider than the budget gets its own line (no intra-word
|
|
958
|
+
* breaks). Tokens keep their trailing whitespace, so fx unit sequences
|
|
959
|
+
* are IDENTICAL wrapped or not — entrances regroup, never recount.
|
|
960
|
+
*/
|
|
961
|
+
maxWidth?: number;
|
|
962
|
+
}
|
|
963
|
+
interface TextOverlayStroke {
|
|
964
|
+
/** CSS stroke color. */
|
|
965
|
+
color: string;
|
|
966
|
+
/** Stroke width in design px at the resolved size. */
|
|
967
|
+
width: number;
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Text background pill. Paddings and radius are EMs of the resolved font
|
|
971
|
+
* size, so the pill scales with the text through size overrides, transform
|
|
972
|
+
* scale and output resolution alike.
|
|
973
|
+
*/
|
|
974
|
+
interface TextOverlayBox {
|
|
975
|
+
/** CSS color of the pill. */
|
|
976
|
+
color: string;
|
|
977
|
+
/** Extra opacity multiplier on top of the clip's fade alpha (default 1). */
|
|
978
|
+
opacity?: number;
|
|
979
|
+
/** Horizontal padding in EMs (default 0.6). */
|
|
980
|
+
paddingX?: number;
|
|
981
|
+
/** Vertical padding in EMs (default 0.35). */
|
|
982
|
+
paddingY?: number;
|
|
983
|
+
/** Corner radius in EMs (default 0.25); clamped to half the pill height. */
|
|
984
|
+
radius?: number;
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Image/video overlay (V1b) — a media card on the overlay layer. `key` rides
|
|
988
|
+
* the same resolution plumbing as source.videoKey / backgroundMedia.key
|
|
989
|
+
* (blob URL in-session, /api/assets URL saved, take-dir path in CLI takes).
|
|
990
|
+
* Sized by `width` (fraction of the FRAME width, aspect from the media) ×
|
|
991
|
+
* transform.scale. Video time is clip-local (t − start), muted (soundtracks
|
|
992
|
+
* belong to doc.audio), looping optional.
|
|
993
|
+
*/
|
|
994
|
+
interface MediaOverlayClip extends OverlayClipBase {
|
|
995
|
+
kind: 'image' | 'video';
|
|
996
|
+
key: string;
|
|
997
|
+
/**
|
|
998
|
+
* The card shadow. Absent = 'soft' — the baked look every
|
|
999
|
+
* doc predating the field renders, so absence lowers byte-identically. 'strong' is the
|
|
1000
|
+
* hero float, 'none' the flat cutout.
|
|
1001
|
+
*/
|
|
1002
|
+
shadow?: 'none' | 'soft' | 'strong';
|
|
1003
|
+
/**
|
|
1004
|
+
* An outline stroke drawn over the clipped media edge.
|
|
1005
|
+
* Absent = none. `width` in design px (scales with the canvas like
|
|
1006
|
+
* radius); any CSS color.
|
|
1007
|
+
*/
|
|
1008
|
+
border?: {
|
|
1009
|
+
width: number;
|
|
1010
|
+
color: string;
|
|
1011
|
+
};
|
|
1012
|
+
/** Base width as a fraction of the frame width [0..1]. Absent = 0.35. */
|
|
1013
|
+
width?: number;
|
|
1014
|
+
/** Corner radius in design px. Absent = 12 (the house card radius). */
|
|
1015
|
+
radius?: number;
|
|
1016
|
+
/** Opacity 0..1. Absent = 1. */
|
|
1017
|
+
opacity?: number;
|
|
1018
|
+
/** Video only: loop while the clip is active. Absent = hold the last frame. */
|
|
1019
|
+
loop?: boolean;
|
|
1020
|
+
}
|
|
1021
|
+
type OverlayClip = TextOverlayClip | MediaOverlayClip;
|
|
1022
|
+
/**
|
|
1023
|
+
* Ceiling on `transform.scale` for text overlays, shared by the canvas box
|
|
1024
|
+
* and the panel so the two can never disagree about where growth stops (a
|
|
1025
|
+
* 64px title at 8× is a 512px hero word — past that it is a poster, not a
|
|
1026
|
+
* caption). The floor is 0.1 in both places.
|
|
1027
|
+
*/
|
|
1028
|
+
declare const OVERLAY_SCALE_MAX = 8;
|
|
1029
|
+
declare const OVERLAY_MEDIA_DEFAULT_WIDTH = 0.35;
|
|
1030
|
+
declare const OVERLAY_MEDIA_DEFAULT_RADIUS = 12;
|
|
1031
|
+
/**
|
|
1032
|
+
* World-space object clips (compositor v2). These shapes are
|
|
1033
|
+
* DRAFTED AS THE FUTURE ENGINE SPEC: field names, asset-ref shape, and transform
|
|
1034
|
+
* convention carry to `objects?: ObjectConfig[]` upstream unchanged — today they
|
|
1035
|
+
* run interpreter-side (ON_FRAME reconciles meshes from ctx.data; live
|
|
1036
|
+
* SET_DATA add/remove), and a later engine release swaps the construction site into the engine.
|
|
1037
|
+
*
|
|
1038
|
+
* Conventions (agent-facing units match the rest of the doc):
|
|
1039
|
+
* - position x/y = FRACTIONS of the frame [0..1] (the overlay/zoom
|
|
1040
|
+
* convention), z = world units TOWARD the camera from the card plane
|
|
1041
|
+
* (0 = on the card's depth; 0.5 floats clearly in front).
|
|
1042
|
+
* - scale = fraction of the FRAME HEIGHT the object's unit size occupies.
|
|
1043
|
+
* - span (OUTPUT seconds) gates visibility with soft edge fades; absent =
|
|
1044
|
+
* the whole timeline.
|
|
1045
|
+
*/
|
|
1046
|
+
type ObjectPrimitiveShape = 'cube' | 'sphere' | 'torus' | 'knot';
|
|
1047
|
+
/**
|
|
1048
|
+
* 3D-text material presets — fleet-audited: everything single-sided,
|
|
1049
|
+
* no `dispersion`, transmission only single-sided (the documented
|
|
1050
|
+
* SwiftShader constraints). Resolved to plain material params at lowering.
|
|
1051
|
+
*/
|
|
1052
|
+
type Text3dMaterial = 'standard' | 'metal' | 'glass' | 'neon';
|
|
1053
|
+
type ObjectAsset =
|
|
1054
|
+
/** Curated primitive props — fleet-safe, no asset fetch. */
|
|
1055
|
+
{
|
|
1056
|
+
kind: 'primitive';
|
|
1057
|
+
shape: ObjectPrimitiveShape;
|
|
1058
|
+
color?: string;
|
|
1059
|
+
}
|
|
1060
|
+
/** GLB by key — accepted in the schema for forward compat with the engine spec; loads in a later slice. */
|
|
1061
|
+
| {
|
|
1062
|
+
kind: 'gltf';
|
|
1063
|
+
key: string;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Extruded 3D text from a hosted typeface JSON. `typeface` is a
|
|
1067
|
+
* catalog slug or family name (GET the list from the typeface catalog;
|
|
1068
|
+
* unknown names fall back to the house face). `depth` is the extrusion as
|
|
1069
|
+
* a fraction of the glyph height (default 0.25); `bevel` defaults on.
|
|
1070
|
+
*/
|
|
1071
|
+
| {
|
|
1072
|
+
kind: 'text3d';
|
|
1073
|
+
text: string;
|
|
1074
|
+
typeface?: string;
|
|
1075
|
+
material?: Text3dMaterial;
|
|
1076
|
+
color?: string;
|
|
1077
|
+
depth?: number;
|
|
1078
|
+
bevel?: boolean;
|
|
1079
|
+
};
|
|
1080
|
+
/** Curated motion presets — pure f(t), deterministic. */
|
|
1081
|
+
type ObjectAnimation = 'spin' | 'float';
|
|
1082
|
+
/**
|
|
1083
|
+
* A pose keyframe on a 3D object clip (the MotionPose model over
|
|
1084
|
+
* transform3d). `at` is CLIP-LOCAL OUTPUT seconds from the clip's span start
|
|
1085
|
+
* (0 when the clip has no span). Absent fields inherit the base transform3d;
|
|
1086
|
+
* `spin`/`float` presets compose ADDITIVELY on top of the sampled pose.
|
|
1087
|
+
*/
|
|
1088
|
+
interface MotionPose3D {
|
|
1089
|
+
at: number;
|
|
1090
|
+
/** Frame fractions (the transform3d.x/y convention). */
|
|
1091
|
+
x?: number;
|
|
1092
|
+
y?: number;
|
|
1093
|
+
/** World units toward the camera from the card plane. */
|
|
1094
|
+
z?: number;
|
|
1095
|
+
/** Euler degrees. */
|
|
1096
|
+
rx?: number;
|
|
1097
|
+
ry?: number;
|
|
1098
|
+
rz?: number;
|
|
1099
|
+
/** Fraction of the frame height. */
|
|
1100
|
+
scale?: number;
|
|
1101
|
+
/** Arrival ease (@vosjs/timeline EASINGS name). Absent = the house motion ease. */
|
|
1102
|
+
ease?: string;
|
|
1103
|
+
}
|
|
1104
|
+
interface ObjectClip {
|
|
1105
|
+
id: string;
|
|
1106
|
+
asset: ObjectAsset;
|
|
1107
|
+
/** OUTPUT-time visibility span; absent = always. */
|
|
1108
|
+
span?: {
|
|
1109
|
+
start: number;
|
|
1110
|
+
duration: number;
|
|
1111
|
+
};
|
|
1112
|
+
transform3d: {
|
|
1113
|
+
x: number;
|
|
1114
|
+
y: number;
|
|
1115
|
+
/** World units toward the camera from the card plane. */
|
|
1116
|
+
z: number;
|
|
1117
|
+
/** Euler degrees. */
|
|
1118
|
+
rx: number;
|
|
1119
|
+
ry: number;
|
|
1120
|
+
rz: number;
|
|
1121
|
+
/** Fraction of the frame height. */
|
|
1122
|
+
scale: number;
|
|
1123
|
+
};
|
|
1124
|
+
animation?: ObjectAnimation | null;
|
|
1125
|
+
/** Pose keyframes (see MotionPose3D). Absent lowers byte-identically. */
|
|
1126
|
+
motion?: MotionPose3D[];
|
|
1127
|
+
}
|
|
1128
|
+
declare const OBJECT_DEFAULT_SCALE = 0.18;
|
|
1129
|
+
/** Enter/exit transition length in seconds (pure f(t) in ON_FRAME). */
|
|
1130
|
+
declare const OVERLAY_TRANSITION_DUR = 0.35;
|
|
1131
|
+
/** Line height multiplier for multi-line text overlays. */
|
|
1132
|
+
declare const OVERLAY_LINE_HEIGHT = 1.25;
|
|
1133
|
+
declare const OVERLAY_MIN_DURATION = 0.2;
|
|
1134
|
+
/**
|
|
1135
|
+
* The editable project state. An app-level convention that *lowers to* a vos
|
|
1136
|
+
* Composition (it is NOT vos core). Fully serializable.
|
|
1137
|
+
*/
|
|
1138
|
+
interface ProjectDoc {
|
|
1139
|
+
source: {
|
|
1140
|
+
videoKey: string;
|
|
1141
|
+
cursor: CursorTrack;
|
|
1142
|
+
meta: RecordingMeta;
|
|
1143
|
+
/** object URL for a separately-recorded webcam track, if the take had a camera. */
|
|
1144
|
+
camKey?: string;
|
|
1145
|
+
/**
|
|
1146
|
+
* object URL for the separately-recorded microphone sidecar (AT split).
|
|
1147
|
+
* When present the recording's own audio track (hasAudio) is SYSTEM/tab
|
|
1148
|
+
* audio and micGain governs this sidecar; absent on legacy takes, where
|
|
1149
|
+
* the recording's track is the old record-time mix.
|
|
1150
|
+
*/
|
|
1151
|
+
micKey?: string;
|
|
1152
|
+
/**
|
|
1153
|
+
* Decode strategy for the recording (vos VideoElement.frameSource):
|
|
1154
|
+
* 'webcodecs' (frame-accurate, MP4), 'html5' (robust, any format), 'auto'.
|
|
1155
|
+
* Defaults to 'auto' in lowering. Dev uploads use 'html5'; the B2 recorder
|
|
1156
|
+
* (known-good MP4) uses 'webcodecs'.
|
|
1157
|
+
*/
|
|
1158
|
+
frameSource?: 'auto' | 'webcodecs' | 'html5';
|
|
1159
|
+
/**
|
|
1160
|
+
* 'image' = videoKey points at a still (screenshot) shown for the doc's
|
|
1161
|
+
* whole duration — the full editing stack (frame, browser bar, zoom, export)
|
|
1162
|
+
* applies unchanged. Defaults to 'video'.
|
|
1163
|
+
*/
|
|
1164
|
+
sourceKind?: 'video' | 'image';
|
|
1165
|
+
/**
|
|
1166
|
+
* drawImage source rect (capture px) for window takes: the viewport's rect
|
|
1167
|
+
* inside the captured frame, derived once at doc build (normalizeCaptureSpace)
|
|
1168
|
+
* — crops the real browser chrome out of the footage so the synthetic
|
|
1169
|
+
* browser bar applies. When set, cursor track + meta dims are already in
|
|
1170
|
+
* crop space. Absent = draw the full frame.
|
|
1171
|
+
*/
|
|
1172
|
+
crop?: Rect;
|
|
1173
|
+
/**
|
|
1174
|
+
* The derived viewport crop + the full capture dims it was cut from — kept
|
|
1175
|
+
* even while the "Original" frame mode shows the uncropped window, so the
|
|
1176
|
+
* crop can be re-applied losslessly (docToCropSpace/docToFullSpace remap
|
|
1177
|
+
* cursor/zoom/meta between the two spaces; the footage itself always holds
|
|
1178
|
+
* the full frame). Present ⟺ crop derivation succeeded at ingest.
|
|
1179
|
+
*/
|
|
1180
|
+
chromeCrop?: {
|
|
1181
|
+
rect: Rect;
|
|
1182
|
+
frameW: number;
|
|
1183
|
+
frameH: number;
|
|
1184
|
+
};
|
|
1185
|
+
};
|
|
1186
|
+
/**
|
|
1187
|
+
* Kept SOURCE-time spans (@vosjs/timeline `Segment`s); the output timeline is
|
|
1188
|
+
* their concatenation — trim/split/cut are all segment edits. Canonical form
|
|
1189
|
+
* is one full-source segment; an empty list is tolerated and means "untrimmed".
|
|
1190
|
+
*/
|
|
1191
|
+
segments: Segment[];
|
|
1192
|
+
/**
|
|
1193
|
+
* Speed-change spans (SOURCE time, footage-anchored — see SpeedSpan).
|
|
1194
|
+
* Optional for backward compatibility with persisted docs; absent = all 1×.
|
|
1195
|
+
*/
|
|
1196
|
+
speed?: SpeedSpan[];
|
|
1197
|
+
/** Zoom regions (SOURCE time, footage-anchored, non-overlapping — see ZoomSpan). */
|
|
1198
|
+
zoom: ZoomSpan[];
|
|
1199
|
+
/**
|
|
1200
|
+
* Camera style — one named strategy preset driving BOTH the auto-zoom
|
|
1201
|
+
* planner and the camera motion (ramps/eases/pans/follow; see zoomStyle.ts).
|
|
1202
|
+
* Absent = DEFAULT_ZOOM_STYLE.
|
|
1203
|
+
*/
|
|
1204
|
+
zoomStyle?: ZoomStyleName;
|
|
1205
|
+
/**
|
|
1206
|
+
* Per-doc overrides on top of the named style — the "Custom" seam for
|
|
1207
|
+
* agents/doc.json (the studio shows Custom while any override is present;
|
|
1208
|
+
* picking a named style clears them). Span edits do NOT set this: the style
|
|
1209
|
+
* describes camera dynamics, spans are content.
|
|
1210
|
+
*/
|
|
1211
|
+
zoomParams?: Partial<ZoomStyleParams>;
|
|
1212
|
+
/**
|
|
1213
|
+
* Per-doc overrides for the auto-speed planner: idle/typing/scroll
|
|
1214
|
+
* thresholds and rates. Absent = DEFAULT_SPEED_PARAMS.
|
|
1215
|
+
*/
|
|
1216
|
+
speedParams?: Partial<SpeedParams>;
|
|
1217
|
+
/**
|
|
1218
|
+
* Tilt regions (SOURCE time, footage-anchored, non-overlapping — see
|
|
1219
|
+
* TiltSpan). Optional: absent lowers byte-identically (no tiltTrack in data).
|
|
1220
|
+
*/
|
|
1221
|
+
tilt?: TiltSpan[];
|
|
1222
|
+
/**
|
|
1223
|
+
* Dynamic-tilt wand intensity (planAutoTilt — the auto-zoom wand contract:
|
|
1224
|
+
* regenerate replaces only `source:'auto'` spans). 'off'/absent = the wand
|
|
1225
|
+
* is off; manual tilt spans work either way.
|
|
1226
|
+
*/
|
|
1227
|
+
tiltStyle?: TiltStyleName;
|
|
1228
|
+
/** music/SFX clips on the output timeline (see AudioClip anchoring note). */
|
|
1229
|
+
audio: AudioClip[];
|
|
1230
|
+
/**
|
|
1231
|
+
* Master gain for the VOICE, 0..1. Absent = 1. With a mic sidecar
|
|
1232
|
+
* (source.micKey) this governs the sidecar; on legacy takes it governs the
|
|
1233
|
+
* recording's own (mixed) track.
|
|
1234
|
+
*/
|
|
1235
|
+
micGain?: number;
|
|
1236
|
+
/**
|
|
1237
|
+
* Master gain for the recording's own SYSTEM/tab audio track, 0..1. Absent
|
|
1238
|
+
* = 1. Only meaningful on split takes (source.micKey present) — legacy
|
|
1239
|
+
* takes have one track and one fader (micGain).
|
|
1240
|
+
*/
|
|
1241
|
+
systemGain?: number;
|
|
1242
|
+
cursor: CursorStyle;
|
|
1243
|
+
cam: CamStyle;
|
|
1244
|
+
/**
|
|
1245
|
+
* Cam pose regions (SOURCE time, footage-anchored, non-overlapping — see
|
|
1246
|
+
* CamPoseSpan). The bubble morphs to a span's pose and back to the rest
|
|
1247
|
+
* pose (doc.cam). Optional: absent lowers byte-identically (no camTrack
|
|
1248
|
+
* in data). Only renders when the take has a cam track (source.camKey).
|
|
1249
|
+
*/
|
|
1250
|
+
camMotion?: CamPoseSpan[];
|
|
1251
|
+
frame: FrameStyle;
|
|
1252
|
+
/**
|
|
1253
|
+
* Card presentation (tilt / entrance) — compositor v2. Optional: absent
|
|
1254
|
+
* lowers byte-identically to a pre-v2 doc and renders pixel-identically.
|
|
1255
|
+
*/
|
|
1256
|
+
/**
|
|
1257
|
+
* Screen-space overlay clips (text; later image/video) — compositor v2.
|
|
1258
|
+
* OUTPUT-anchored spans on the overlay layer. Optional: absent lowers
|
|
1259
|
+
* byte-identically to a doc predating overlays.
|
|
1260
|
+
*/
|
|
1261
|
+
overlays?: OverlayClip[];
|
|
1262
|
+
/**
|
|
1263
|
+
* World-space object clips (interpreter-side; the drafted engine spec).
|
|
1264
|
+
* Optional: absent lowers byte-identically.
|
|
1265
|
+
*/
|
|
1266
|
+
objects?: ObjectClip[];
|
|
1267
|
+
export: {
|
|
1268
|
+
resolution: ExportResolution;
|
|
1269
|
+
fps: 30 | 60;
|
|
1270
|
+
format: 'mp4';
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
/** Export quality presets — each names the SHORT edge of the output (see resolveExportSize). */
|
|
1274
|
+
type ExportResolution = '720p' | '1080p' | '2k' | '4k';
|
|
1275
|
+
declare const EXPORT_SHORT_EDGE: Record<ExportResolution, number>;
|
|
1276
|
+
/** Presets in ascending quality order (picker order; recommendedExportResolution walks it). */
|
|
1277
|
+
declare const EXPORT_RESOLUTION_OPTIONS: ExportResolution[];
|
|
1278
|
+
/**
|
|
1279
|
+
* Default ON (ripple + press, medium, neutral): click emphasis is the point of
|
|
1280
|
+
* the product, and the wrong-window/coverage gates already drop the cursor
|
|
1281
|
+
* track (and with it every click) on takes where effects would misfire.
|
|
1282
|
+
*/
|
|
1283
|
+
declare const DEFAULT_CLICK_FX: ClickFxStyle;
|
|
1284
|
+
declare const DEFAULT_CURSOR_STYLE: CursorStyle;
|
|
1285
|
+
declare const DEFAULT_CAM_STYLE: CamStyle;
|
|
1286
|
+
declare const DEFAULT_BROWSER_BAR: BrowserBarStyle;
|
|
1287
|
+
/**
|
|
1288
|
+
* The frame a NEW take opens on. Once `BACKDROP_DEFAULT_ON`
|
|
1289
|
+
* flips (backdrop.ts), the default is the house loop on its own ground;
|
|
1290
|
+
* until then the brand gradient. Every ingest path spreads this, so the
|
|
1291
|
+
* flip reaches the extension handoff, the in-page recorder, a dropped file
|
|
1292
|
+
* and `vos record` at once; a doc that already carries a frame keeps it.
|
|
1293
|
+
*/
|
|
1294
|
+
declare const DEFAULT_FRAME_STYLE: FrameStyle;
|
|
1295
|
+
/** Border alpha applied when the Frame-border toggle turns on. */
|
|
1296
|
+
declare const FRAME_BORDER_DEFAULT = 0.35;
|
|
1297
|
+
/**
|
|
1298
|
+
* The border a doc that names no width/colour is drawn with: the hairline
|
|
1299
|
+
* white stroke that was hard-coded in ON_FRAME before the two knobs existed,
|
|
1300
|
+
* so every take made before them renders byte-identically after.
|
|
1301
|
+
*/
|
|
1302
|
+
declare const FRAME_BORDER_WIDTH_DEFAULT = 1.5;
|
|
1303
|
+
declare const FRAME_BORDER_COLOR_DEFAULT = "#ffffff";
|
|
1304
|
+
/**
|
|
1305
|
+
* The realistic browser-bar kind matching the recorder's OS — seeds Default
|
|
1306
|
+
* mode so the synthetic chrome looks native to where the take was recorded
|
|
1307
|
+
* (light variants: browsers default light). Windows/Linux get the windows
|
|
1308
|
+
* chrome; when the platform is unknown — a direct upload with no browser
|
|
1309
|
+
* information — we default to macOS.
|
|
1310
|
+
*/
|
|
1311
|
+
declare function platformBarKind(platform: RecordingMeta['platform']): BrowserBarStyle['kind'];
|
|
1312
|
+
/**
|
|
1313
|
+
* Address-pill display text for a recorded page URL: hostname (www. stripped)
|
|
1314
|
+
* plus a non-root path. Empty for non-http(s) or unparsable URLs.
|
|
1315
|
+
*/
|
|
1316
|
+
declare function pageDisplayUrl(pageUrl: string | undefined): string;
|
|
1317
|
+
/** Output aspect-ratio presets (id used as FrameStyle.aspectRatio). Ordered for the picker. */
|
|
1318
|
+
interface AspectRatioOption {
|
|
1319
|
+
id: string;
|
|
1320
|
+
label: string;
|
|
1321
|
+
}
|
|
1322
|
+
declare const ASPECT_RATIOS: AspectRatioOption[];
|
|
1323
|
+
/** Numeric width/height ratio for an aspect-ratio id; 'native' resolves from the source meta. */
|
|
1324
|
+
declare function aspectRatioValue(id: string, meta: {
|
|
1325
|
+
width: number;
|
|
1326
|
+
height: number;
|
|
1327
|
+
}): number;
|
|
1328
|
+
/**
|
|
1329
|
+
* Resolve the export pixel dimensions from the chosen aspect ratio + quality. The quality
|
|
1330
|
+
* (`export.resolution`) is the SHORT edge (720/1080/1440/2160), so 16:9 @ 1080p→1920×1080,
|
|
1331
|
+
* 9:16 @ 4k→2160×3840, 1:1 @ 1080p→1080×1080. Dimensions are rounded to even numbers
|
|
1332
|
+
* (H.264 requires it). Unknown values (hand-edited doc.json) fall back to 1080p.
|
|
1333
|
+
*/
|
|
1334
|
+
declare function resolveExportSize(doc: Pick<ProjectDoc, 'frame' | 'source' | 'export'>, resolution?: ExportResolution): {
|
|
1335
|
+
width: number;
|
|
1336
|
+
height: number;
|
|
1337
|
+
};
|
|
1338
|
+
/**
|
|
1339
|
+
* The quality-preset → pixels math, free of any document. Every product's
|
|
1340
|
+
* export UI resolves its dimensions through this one function (the shared
|
|
1341
|
+
* ExportDialog included), so a preset name means the same thing everywhere:
|
|
1342
|
+
* before it existed the web app carried three private resolution tables that
|
|
1343
|
+
* disagreed about what "2K" was.
|
|
1344
|
+
*/
|
|
1345
|
+
declare function exportSizeFor(ratio: number, resolution: ExportResolution): {
|
|
1346
|
+
width: number;
|
|
1347
|
+
height: number;
|
|
1348
|
+
};
|
|
1349
|
+
|
|
1350
|
+
/**
|
|
1351
|
+
* Capture-space normalization — the single seam that makes window/monitor
|
|
1352
|
+
* recordings look like tab recordings to everything downstream.
|
|
1353
|
+
*
|
|
1354
|
+
* Tab captures record the viewport, so CursorEvent.x/y (viewport CSS px) IS the
|
|
1355
|
+
* cursor space and meta.width/height describes it. For window/monitor captures
|
|
1356
|
+
* the viewport is only part of the frame, so events are mapped into capture
|
|
1357
|
+
* pixels here — once, at doc-build time — using each event's screen coords
|
|
1358
|
+
* (sx/sy) and the geometry sampled at record start. After normalization the
|
|
1359
|
+
* planner, lowering, and composition consume the doc unchanged.
|
|
1360
|
+
*
|
|
1361
|
+
* Window takes additionally get a VIEWPORT CROP when the geometry is clean
|
|
1362
|
+
* (deriveViewportCrop): the real browser
|
|
1363
|
+
* chrome is cut out of the footage at the drawImage seam, the cursor/meta are
|
|
1364
|
+
* rewritten into crop space, and the synthetic browser bar becomes available
|
|
1365
|
+
* exactly as on tab takes. Fail-closed: a wrong crop (chrome sliver, cut page
|
|
1366
|
+
* edge) reads far worse than no crop, so any geometry doubt → no crop.
|
|
1367
|
+
*/
|
|
1368
|
+
|
|
1369
|
+
interface CaptureNormalization {
|
|
1370
|
+
cursor: CursorTrack;
|
|
1371
|
+
meta: RecordingMeta;
|
|
1372
|
+
/**
|
|
1373
|
+
* Fraction of mapped events that landed inside the captured frame (1 for tab
|
|
1374
|
+
* captures). Low coverage means the user shared a different window/display
|
|
1375
|
+
* than the one hosting the recorded tab — the app should skip auto-zoom and
|
|
1376
|
+
* cursor overlay rather than render them at wrong positions.
|
|
1377
|
+
*/
|
|
1378
|
+
coverage: number;
|
|
1379
|
+
/**
|
|
1380
|
+
* Window takes with clean geometry: the viewport's rect inside the capture
|
|
1381
|
+
* frame (capture px) — the drawImage source crop that removes the real
|
|
1382
|
+
* browser chrome. When present, the returned cursor/meta are already in
|
|
1383
|
+
* crop space. Absent = render the full frame.
|
|
1384
|
+
*/
|
|
1385
|
+
crop?: Rect;
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Derive the viewport crop for a window take: where the page viewport sits
|
|
1389
|
+
* inside the captured window frame, in capture px.
|
|
1390
|
+
*
|
|
1391
|
+
* Two independent estimators cross-check each other:
|
|
1392
|
+
* 1. event-derived (primary): each event's own viewport→screen offset
|
|
1393
|
+
* (sx − x, sy − y) — exact wherever the chrome actually is, but needs events;
|
|
1394
|
+
* 2. window-derived: windowRect vs meta.viewport under the chrome-on-top
|
|
1395
|
+
* assumption (side insets split evenly) — no events needed, but wrong for
|
|
1396
|
+
* docked devtools / exotic decorations.
|
|
1397
|
+
*
|
|
1398
|
+
* Only offsets agreeing with (2) are kept — this simultaneously validates the
|
|
1399
|
+
* chrome-on-top assumption AND rejects cross-origin-iframe events, whose
|
|
1400
|
+
* offsets are the IFRAME's origin, not the top viewport's. Returns null
|
|
1401
|
+
* (no crop) on any doubt — see the fail-closed matrix in the analysis doc.
|
|
1402
|
+
*/
|
|
1403
|
+
declare function deriveViewportCrop(cursor: CursorTrack, meta: RecordingMeta): Rect | null;
|
|
1404
|
+
/**
|
|
1405
|
+
* Map a cursor track into the capture's pixel space. Identity for tab captures
|
|
1406
|
+
* (or when geometry is missing). For window/monitor captures the returned meta
|
|
1407
|
+
* has width/height set to the capture pixel dimensions (the new cursor space)
|
|
1408
|
+
* and dpr/zoom reset to 1 — cursor space and video pixels now coincide. When a
|
|
1409
|
+
* window take yields a viewport crop, cursor space is the CROPPED frame and
|
|
1410
|
+
* meta dims are the crop dims (downstream layout/planner/zoom need no crop
|
|
1411
|
+
* awareness — only drawImage reads the rect).
|
|
1412
|
+
*/
|
|
1413
|
+
declare function normalizeCaptureSpace(cursor: CursorTrack, meta: RecordingMeta): CaptureNormalization;
|
|
1414
|
+
/** Coverage below this → treat the cursor track as unusable (skip auto-zoom + overlay). */
|
|
1415
|
+
declare const CAPTURE_COVERAGE_MIN = 0.5;
|
|
1416
|
+
/**
|
|
1417
|
+
* Window takes whose browser window was focused for less than this fraction of
|
|
1418
|
+
* the take are treated as wrong-window shares: cursor track dropped, no
|
|
1419
|
+
* viewport crop (see RecordingMeta.windowFocusedFrac).
|
|
1420
|
+
*/
|
|
1421
|
+
declare const WINDOW_FOCUS_MIN = 0.5;
|
|
1422
|
+
/**
|
|
1423
|
+
* Crop-space ↔ full-space doc remaps — the "Original" frame mode (show the
|
|
1424
|
+
* user's real browser chrome on a cropped window take). The footage always
|
|
1425
|
+
* holds the full frame, so the toggle is a LOSSLESS coordinate remap of the
|
|
1426
|
+
* doc (cursor events/rects, zoom focus points, meta dims) driven by the
|
|
1427
|
+
* chromeCrop record kept from ingest — one undoable patch-store edit, program
|
|
1428
|
+
* string untouched (everything involved lowers into ctx.data). Time-based
|
|
1429
|
+
* state (segments, speed, audio, cam window) is space-independent.
|
|
1430
|
+
*
|
|
1431
|
+
* Both helpers MUTATE a draft doc (call inside the store's edit()) and are
|
|
1432
|
+
* no-ops when the doc is already in the requested space or has no chromeCrop.
|
|
1433
|
+
*/
|
|
1434
|
+
/** Remap a crop-space doc to full-capture space (show the original chrome). */
|
|
1435
|
+
declare function docToFullSpace(d: ProjectDoc): void;
|
|
1436
|
+
/** Remap a full-space doc back into crop space (hide the chrome again). */
|
|
1437
|
+
declare function docToCropSpace(d: ProjectDoc): void;
|
|
1438
|
+
|
|
1439
|
+
/** Build a ProjectDoc from a RecordingArtifact handed off by a recorder. */
|
|
1440
|
+
declare function projectFromArtifact(artifact: RecordingArtifact, videoUrl: string): {
|
|
1441
|
+
doc: ProjectDoc;
|
|
1442
|
+
videoUrl: string;
|
|
1443
|
+
};
|
|
1444
|
+
|
|
1445
|
+
/**
|
|
1446
|
+
* Hosted-doc schema versioning: the scoped reversal of "ProjectDocs
|
|
1447
|
+
* are never persisted" is hosted versions only, and every persisted doc is
|
|
1448
|
+
* stamped `docSchemaVersion` from day one so the migration obligation the
|
|
1449
|
+
* old rule avoided stays bounded to one seam — migrate-on-read, here.
|
|
1450
|
+
*
|
|
1451
|
+
* Local studio sessions still never persist docs; CLI take dirs carry
|
|
1452
|
+
* doc.json under `schema/doc.schema.json` (which tolerates the stamp via
|
|
1453
|
+
* additionalProperties). Both hydration paths (studio handback, `vos
|
|
1454
|
+
* pull`) run through migrateHostedDoc before trusting a hosted doc.
|
|
1455
|
+
*/
|
|
1456
|
+
/**
|
|
1457
|
+
* 2 = the document FAMILY era: a doc is a recording document (`source`)
|
|
1458
|
+
* or a program document (`program.config`). A v1 doc IS a recording document,
|
|
1459
|
+
* field for field, so 1 → 2 is a stamp; 0 → 1 was a stamp too.
|
|
1460
|
+
*/
|
|
1461
|
+
declare const DOC_SCHEMA_VERSION = 2;
|
|
1462
|
+
/**
|
|
1463
|
+
* Upgrade a hosted doc.json payload to the current schema version.
|
|
1464
|
+
* Unstamped docs are v0 — the pre-stamp era. Every step so far is a stamp
|
|
1465
|
+
* (a post-v1 doc field is optional by doctrine, and v2 only widened the
|
|
1466
|
+
* family), so migration is structural identity. A real shape change chains
|
|
1467
|
+
* its step here.
|
|
1468
|
+
*/
|
|
1469
|
+
declare function migrateHostedDoc(raw: Record<string, unknown>): Record<string, unknown>;
|
|
1470
|
+
|
|
1471
|
+
/**
|
|
1472
|
+
* The studio document family.
|
|
1473
|
+
*
|
|
1474
|
+
* A document is an ANCHOR plus the layers every anchor shares. `ProjectDoc`
|
|
1475
|
+
* is the recording-anchored member, field for field what it always was (its
|
|
1476
|
+
* wire, `doc.json`, does not move). `ProgramAnchorDoc` is the program-anchored
|
|
1477
|
+
* member: its anchor IS the user's config — the execution IR, untouched
|
|
1478
|
+
* — plus the tween-timing overlay that used to live only in a
|
|
1479
|
+
* hook. The shared layers are optional on it until the shared modules activate them.
|
|
1480
|
+
*
|
|
1481
|
+
* Discriminated on `source`: every recording doc carries one, no program doc
|
|
1482
|
+
* may.
|
|
1483
|
+
*/
|
|
1484
|
+
/** One entry of the tween-timing overlay — `@vosjs/tween`'s `TweenEdit`, structurally. */
|
|
1485
|
+
interface ProgramTweenEdit {
|
|
1486
|
+
index: number;
|
|
1487
|
+
startTime?: number;
|
|
1488
|
+
duration?: number;
|
|
1489
|
+
ease?: string;
|
|
1490
|
+
to?: Record<string, number>;
|
|
1491
|
+
from?: Record<string, number>;
|
|
1492
|
+
}
|
|
1493
|
+
interface ProgramAnchorDoc {
|
|
1494
|
+
program: {
|
|
1495
|
+
/** THE user's config, as authored (functions as strings). Never composed here. */
|
|
1496
|
+
config: Record<string, unknown>;
|
|
1497
|
+
/** Retimes over the config's recorded tweens, by spec index. */
|
|
1498
|
+
tweenEdits?: Record<number, ProgramTweenEdit>;
|
|
1499
|
+
/** The anchor's own length when the config's is a placeholder. */
|
|
1500
|
+
duration?: number;
|
|
1501
|
+
};
|
|
1502
|
+
overlays?: OverlayClip[];
|
|
1503
|
+
objects?: ObjectClip[];
|
|
1504
|
+
/** Required, like the recording's: the audio module and its lane read it without a guard. Minted `[]`. */
|
|
1505
|
+
audio: AudioClip[];
|
|
1506
|
+
/** Retime spans over the ANCHOR's clock: the recording's type, `in`/`out` in program seconds. */
|
|
1507
|
+
speed?: SpeedSpan[];
|
|
1508
|
+
export?: ProjectDoc['export'];
|
|
1509
|
+
}
|
|
1510
|
+
type StudioDoc = ProjectDoc | ProgramAnchorDoc;
|
|
1511
|
+
type AnchorKind = 'recording' | 'program';
|
|
1512
|
+
declare const anchorKindOf: (doc: StudioDoc) => AnchorKind;
|
|
1513
|
+
declare const isRecordingDoc: (doc: StudioDoc) => doc is ProjectDoc;
|
|
1514
|
+
declare const isProgramDoc: (doc: StudioDoc) => doc is ProgramAnchorDoc;
|
|
1515
|
+
/** A program anchor's own length in seconds: `program.duration`, else the config's. */
|
|
1516
|
+
declare function programDuration(doc: ProgramAnchorDoc): number;
|
|
1517
|
+
/**
|
|
1518
|
+
* The anchor's SOURCE length: the footage's for a recording, the
|
|
1519
|
+
* program's own for a program. Speed spans, segments and every source-time
|
|
1520
|
+
* floor measure against it.
|
|
1521
|
+
*/
|
|
1522
|
+
declare function anchorSourceDuration(doc: StudioDoc): number;
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* Cursor smoothing.
|
|
1526
|
+
*
|
|
1527
|
+
* Raw pointer samples are jittery and irregularly spaced. We resample to a fixed
|
|
1528
|
+
* cadence and apply an exponential lerp — counterintuitively, linear smoothing
|
|
1529
|
+
* beats ease-in-out for cursors (easing stutters between samples). Pure and
|
|
1530
|
+
* deterministic: same input → same output.
|
|
1531
|
+
*/
|
|
1532
|
+
|
|
1533
|
+
interface SmoothPoint {
|
|
1534
|
+
/** seconds. */
|
|
1535
|
+
t: number;
|
|
1536
|
+
x: number;
|
|
1537
|
+
y: number;
|
|
1538
|
+
}
|
|
1539
|
+
interface SmoothOptions {
|
|
1540
|
+
/** lerp factor per step, 0..1 (higher = smoother/laggier). Default 0.15. */
|
|
1541
|
+
factor?: number;
|
|
1542
|
+
/** resample cadence in fps. Default 60. */
|
|
1543
|
+
fps?: number;
|
|
1544
|
+
/**
|
|
1545
|
+
* Pull the smoothed path onto each click's true position around the click
|
|
1546
|
+
* instant: click effects anchor at the
|
|
1547
|
+
* click point, so the (laggy) smoothed cursor must arrive on time or the
|
|
1548
|
+
* ring blooms away from the dot. The pull feeds back into the lerp state,
|
|
1549
|
+
* so the path continues from the click point afterwards. Deterministic.
|
|
1550
|
+
*/
|
|
1551
|
+
clickSnap?: boolean;
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Produce a smoothed, fixed-cadence cursor path (in seconds) from a raw track.
|
|
1555
|
+
* Only `move`/`down`/`up` events carry positions; others are ignored here —
|
|
1556
|
+
* `scroll` re-emits a stale point, and `focus`/`key` synthesize element
|
|
1557
|
+
* centers the cursor never visited (letting those in would teleport the dot).
|
|
1558
|
+
*/
|
|
1559
|
+
declare function smoothCursor(track: CursorTrack, options?: SmoothOptions): SmoothPoint[];
|
|
1560
|
+
|
|
1561
|
+
/**
|
|
1562
|
+
* A click cluster whose element FITS the frame at less than this level is
|
|
1563
|
+
* not a target: it is a drag (aiming, scrubbing, moving a thing across the
|
|
1564
|
+
* canvas) or a frame-sized surface, and a zoom on it says nothing. Five real
|
|
1565
|
+
* takes (2026-08-25) each carried 1-4 such clusters, planned at the floor
|
|
1566
|
+
* level for 10-25s; every one was dropped by hand. Now they plan nothing.
|
|
1567
|
+
*/
|
|
1568
|
+
declare const DRAG_FIT_LEVEL = 1.15;
|
|
1569
|
+
interface PlanOptions {
|
|
1570
|
+
/** captured frame size (for normalizing rects → [0..1] focus points). */
|
|
1571
|
+
width: number;
|
|
1572
|
+
height: number;
|
|
1573
|
+
/**
|
|
1574
|
+
* Camera style whose planner params seed every default below (zoomStyle.ts).
|
|
1575
|
+
* Explicit options still win. Absent = DEFAULT_ZOOM_STYLE.
|
|
1576
|
+
*/
|
|
1577
|
+
style?: ZoomStyleName;
|
|
1578
|
+
/** per-doc overrides on top of the style (doc.zoomParams — the Custom seam). */
|
|
1579
|
+
params?: Partial<ZoomStyleParams>;
|
|
1580
|
+
/** target: zoom so the element fills ~this fraction of the frame. */
|
|
1581
|
+
targetFill?: number;
|
|
1582
|
+
/** clamp zoom level. */
|
|
1583
|
+
minLevel?: number;
|
|
1584
|
+
maxLevel?: number;
|
|
1585
|
+
/** clicks within this many seconds merge into one zoom (session merge). */
|
|
1586
|
+
clusterGap?: number;
|
|
1587
|
+
/** span lead-in before the first click + hold after the last. */
|
|
1588
|
+
lead?: number;
|
|
1589
|
+
hold?: number;
|
|
1590
|
+
/** minimum clicks for a cluster to earn a zoom (Cursorful's ≥2 rule). */
|
|
1591
|
+
minClusterClicks?: number;
|
|
1592
|
+
/** emit spans with focusMode 'auto' (cursor-follow camera). */
|
|
1593
|
+
followByDefault?: boolean;
|
|
1594
|
+
/** typing sessions plan spans. */
|
|
1595
|
+
typingZoom?: boolean;
|
|
1596
|
+
/** max silence between `key` pings before the typing session ends. */
|
|
1597
|
+
typingGap?: number;
|
|
1598
|
+
/** hold after the last keystroke (the read-what-you-typed beat). */
|
|
1599
|
+
typingHold?: number;
|
|
1600
|
+
/** level floor for typing spans (a wide field still reads punchier). */
|
|
1601
|
+
typingMinLevel?: number;
|
|
1602
|
+
}
|
|
1603
|
+
declare function planAutoZoom(track: CursorTrack, options: PlanOptions): ZoomSpan[];
|
|
1604
|
+
|
|
1605
|
+
/** Zoom spans shorter than this (OUTPUT seconds) get no tilt — a pose that
|
|
1606
|
+
* can't settle before the zoom leaves reads as wobble, not emphasis. */
|
|
1607
|
+
declare const TILT_AUTO_MIN = 1.2;
|
|
1608
|
+
/** Focus offsets under this (|cx−0.5| fraction) don't tilt that axis — a
|
|
1609
|
+
* centered zoom keeps the pure push-in feel; tilt is for off-center focus. */
|
|
1610
|
+
declare const TILT_AUTO_DEAD_ZONE = 0.12;
|
|
1611
|
+
interface PlanTiltOptions {
|
|
1612
|
+
/** Intensity ladder — max degrees per axis (TILT_INTENSITY_MAX). */
|
|
1613
|
+
intensity: Exclude<TiltStyleName, 'off'>;
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* One tilt span per qualifying zoom span, SAME source extents (the tracks
|
|
1617
|
+
* ramp/chain together), pose aimed at the zoom's focus. Spans whose footage
|
|
1618
|
+
* is cut away, whose output run is too short, or whose focus is centered
|
|
1619
|
+
* (both axes inside the dead zone) emit nothing.
|
|
1620
|
+
*/
|
|
1621
|
+
declare function planAutoTilt(zoom: readonly ZoomSpan[], segments: Segment[], options: PlanTiltOptions): TiltSpan[];
|
|
1622
|
+
|
|
1623
|
+
type MomentKind = 'head' | 'tail' | 'click' | 'typing' | 'scroll' | 'dwell' | 'idle' | 'scene';
|
|
1624
|
+
/** A rect in normalized [0..1] frame fractions (the zoom cx/cy convention). */
|
|
1625
|
+
interface NormRect {
|
|
1626
|
+
x: number;
|
|
1627
|
+
y: number;
|
|
1628
|
+
w: number;
|
|
1629
|
+
h: number;
|
|
1630
|
+
}
|
|
1631
|
+
interface Moment {
|
|
1632
|
+
id: string;
|
|
1633
|
+
kind: MomentKind;
|
|
1634
|
+
/** Footage seconds. Instants (scene/head/tail) have in === out. */
|
|
1635
|
+
source: {
|
|
1636
|
+
in: number;
|
|
1637
|
+
out: number;
|
|
1638
|
+
};
|
|
1639
|
+
/** The same window in OUTPUT seconds, null when trimmed away. */
|
|
1640
|
+
output: {
|
|
1641
|
+
in: number;
|
|
1642
|
+
out: number;
|
|
1643
|
+
} | null;
|
|
1644
|
+
/** The source instant the digest frames (null = no frame, e.g. idle). */
|
|
1645
|
+
at: number | null;
|
|
1646
|
+
/** `at` mapped to output time (null when cut or frameless). */
|
|
1647
|
+
outputAt: number | null;
|
|
1648
|
+
/** Normalized focus — copy into a ZoomSpan's cx/cy. */
|
|
1649
|
+
focus: {
|
|
1650
|
+
cx: number;
|
|
1651
|
+
cy: number;
|
|
1652
|
+
} | null;
|
|
1653
|
+
/** Normalized target bounds (element rect union), when the events had one. */
|
|
1654
|
+
rect: NormRect | null;
|
|
1655
|
+
clicks?: number;
|
|
1656
|
+
pings?: number;
|
|
1657
|
+
/** Motion in the window, 0..1 (fraction of changed pixels), null without frames. */
|
|
1658
|
+
activity: number | null;
|
|
1659
|
+
/** Planner spans (from `plan`) that cover this moment, by id. */
|
|
1660
|
+
proposed: {
|
|
1661
|
+
zoom?: string;
|
|
1662
|
+
speed?: string;
|
|
1663
|
+
tilt?: string;
|
|
1664
|
+
};
|
|
1665
|
+
/** Transcript text over the window, when a transcript was merged. */
|
|
1666
|
+
said: string | null;
|
|
1667
|
+
}
|
|
1668
|
+
interface DigestPlan {
|
|
1669
|
+
zoom: ZoomSpan[];
|
|
1670
|
+
speed: SpeedSpan[];
|
|
1671
|
+
tilt: TiltSpan[];
|
|
1672
|
+
}
|
|
1673
|
+
interface TranscriptSegment {
|
|
1674
|
+
/** SOURCE seconds (the recording's own clock). */
|
|
1675
|
+
start: number;
|
|
1676
|
+
end: number;
|
|
1677
|
+
text: string;
|
|
1678
|
+
}
|
|
1679
|
+
interface MomentsOptions {
|
|
1680
|
+
/** Per-SOURCE-second motion bins (0..1) from the frame-diff pass. */
|
|
1681
|
+
bins?: readonly number[] | null;
|
|
1682
|
+
/** Source seconds of visual scene changes (see scenes.ts). */
|
|
1683
|
+
scenes?: readonly number[];
|
|
1684
|
+
transcript?: readonly TranscriptSegment[] | null;
|
|
1685
|
+
/** Idle threshold (seconds); defaults to the speed planner's. */
|
|
1686
|
+
idleMin?: number;
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* The three planners, run fresh under the doc's own style — the proposals.
|
|
1690
|
+
* With activity bins (a digest's decode pass), the speed planner can tell
|
|
1691
|
+
* playback from idle.
|
|
1692
|
+
*/
|
|
1693
|
+
declare function planForDigest(doc: ProjectDoc, activity?: readonly number[] | null): DigestPlan;
|
|
1694
|
+
declare function momentsFromDoc(doc: ProjectDoc, plan: DigestPlan, opts?: MomentsOptions): Moment[];
|
|
1695
|
+
|
|
1696
|
+
/**
|
|
1697
|
+
* Scene changes from the digest's motion bins: a SOURCE second whose
|
|
1698
|
+
* changed-pixel fraction jumps past `motion` after a second at or below
|
|
1699
|
+
* `quiet` is a scene — a navigation, a dialog, a page swap. A luma diff, not
|
|
1700
|
+
* a scene detector: a video playing inside the page is a permanent change,
|
|
1701
|
+
* and the moment kind says `scene`, never "navigation" — the agent looks at
|
|
1702
|
+
* the frame to say what it was.
|
|
1703
|
+
*/
|
|
1704
|
+
interface SceneOptions {
|
|
1705
|
+
/** Changed-pixel fraction that reads as a change. */
|
|
1706
|
+
motion?: number;
|
|
1707
|
+
/** The bin before must be at or below this. */
|
|
1708
|
+
quiet?: number;
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Calibrated on a dark-theme CLI take (launch-d2, 2026-08-25): a page swap
|
|
1712
|
+
* moved 0.33–0.36 of a 64×36 luma thumb, a click's own ripple ≤0.03 — so a
|
|
1713
|
+
* quarter of the pixels is a scene and a tenth is still quiet.
|
|
1714
|
+
*/
|
|
1715
|
+
declare const SCENE_MOTION = 0.25;
|
|
1716
|
+
declare const SCENE_QUIET = 0.1;
|
|
1717
|
+
declare function sceneChanges(bins: readonly number[], opts?: SceneOptions): number[];
|
|
1718
|
+
|
|
1719
|
+
interface CardLayout {
|
|
1720
|
+
/** canvas size the layout was computed for (comp px). */
|
|
1721
|
+
W: number;
|
|
1722
|
+
H: number;
|
|
1723
|
+
/** video destination rect, comp px. Contain: the fitted video. Cover
|
|
1724
|
+
* the cover-scaled video, positioned by frame.focus — it can
|
|
1725
|
+
* overflow the card, which crops it. */
|
|
1726
|
+
dx: number;
|
|
1727
|
+
dy: number;
|
|
1728
|
+
dw: number;
|
|
1729
|
+
dh: number;
|
|
1730
|
+
/** card rect = browser-bar strip + footage area — what the zoom must keep
|
|
1731
|
+
* covering. Contain: cardX/cardW equal dx/dw. Cover: the padded area
|
|
1732
|
+
* itself, which the video rect overflows. */
|
|
1733
|
+
cardX: number;
|
|
1734
|
+
cardY: number;
|
|
1735
|
+
cardW: number;
|
|
1736
|
+
cardH: number;
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Mirror of ON_FRAME's destination-rect math (see the "video destination rect"
|
|
1740
|
+
* block in lowerToComposition's ON_FRAME string). `video` is the source's
|
|
1741
|
+
* pixel size — only its aspect matters (contain-fit scales it).
|
|
1742
|
+
*/
|
|
1743
|
+
declare function computeCardLayout(frame: FrameStyle, video: {
|
|
1744
|
+
width: number;
|
|
1745
|
+
height: number;
|
|
1746
|
+
}, W: number, H: number): CardLayout;
|
|
1747
|
+
/**
|
|
1748
|
+
* The doc's card layout at design size (H = 1080, W from the output aspect).
|
|
1749
|
+
* All layout terms scale linearly with the canvas at a fixed aspect, so the
|
|
1750
|
+
* normalized focus bounds computed from this layout hold at ANY render size.
|
|
1751
|
+
*
|
|
1752
|
+
* Viewport-cropped window takes need no special casing here: normalization
|
|
1753
|
+
* rewrote meta.captureWidth/Height to the CROP dims, which is exactly what
|
|
1754
|
+
* ON_FRAME uses as source dims when `d.crop` is set (crp.w/crp.h) — the
|
|
1755
|
+
* golden contract holds through the crop.
|
|
1756
|
+
*/
|
|
1757
|
+
declare function docCardLayout(doc: Pick<ProjectDoc, 'frame' | 'source'>): CardLayout;
|
|
1758
|
+
/**
|
|
1759
|
+
* Largest export preset whose footage card still gets ≥1 captured px per output
|
|
1760
|
+
* px. The composited chrome (background, bar, cursor, effects) is vector-drawn
|
|
1761
|
+
* and real at any size; the footage layer is bounded by capture pixels, so
|
|
1762
|
+
* presets above this one upscale the footage (the picker labels them, never
|
|
1763
|
+
* hides them). Judged on the width of
|
|
1764
|
+
* the contain-fitted card at each preset's output size vs meta.captureWidth
|
|
1765
|
+
* (crop-space when a viewport crop applies — ingest rewrote meta to crop dims).
|
|
1766
|
+
* Floors at the smallest preset for tiny captures.
|
|
1767
|
+
*/
|
|
1768
|
+
declare function recommendedExportResolution(doc: Pick<ProjectDoc, 'frame' | 'source' | 'export'>): ExportResolution;
|
|
1769
|
+
interface CamBubbleRect {
|
|
1770
|
+
/** top-left corner of the bubble's square, design px (docCardLayout space). */
|
|
1771
|
+
x: number;
|
|
1772
|
+
y: number;
|
|
1773
|
+
/** the square's side — the bubble diameter. */
|
|
1774
|
+
size: number;
|
|
1775
|
+
/** corner radius (size/2 for circles, the fixed rounded radius otherwise). */
|
|
1776
|
+
radius: number;
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Host-side mirror of ON_FRAME's webcam-bubble geometry (the "webcam bubble"
|
|
1780
|
+
* block in lowerToComposition) — the picking oracle for the on-canvas cam
|
|
1781
|
+
* layer. Same design space as docCardLayout (H = 1080), and like ON_FRAME the
|
|
1782
|
+
* bubble is FRAME-owned chrome: everything scales by s = H/1080, never the
|
|
1783
|
+
* card-chrome cf. The bubble ignores card tilt/zoom (it paints on the
|
|
1784
|
+
* screen-space overlay plane), so this rect is valid under any camera pose.
|
|
1785
|
+
* camDraw.test.ts pins this to the painted geometry — change them TOGETHER.
|
|
1786
|
+
*/
|
|
1787
|
+
declare function camBubbleRect(cam: CamStyle, W: number, H?: number): CamBubbleRect;
|
|
1788
|
+
interface FocusBounds {
|
|
1789
|
+
minX: number;
|
|
1790
|
+
maxX: number;
|
|
1791
|
+
minY: number;
|
|
1792
|
+
maxY: number;
|
|
1793
|
+
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Focus bounds (normalized video coords) so the zoomed CARD covers the whole
|
|
1796
|
+
* canvas — the crop never reveals background past a card edge at full zoom.
|
|
1797
|
+
*
|
|
1798
|
+
* Derivation: ON_FRAME's transform maps content point p → f + (p − f)·L around
|
|
1799
|
+
* the focus anchor f (fx = dx + zx·dw), so the visible canvas [0, V] shows
|
|
1800
|
+
* content [f − f/L, f + (V − f)/L]. Requiring that window ⊆ the cover range
|
|
1801
|
+
* [o, o + c] and solving for the anchor (k = 1 − 1/L):
|
|
1802
|
+
*
|
|
1803
|
+
* f ≥ o / k and f ≤ (o + c − V/L) / k
|
|
1804
|
+
*
|
|
1805
|
+
* When the zoomed card is too small to cover the canvas (low level + padding),
|
|
1806
|
+
* the bounds cross — collapse to their midpoint (the least-uncovered focus;
|
|
1807
|
+
* 0.5 for a centered card, matching OpenScreen's margin collapse).
|
|
1808
|
+
*/
|
|
1809
|
+
declare function focusBounds(level: number, layout: CardLayout): FocusBounds;
|
|
1810
|
+
/** Clamp a focus point into the bounds for its zoom level. */
|
|
1811
|
+
declare function clampFocus(cx: number, cy: number, level: number, layout: CardLayout): {
|
|
1812
|
+
cx: number;
|
|
1813
|
+
cy: number;
|
|
1814
|
+
};
|
|
1815
|
+
/**
|
|
1816
|
+
* The zoom level a focus rect of this canvas-fraction size
|
|
1817
|
+
* means — the aiming rect's size is purely 1/level, so a corner drag IS a
|
|
1818
|
+
* level drag, and this is the inverse. Floored a hair above the identity so
|
|
1819
|
+
* a drag can never reach level ≈ 1 and dismiss the aiming rect mid-gesture;
|
|
1820
|
+
* clamped and quantized like every stored level (clampZoomLevel).
|
|
1821
|
+
*/
|
|
1822
|
+
declare function levelForFocusFraction(frac: number): number;
|
|
1823
|
+
|
|
1824
|
+
interface ZoomWindow {
|
|
1825
|
+
x0: number;
|
|
1826
|
+
x1: number;
|
|
1827
|
+
y0: number;
|
|
1828
|
+
y1: number;
|
|
1829
|
+
}
|
|
1830
|
+
/** The visible window in normalized video coords at `level` around the focus. */
|
|
1831
|
+
declare function zoomWindow(span: {
|
|
1832
|
+
level: number;
|
|
1833
|
+
cx: number;
|
|
1834
|
+
cy: number;
|
|
1835
|
+
}, layout: CardLayout): ZoomWindow;
|
|
1836
|
+
/**
|
|
1837
|
+
* True when the rect (normalized) sits inside the zoom's visible window, with
|
|
1838
|
+
* `tol` of slack per edge (frame fractions). A level ≤ 1 zoom shows the whole
|
|
1839
|
+
* frame and covers everything.
|
|
1840
|
+
*/
|
|
1841
|
+
declare function zoomCoversRect(span: {
|
|
1842
|
+
level: number;
|
|
1843
|
+
cx: number;
|
|
1844
|
+
cy: number;
|
|
1845
|
+
}, rect: NormRect, layout: CardLayout, tol?: number): boolean;
|
|
1846
|
+
|
|
1847
|
+
/**
|
|
1848
|
+
* Style as DATA: the fields of a signed-off take's doc that ARE its
|
|
1849
|
+
* style — camera, speed, tilt personality, frame, cursor, cam bubble, export.
|
|
1850
|
+
* `copyStyle` carries them onto another take deterministically, so a series
|
|
1851
|
+
* shares them by construction and the recipe (CUT.md) only has to say what a
|
|
1852
|
+
* number cannot. Never the spans, overlays or audio: those are the cut.
|
|
1853
|
+
*/
|
|
1854
|
+
|
|
1855
|
+
declare const STYLE_FIELDS: readonly ["zoomStyle", "zoomParams", "speedParams", "tiltStyle", "frame", "cursor", "cam", "export"];
|
|
1856
|
+
type StyleField = (typeof STYLE_FIELDS)[number];
|
|
1857
|
+
/** The style fields present on a doc, deep-cloned. */
|
|
1858
|
+
declare function pickStyle(doc: ProjectDoc): Partial<Pick<ProjectDoc, StyleField>>;
|
|
1859
|
+
/**
|
|
1860
|
+
* A new doc: `to` with `from`'s style fields. A field absent on `from` is
|
|
1861
|
+
* removed from the result (the seed's absence is a choice — the default).
|
|
1862
|
+
*/
|
|
1863
|
+
declare function copyStyle(from: ProjectDoc, to: ProjectDoc): ProjectDoc;
|
|
1864
|
+
|
|
1865
|
+
/**
|
|
1866
|
+
* The digest's crop geometry, pure and shared by the CLI's page and
|
|
1867
|
+
* the fleet's page: cursor/meta coords are CSS px of the viewport (or the
|
|
1868
|
+
* crop space when `source.crop` is set); the frame is capture px. A window
|
|
1869
|
+
* take's crop applies to the FRAME, never to the already-cropped cursor
|
|
1870
|
+
* coords. Both hosts compute boxes here so the two never drift by a dpr.
|
|
1871
|
+
*/
|
|
1872
|
+
|
|
1873
|
+
interface PxRect {
|
|
1874
|
+
x: number;
|
|
1875
|
+
y: number;
|
|
1876
|
+
w: number;
|
|
1877
|
+
h: number;
|
|
1878
|
+
}
|
|
1879
|
+
/** A crop box is at least this fraction of the frame width (floor 320px). */
|
|
1880
|
+
declare const CROP_MIN_FRAC = 0.25;
|
|
1881
|
+
declare const CROP_MIN_PX = 320;
|
|
1882
|
+
declare const CROP_MAX_PX = 1024;
|
|
1883
|
+
declare const CROP_PAD = 0.25;
|
|
1884
|
+
/** Default long edges (px) of the emitted images — the agent's token budget. */
|
|
1885
|
+
declare const DIGEST_FULL_MAX = 960;
|
|
1886
|
+
declare const DIGEST_CROP_MAX = 640;
|
|
1887
|
+
/** Changed-pixel threshold (luma, 0..255) for the motion bins. */
|
|
1888
|
+
declare const MOTION_DELTA = 24;
|
|
1889
|
+
interface FrameGeometry {
|
|
1890
|
+
/** The region of the frame the doc renders (the viewport crop, or all). */
|
|
1891
|
+
region: PxRect;
|
|
1892
|
+
/** Cursor px → frame px. */
|
|
1893
|
+
scale: number;
|
|
1894
|
+
}
|
|
1895
|
+
/** The frame's pixel size when no decode has told us: capture px, else CSS×dpr. */
|
|
1896
|
+
declare function expectedFrameSize(doc: ProjectDoc): {
|
|
1897
|
+
width: number;
|
|
1898
|
+
height: number;
|
|
1899
|
+
};
|
|
1900
|
+
declare function frameGeometry(doc: ProjectDoc, frameW: number, frameH: number): FrameGeometry;
|
|
1901
|
+
/** The crop box (frame px) around a moment's rect or focus point, or null. */
|
|
1902
|
+
declare function cropBox(m: Pick<Moment, 'rect' | 'focus'>, geo: FrameGeometry, meta: {
|
|
1903
|
+
width: number;
|
|
1904
|
+
height: number;
|
|
1905
|
+
}): PxRect | null;
|
|
1906
|
+
|
|
1907
|
+
declare const DIGEST_VERSION = 1;
|
|
1908
|
+
interface DigestImageRef {
|
|
1909
|
+
full: string | null;
|
|
1910
|
+
crop: string | null;
|
|
1911
|
+
/** The crop's source box in FRAME px (what `crop` shows). */
|
|
1912
|
+
box: PxRect | null;
|
|
1913
|
+
fullSize?: {
|
|
1914
|
+
width: number;
|
|
1915
|
+
height: number;
|
|
1916
|
+
} | null;
|
|
1917
|
+
cropSize?: {
|
|
1918
|
+
width: number;
|
|
1919
|
+
height: number;
|
|
1920
|
+
} | null;
|
|
1921
|
+
}
|
|
1922
|
+
interface DigestTakeFacts {
|
|
1923
|
+
sourceDuration: number;
|
|
1924
|
+
outputDuration: number;
|
|
1925
|
+
width: number;
|
|
1926
|
+
height: number;
|
|
1927
|
+
captureWidth: number | null;
|
|
1928
|
+
captureHeight: number | null;
|
|
1929
|
+
frameWidth: number | null;
|
|
1930
|
+
frameHeight: number | null;
|
|
1931
|
+
surface: string;
|
|
1932
|
+
producer: string;
|
|
1933
|
+
pageUrl: string | null;
|
|
1934
|
+
pageTitle: string | null;
|
|
1935
|
+
hasMic: boolean;
|
|
1936
|
+
hasSystemAudio: boolean;
|
|
1937
|
+
hasCursor: boolean;
|
|
1938
|
+
windowFocusedFrac: number | null;
|
|
1939
|
+
}
|
|
1940
|
+
interface Digest {
|
|
1941
|
+
digestVersion: number;
|
|
1942
|
+
take: DigestTakeFacts;
|
|
1943
|
+
units: {
|
|
1944
|
+
source: 'seconds of footage';
|
|
1945
|
+
output: 'seconds of the rendered video (trims and speed applied)';
|
|
1946
|
+
focus: 'fractions of the video frame [0..1], the zoom cx/cy convention';
|
|
1947
|
+
activity: 'fraction of pixels that changed, per SOURCE second';
|
|
1948
|
+
};
|
|
1949
|
+
moments: (Moment & {
|
|
1950
|
+
full: string | null;
|
|
1951
|
+
crop: string | null;
|
|
1952
|
+
box: PxRect | null;
|
|
1953
|
+
})[];
|
|
1954
|
+
activity: number[] | null;
|
|
1955
|
+
plan: DigestPlan;
|
|
1956
|
+
doc: {
|
|
1957
|
+
manual: {
|
|
1958
|
+
zoom: number;
|
|
1959
|
+
speed: number;
|
|
1960
|
+
tilt: number;
|
|
1961
|
+
overlays: number;
|
|
1962
|
+
};
|
|
1963
|
+
zoomStyle: string | null;
|
|
1964
|
+
tiltStyle: string | null;
|
|
1965
|
+
};
|
|
1966
|
+
style: {
|
|
1967
|
+
from: string;
|
|
1968
|
+
fields: Record<string, unknown>;
|
|
1969
|
+
} | null;
|
|
1970
|
+
transcript: TranscriptSegment[] | null;
|
|
1971
|
+
images: {
|
|
1972
|
+
full: number;
|
|
1973
|
+
crop: number;
|
|
1974
|
+
sheet: string | null;
|
|
1975
|
+
tokensEstimateClaude: number;
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
interface BuildDigestInput {
|
|
1979
|
+
doc: ProjectDoc;
|
|
1980
|
+
plan: DigestPlan;
|
|
1981
|
+
moments: Moment[];
|
|
1982
|
+
outputDuration: number;
|
|
1983
|
+
bins: number[] | null;
|
|
1984
|
+
frame: {
|
|
1985
|
+
width: number;
|
|
1986
|
+
height: number;
|
|
1987
|
+
} | null;
|
|
1988
|
+
images: Map<string, DigestImageRef>;
|
|
1989
|
+
sheet: string | null;
|
|
1990
|
+
style?: {
|
|
1991
|
+
from: string;
|
|
1992
|
+
doc: ProjectDoc;
|
|
1993
|
+
} | null;
|
|
1994
|
+
transcript?: readonly TranscriptSegment[] | null;
|
|
1995
|
+
}
|
|
1996
|
+
declare function buildDigest(input: BuildDigestInput): Digest;
|
|
1997
|
+
/** OUTPUT seconds of a doc: its kept footage through the rate map. */
|
|
1998
|
+
declare function outputDurationOf(doc: ProjectDoc): number;
|
|
1999
|
+
|
|
2000
|
+
/** Map an OUTPUT-time range to SOURCE seconds through the doc's rate map. */
|
|
2001
|
+
declare function outputRangeToSource(doc: StudioDoc, t0: number, t1: number): {
|
|
2002
|
+
srcIn: number;
|
|
2003
|
+
srcOut: number;
|
|
2004
|
+
};
|
|
2005
|
+
/**
|
|
2006
|
+
* Re-rate a SOURCE range: spans overlapping it are trimmed (a span split in
|
|
2007
|
+
* two mints a fresh id for the second half), then a manual span at `rate`
|
|
2008
|
+
* covers the range. `rate: null` just clears — the 1× chip.
|
|
2009
|
+
*/
|
|
2010
|
+
declare function setSpeedInRange(spans: readonly SpeedSpan[], srcIn: number, srcOut: number, rate: number | null): SpeedSpan[];
|
|
2011
|
+
/** The Remove chip: drop every span the range touches, whole. */
|
|
2012
|
+
declare function removeSpeedInRange(spans: readonly SpeedSpan[], srcIn: number, srcOut: number): SpeedSpan[];
|
|
2013
|
+
/**
|
|
2014
|
+
* The Cut chip: subtract a SOURCE range from the kept segments. Segment
|
|
2015
|
+
* order (and any reorder) is preserved; a remainder below MIN_SEGMENT is
|
|
2016
|
+
* dropped with its parent. Never empties the take: cutting everything
|
|
2017
|
+
* returns the original list unchanged.
|
|
2018
|
+
*/
|
|
2019
|
+
declare function removeSourceRange(segments: readonly Segment[], srcIn: number, srcOut: number): Segment[];
|
|
2020
|
+
/**
|
|
2021
|
+
* The Zoom chip: a manual zoom span covering as much of the range as the
|
|
2022
|
+
* lane's non-overlap rule allows — clipped against existing spans, starting
|
|
2023
|
+
* at the first free moment inside the range. Null when the free room is
|
|
2024
|
+
* below the zoom floor (the chip greys out).
|
|
2025
|
+
*/
|
|
2026
|
+
declare function zoomSpanForRange(zoom: readonly ZoomSpan[], srcIn: number, srcOut: number): ZoomSpan | null;
|
|
2027
|
+
|
|
2028
|
+
/** Legacy defaults (= the default style's values); prefer FollowOptions. */
|
|
2029
|
+
declare const FOLLOW_SAFE_RATIO: number;
|
|
2030
|
+
declare const FOLLOW_RECENTER: number;
|
|
2031
|
+
interface FollowOptions {
|
|
2032
|
+
/** recenter when the cursor exits this central fraction of the crop. */
|
|
2033
|
+
safeRatio?: number;
|
|
2034
|
+
/** seconds the camera takes to glide to a recentered focus. */
|
|
2035
|
+
recenter?: number;
|
|
2036
|
+
/** target the cursor this many seconds ahead of the exit moment. */
|
|
2037
|
+
lookahead?: number;
|
|
2038
|
+
}
|
|
2039
|
+
interface FollowEvent {
|
|
2040
|
+
/** SOURCE seconds — the moment the recenter starts. */
|
|
2041
|
+
t: number;
|
|
2042
|
+
cx: number;
|
|
2043
|
+
cy: number;
|
|
2044
|
+
}
|
|
2045
|
+
declare function followFocusEvents(span: ZoomSpan, cursor: CursorTrack, space: {
|
|
2046
|
+
w: number;
|
|
2047
|
+
h: number;
|
|
2048
|
+
}, layout: CardLayout, options?: FollowOptions): {
|
|
2049
|
+
entry: {
|
|
2050
|
+
cx: number;
|
|
2051
|
+
cy: number;
|
|
2052
|
+
} | null;
|
|
2053
|
+
events: FollowEvent[];
|
|
2054
|
+
};
|
|
2055
|
+
|
|
2056
|
+
interface LoweredComposition {
|
|
2057
|
+
/** The composed config: the anchor's program plus the studio stack entry. */
|
|
2058
|
+
config: Record<string, unknown>;
|
|
2059
|
+
/** The MAIN program's ctx.data. */
|
|
2060
|
+
data: Record<string, unknown>;
|
|
2061
|
+
/** Each stack entry's own ctx.data, by entry id (`deps.stack` / `SET_DATA { target }`). */
|
|
2062
|
+
stack: Record<string, Record<string, unknown>>;
|
|
2063
|
+
/**
|
|
2064
|
+
* A program anchor's tween-timing overlay: delivered to the player
|
|
2065
|
+
* LIVE (`SET_TWEEN_EDITS`, bridge protocol 8) so a retime never changes
|
|
2066
|
+
* the program string. Absent on a recording, and on a stored (baked)
|
|
2067
|
+
* program config.
|
|
2068
|
+
*/
|
|
2069
|
+
tweenEdits?: readonly TimelineEdit[];
|
|
2070
|
+
/** Output duration in seconds (drives the carrier + SET_DURATION). */
|
|
2071
|
+
duration: number;
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* Zoom transition shape — deterministic pure keyframes evaluated by TL.sample
|
|
2075
|
+
* (no stateful springs, seek stays a pure function of t). The zoom-in ramp
|
|
2076
|
+
* starts BEFORE the span and lands rampInOverlap into it (the camera arrives
|
|
2077
|
+
* just after the moment it frames); the zoom-out starts at the span's end.
|
|
2078
|
+
* All timing/ease constants come from the doc's zoom STYLE (zoomStyle.ts —
|
|
2079
|
+
* named strategy presets grounded in the measured competitor comparison).
|
|
2080
|
+
* Eases are `css-bezier(…)` curves
|
|
2081
|
+
* (@vosjs/timeline >=0.4.0 — parsed identically by host + runtime bundle).
|
|
2082
|
+
*
|
|
2083
|
+
* Legacy constant names = the DEFAULT style's values, kept for scripts/tests
|
|
2084
|
+
* that reason about "the default ramp" symbolically.
|
|
2085
|
+
*/
|
|
2086
|
+
declare const ZOOM_RAMP_IN: number;
|
|
2087
|
+
declare const ZOOM_RAMP_IN_OVERLAP: number;
|
|
2088
|
+
declare const ZOOM_RAMP_OUT: number;
|
|
2089
|
+
/** Output-time gap ≤ this → pan straight to the next span (no zoom-out). */
|
|
2090
|
+
declare const ZOOM_CHAIN_GAP: number;
|
|
2091
|
+
/** Connected-zoom pan duration (compressed into short gaps). */
|
|
2092
|
+
declare const ZOOM_PAN: number;
|
|
2093
|
+
declare const ZOOM_EASE: string;
|
|
2094
|
+
declare const ZOOM_PAN_EASE: string;
|
|
2095
|
+
/**
|
|
2096
|
+
* Tilt transition shape (tilt spans). Fixed constants, deliberately NOT the
|
|
2097
|
+
* zoom style's (switching
|
|
2098
|
+
* "Camera style" must not silently change tilt feel; a tiltParams override
|
|
2099
|
+
* layer can arrive later if real use demands it). The eases are the same
|
|
2100
|
+
* measured css-bezier family the default zoom style uses. Unlike zoom, the
|
|
2101
|
+
* pose is SETTLED at span.in (the ramp starts TILT_RAMP_IN before, with no
|
|
2102
|
+
* overlap-into-span): tilt frames a moment, it doesn't chase content.
|
|
2103
|
+
*/
|
|
2104
|
+
declare const TILT_RAMP_IN = 0.9;
|
|
2105
|
+
declare const TILT_RAMP_OUT = 0.8;
|
|
2106
|
+
/** Output-time gap ≤ this → swing straight to the next pose (no flatten between). */
|
|
2107
|
+
declare const TILT_CHAIN_GAP = 1.35;
|
|
2108
|
+
/** Connected-tilt swing duration (compressed into short gaps). */
|
|
2109
|
+
declare const TILT_PAN = 0.9;
|
|
2110
|
+
declare const TILT_EASE: string;
|
|
2111
|
+
declare const TILT_PAN_EASE: string;
|
|
2112
|
+
/**
|
|
2113
|
+
* Cam-move transition shape (animated cam layouts). Own constants,
|
|
2114
|
+
* deliberately NOT the zoom style's or tilt's (switching another
|
|
2115
|
+
* subsystem's personality must never silently change the bubble's feel). The
|
|
2116
|
+
* premium band for an in-video layout morph is 0.6–1.0s (Descript's Smart
|
|
2117
|
+
* Transition defaults to 0.8s); the bubble is small chrome, so it sits at the
|
|
2118
|
+
* fast end. Like tilt, the pose is SETTLED at span.in (the ramp starts before,
|
|
2119
|
+
* no overlap-into-span): a cam move frames what follows.
|
|
2120
|
+
*/
|
|
2121
|
+
declare const CAM_RAMP_IN = 0.65;
|
|
2122
|
+
declare const CAM_RAMP_OUT = 0.65;
|
|
2123
|
+
/** Output-time gap ≤ this → morph straight to the next pose (no return to rest). */
|
|
2124
|
+
declare const CAM_CHAIN_GAP = 1.2;
|
|
2125
|
+
/** Connected-move morph duration (compressed into short gaps). */
|
|
2126
|
+
declare const CAM_PAN = 0.7;
|
|
2127
|
+
declare const CAM_EASE: string;
|
|
2128
|
+
declare const CAM_PAN_EASE: string;
|
|
2129
|
+
/**
|
|
2130
|
+
* The doc's kept spans with speed spans applied — the OUTPUT-time truth every
|
|
2131
|
+
* downstream consumer evaluates (mapTime in ON_FRAME, duration, zoom remap,
|
|
2132
|
+
* the export's audio splice). An empty segment list means "untrimmed", so
|
|
2133
|
+
* speed spans still apply over one synthesized full-source segment.
|
|
2134
|
+
*/
|
|
2135
|
+
declare function ratedSegments(doc: StudioDoc): Segment[];
|
|
2136
|
+
/**
|
|
2137
|
+
* Map a SOURCE-time span onto the output timeline through the RATED segment
|
|
2138
|
+
* list: the output extent of its KEPT footage (a partially-cut span snaps its
|
|
2139
|
+
* edges into kept footage; a fully-cut span returns null — it follows its
|
|
2140
|
+
* footage, like every source-anchored feature). Rate-aware: output positions
|
|
2141
|
+
* accumulate each piece's (out − in) / rate.
|
|
2142
|
+
*/
|
|
2143
|
+
declare function spanOutputExtent(segments: Segment[], sIn: number, sOut: number): {
|
|
2144
|
+
start: number;
|
|
2145
|
+
end: number;
|
|
2146
|
+
} | null;
|
|
2147
|
+
/**
|
|
2148
|
+
* Expand the doc's source-anchored zoom spans into a standard @vosjs/timeline
|
|
2149
|
+
* keyframe track in OUTPUT time (values are [level, cx, cy] vectors):
|
|
2150
|
+
*
|
|
2151
|
+
* rest ──ramp-in──▶ [level,cx,cy] ──hold──▶ span end ──ramp-out──▶ rest
|
|
2152
|
+
*
|
|
2153
|
+
* with one twist: when the output gap to the NEXT span is ≤ ZOOM_CHAIN_GAP,
|
|
2154
|
+
* the camera never returns to rest — it pans straight to the next span's
|
|
2155
|
+
* state over ZOOM_PAN and holds it through the gap (OpenScreen's connected
|
|
2156
|
+
* zooms: the camera glides from focus to focus). Transitions run in output
|
|
2157
|
+
* time, so they never straddle a cut; keyframe times are strictly increasing
|
|
2158
|
+
* (dense spans compress rather than reorder).
|
|
2159
|
+
*/
|
|
2160
|
+
/** A span enriched by the lowering with baked cursor-follow recenters. */
|
|
2161
|
+
interface LoweredZoomSpan extends ZoomSpan {
|
|
2162
|
+
followEvents?: FollowEvent[];
|
|
2163
|
+
}
|
|
2164
|
+
declare function zoomTrackFromDoc(zoom: LoweredZoomSpan[], segments: Segment[], style?: ZoomStyleParams): KeyframeTrack<number[]>;
|
|
2165
|
+
/**
|
|
2166
|
+
* Expand the doc's source-anchored tilt spans into an OUTPUT-time keyframe
|
|
2167
|
+
* track of [rx, ry] DEGREES (ON_FRAME converts to radians at the mesh):
|
|
2168
|
+
*
|
|
2169
|
+
* flat ──ramp-in──▶ [rx,ry] ──hold──▶ span end ──ramp-out──▶ flat
|
|
2170
|
+
*
|
|
2171
|
+
* Rest is FLAT: there is no static card pose to return to (decided
|
|
2172
|
+
* 2026-08-03 — a lean is a moment on the timeline), which makes this the
|
|
2173
|
+
* exact analog of zoom's level-1 rest. Deliberate
|
|
2174
|
+
* differences from zoomTrackFromDoc: the pose is SETTLED at
|
|
2175
|
+
* span.in (ramp starts TILT_RAMP_IN before, no overlap-into-span), there are
|
|
2176
|
+
* no follow events, and ramps are fixed constants rather than the zoom
|
|
2177
|
+
* style's. Spans ≤ TILT_CHAIN_GAP apart in output time swing pose-to-pose
|
|
2178
|
+
* without flattening between (the connected-zoom rule). Transitions run in
|
|
2179
|
+
* output time so they never straddle a cut; keyframe times are strictly
|
|
2180
|
+
* increasing (dense spans compress rather than reorder).
|
|
2181
|
+
*/
|
|
2182
|
+
declare function tiltTrackFromDoc(tilt: TiltSpan[], segments: Segment[], motion?: {
|
|
2183
|
+
rampIn?: number;
|
|
2184
|
+
rampOut?: number;
|
|
2185
|
+
chainGap?: number;
|
|
2186
|
+
pan?: number;
|
|
2187
|
+
}): KeyframeTrack<number[]>;
|
|
2188
|
+
/**
|
|
2189
|
+
* The bubble's rest pose as [x, y, size] frame fractions, resolved through the
|
|
2190
|
+
* SAME oracle the picking layer uses (camBubbleRect) so the corner math can
|
|
2191
|
+
* never fork a third way (draw / pick / lowering). Fractions are resolution-
|
|
2192
|
+
* stable at a fixed aspect: the margin (24·s) and diameter (size·H) both
|
|
2193
|
+
* scale with s = H/1080, so the fraction depends only on the aspect ratio.
|
|
2194
|
+
*/
|
|
2195
|
+
declare function camRestPose(cam: CamStyle, W: number, H?: number): number[];
|
|
2196
|
+
/**
|
|
2197
|
+
* Expand the doc's source-anchored cam pose spans into an OUTPUT-time keyframe
|
|
2198
|
+
* track of [x, y, size] frame fractions (the third consumer of the
|
|
2199
|
+
* span→track seam):
|
|
2200
|
+
*
|
|
2201
|
+
* rest ──ramp-in──▶ [x,y,size] ──hold──▶ span end ──ramp-out──▶ rest
|
|
2202
|
+
*
|
|
2203
|
+
* Rest is the doc's cam style resolved to fractions (camRestPose) — doc.cam IS
|
|
2204
|
+
* the rest pose, exactly as tilt's rest is flat. The pose is SETTLED at
|
|
2205
|
+
* span.in (ramp starts CAM_RAMP_IN before): a cam move frames what follows.
|
|
2206
|
+
* Spans ≤ CAM_CHAIN_GAP apart in output time morph pose-to-pose without
|
|
2207
|
+
* returning to rest (the connected-zoom rule). Absent pose fields inherit the
|
|
2208
|
+
* rest pose. Transitions run in output time so they never straddle a cut.
|
|
2209
|
+
*/
|
|
2210
|
+
declare function camTrackFromDoc(cam: CamStyle, spans: CamPoseSpan[], segments: Segment[], W: number, H?: number): KeyframeTrack<number[]>;
|
|
2211
|
+
/**
|
|
2212
|
+
* Pose fractions → the bubble square, mirroring ON_FRAME's pose branch (the
|
|
2213
|
+
* 40px floor and the rounded radius ride s, exactly like the static path).
|
|
2214
|
+
*/
|
|
2215
|
+
declare function camRectFromPose(pose: readonly number[], cam: CamStyle, W: number, H?: number): CamBubbleRect;
|
|
2216
|
+
/**
|
|
2217
|
+
* The bubble rect at OUTPUT time t — the time-aware picking oracle.
|
|
2218
|
+
* With no motion spans it is exactly camBubbleRect at the doc's design layout;
|
|
2219
|
+
* with spans it samples the SAME track the lowering ships, so picking can
|
|
2220
|
+
* never drift from the paint (camDraw.test.ts pins both paths). Design space
|
|
2221
|
+
* is docCardLayout's (H = 1080, W from the output aspect).
|
|
2222
|
+
*/
|
|
2223
|
+
declare function camBubbleRectAt(doc: ProjectDoc, t: number): CamBubbleRect;
|
|
2224
|
+
/** A resolved pose keyframe: clip-local time + full value vector. */
|
|
2225
|
+
interface MotionKey {
|
|
2226
|
+
at: number;
|
|
2227
|
+
value: number[];
|
|
2228
|
+
ease?: string;
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Bake resolved pose keyframes into a CLIP-LOCAL keyframe track.
|
|
2232
|
+
* The base vector holds until the first pose (a leading keyframe at 0 pins
|
|
2233
|
+
* it), values interpolate across each gap (ease-into per pose), and the last
|
|
2234
|
+
* pose holds to the clip's end (sample clamps). Same emitter, same
|
|
2235
|
+
* interpolator, same purity as the zoom/tilt/cam tracks.
|
|
2236
|
+
*/
|
|
2237
|
+
declare function motionTrack(base: readonly number[], keys: MotionKey[], dur: number): KeyframeTrack<number[]>;
|
|
2238
|
+
/** An overlay clip's base vector: [x, y, scale, rotation, opacityMul]. */
|
|
2239
|
+
declare function overlayMotionBase(o: OverlayClip): number[];
|
|
2240
|
+
/**
|
|
2241
|
+
* Effective [x, y, scale, rotation, opacityMul] of an overlay clip at
|
|
2242
|
+
* CLIP-LOCAL time t — the host-side mirror of ON_FRAME's sampling (the
|
|
2243
|
+
* picking layer substitutes it into the clip's transform so hit rects track
|
|
2244
|
+
* the animated element). Null = the clip has no motion.
|
|
2245
|
+
*/
|
|
2246
|
+
declare function overlayMotionPoseAt(o: OverlayClip, t: number): number[] | null;
|
|
2247
|
+
/** An object clip's base vector: [x, y, z, rx, ry, rz, scale]. */
|
|
2248
|
+
declare function objectMotionBase(o: ObjectClip): number[];
|
|
2249
|
+
/**
|
|
2250
|
+
* Effective [x, y, z, rx, ry, rz, scale] of an object clip at CLIP-LOCAL
|
|
2251
|
+
* time t (from the span start; 0 when span-less over `clipDur`). Null = the
|
|
2252
|
+
* clip has no motion. The 3D mirror of overlayMotionPoseAt.
|
|
2253
|
+
*/
|
|
2254
|
+
declare function objectMotionPoseAt(o: ObjectClip, t: number, clipDur: number): number[] | null;
|
|
2255
|
+
/**
|
|
2256
|
+
* The shared layers as the studio entry's data: overlay clips (presets resolved
|
|
2257
|
+
* to plain values HERE, ON_FRAME reads no registry), 3D props (numbers resolved
|
|
2258
|
+
* HERE), the extra font faces SETUP awaits. Every key is omitted when its layer
|
|
2259
|
+
* is absent — data byte parity for docs that never touched it. Both anchors
|
|
2260
|
+
* call this with their own output duration.
|
|
2261
|
+
*/
|
|
2262
|
+
declare function studioLayerData(layers: {
|
|
2263
|
+
overlays?: OverlayClip[];
|
|
2264
|
+
objects?: ObjectClip[];
|
|
2265
|
+
audio?: AudioClip[];
|
|
2266
|
+
}, duration: number): Record<string, unknown>;
|
|
2267
|
+
declare function lowerToComposition(doc: ProjectDoc): LoweredComposition;
|
|
2268
|
+
|
|
2269
|
+
/**
|
|
2270
|
+
* ONE lowering for the document family: the anchor's program
|
|
2271
|
+
* plus the studio stack entry, on every anchor.
|
|
2272
|
+
*
|
|
2273
|
+
* - A recording lowers to its card program (`lowerToComposition`), which
|
|
2274
|
+
* already carries the entry.
|
|
2275
|
+
* - A program lowers to the user's config, COMPLETE and untouched (the
|
|
2276
|
+
* execution IR, D1) with its tween-timing overlay baked into
|
|
2277
|
+
* `createTimeline`, plus the same entry carrying the shared layers. Params
|
|
2278
|
+
* and Looks ride the config as authored. `retime` arrives with speed spans
|
|
2279
|
+
* absent, the engine's identity is the identity.
|
|
2280
|
+
*
|
|
2281
|
+
* The composed config is what the platform stores for a layered program and
|
|
2282
|
+
* what the fleet compiles; the player runs it MINUS every data object (the
|
|
2283
|
+
* structural hash), with `data` and `stack` delivered live.
|
|
2284
|
+
*/
|
|
2285
|
+
declare function lowerStudioDoc(doc: StudioDoc, opts?: LowerProgramOptions): LoweredComposition;
|
|
2286
|
+
interface LowerProgramOptions {
|
|
2287
|
+
/**
|
|
2288
|
+
* Bake the tween overlay into `createTimeline` (the STORED composed
|
|
2289
|
+
* config: the fleet, the watch page and `vos render` have no bridge to
|
|
2290
|
+
* hand an overlay to). Off by default: the player runs the user's
|
|
2291
|
+
* timeline and retimes it live, so the program string is constant
|
|
2292
|
+
* across every timing edit.
|
|
2293
|
+
*/
|
|
2294
|
+
bake?: boolean;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
/**
|
|
2298
|
+
* `config.retime` for a program with speed spans: output time →
|
|
2299
|
+
* program time through the RATED segments on `data.retime` (the same map
|
|
2300
|
+
* `mapTime` performs; inlined so the config stays self-contained, tested
|
|
2301
|
+
* against it). Reads data live, so a rate edit is SET_DATA, never a LOAD.
|
|
2302
|
+
*/
|
|
2303
|
+
declare const PROGRAM_RETIME = "(t, data) => {\n var s = data && data.retime\n if (!s || !s.length) return t\n var acc = 0\n for (var i = 0; i < s.length; i++) {\n var r = s[i].rate && s[i].rate > 0 ? s[i].rate : 1\n var d = (s[i].out - s[i].in) / r\n if (t < acc + d) return s[i].in + (t - acc) * r\n acc += d\n }\n return s[s.length - 1].out\n}";
|
|
2304
|
+
/**
|
|
2305
|
+
* With speed spans the OUTPUT length is not the program's own, but the
|
|
2306
|
+
* engine hands ONE \`duration\` to both the clock and \`createTimeline\`. The
|
|
2307
|
+
* composed config's \`duration\` is the output length (the clock, the fleet's
|
|
2308
|
+
* render length); this wrapper hands the user's function the program's own
|
|
2309
|
+
* length from \`data.programDuration\` — data, so a rate edit stays live.
|
|
2310
|
+
*/
|
|
2311
|
+
declare function wrapProgramLength(source: string): string;
|
|
2312
|
+
declare function lowerProgramDoc(doc: ProgramAnchorDoc, opts?: LowerProgramOptions): LoweredComposition;
|
|
2313
|
+
|
|
2314
|
+
/**
|
|
2315
|
+
* A destination is where a release's media goes — the output twin of the
|
|
2316
|
+
* doors registry's "a door is what you bring". One row per channel asset,
|
|
2317
|
+
* derived from the verified channel specs; `vos deliver` loops these and
|
|
2318
|
+
* the kit manifest records them.
|
|
2319
|
+
*/
|
|
2320
|
+
interface Destination {
|
|
2321
|
+
/** `${channel}-${asset}` — the id `vos deliver --to` and kit.json use. */
|
|
2322
|
+
id: string;
|
|
2323
|
+
channel: string;
|
|
2324
|
+
asset: string;
|
|
2325
|
+
label: string;
|
|
2326
|
+
kind: 'video' | 'still' | 'still-set';
|
|
2327
|
+
/** Reduced aspect ratio, the exportSizeFor convention. */
|
|
2328
|
+
ratio: string;
|
|
2329
|
+
px: {
|
|
2330
|
+
w: number;
|
|
2331
|
+
h: number;
|
|
2332
|
+
};
|
|
2333
|
+
/** still-set only: how many the channel takes. */
|
|
2334
|
+
count?: {
|
|
2335
|
+
min: number;
|
|
2336
|
+
max: number;
|
|
2337
|
+
};
|
|
2338
|
+
/**
|
|
2339
|
+
* Image genre: 'screenshot' = real UI from the take (store policy demands
|
|
2340
|
+
* real UX); 'card' = a COMPOSED cover — rendered from the maker's poster
|
|
2341
|
+
* program when `vos deliver --poster` has one.
|
|
2342
|
+
*/
|
|
2343
|
+
genre?: 'screenshot' | 'card';
|
|
2344
|
+
minSeconds?: number;
|
|
2345
|
+
maxSeconds?: number;
|
|
2346
|
+
maxBytes?: number;
|
|
2347
|
+
/** The format the kit renders. */
|
|
2348
|
+
format: 'mp4' | 'png';
|
|
2349
|
+
/** What the channel accepts, in the spec's own words. */
|
|
2350
|
+
accepts: string;
|
|
2351
|
+
/** How footage meets an off-ratio frame (a still fills, a video letterboxes). */
|
|
2352
|
+
fit: 'contain' | 'cover';
|
|
2353
|
+
notes: string;
|
|
2354
|
+
}
|
|
2355
|
+
declare const CHANNEL_SPECS_VERIFIED = "2026-08-04";
|
|
2356
|
+
declare const CHANNEL_SPECS_HASH = "7a3ad4301f96507ec471a35fa37f367dcaf4bd405b008f551d0ac4cd395d61f7";
|
|
2357
|
+
declare const DESTINATIONS: Destination[];
|
|
2358
|
+
declare function destinationById(id: string): Destination | undefined;
|
|
2359
|
+
declare function destinationsForChannel(channel: string): Destination[];
|
|
2360
|
+
|
|
2361
|
+
/**
|
|
2362
|
+
* The studio's program: the SHARED layers (text/image/video overlay clips, the
|
|
2363
|
+
* 3D prop pool) as ONE engine stack entry (`config.stack`, @vosjs/core ≥0.21)
|
|
2364
|
+
* that runs after the anchor's program on the same ctx — same scene, camera,
|
|
2365
|
+
* overlayScene, renderer, master clock — with its OWN `ctx.data` and its own
|
|
2366
|
+
* error boundary. The same entry rides every anchor: a recording's card
|
|
2367
|
+
* program and a user's own config alike.
|
|
2368
|
+
*
|
|
2369
|
+
* Everything here is CONSTANT text: a layer edit is `SET_DATA { target }` on
|
|
2370
|
+
* this entry, never a program change (the liveEdit invariant). The paint code
|
|
2371
|
+
* is the take editor's compositor, moved out of its main program unchanged;
|
|
2372
|
+
* it reads only what an entry is given — the renderer size, the output
|
|
2373
|
+
* clock (`ctx.time` on an entry IS the output time), the shared
|
|
2374
|
+
* `window.__vos__` caches and `globalThis.__vosTimeline` — never the anchor's
|
|
2375
|
+
* card geometry.
|
|
2376
|
+
*
|
|
2377
|
+
* The overlay layer mounts in `ctx.overlayScene` (the engine's 2D group,
|
|
2378
|
+
* rendered after every 3D group under the ortho `overlayCamera`), sized to that
|
|
2379
|
+
* camera's bounds, so it fills the frame on any anchor whatever its camera.
|
|
2380
|
+
* Props mount in `ctx.scene` at renderOrder 1.5 (between a recording's card and
|
|
2381
|
+
* its cam bubble) on the ANCHOR's camera: a perspective camera anywhere, or an
|
|
2382
|
+
* orthographic one (a program's `fullscreen` preset), where the prop group
|
|
2383
|
+
* carries the camera pose and a pixel-aspect squash. Lights: the entry adds
|
|
2384
|
+
* its pair when its data says `lights` (the recording anchor), and lazily,
|
|
2385
|
+
* once, when a program's scene turns out to have none (a shader program).
|
|
2386
|
+
*/
|
|
2387
|
+
declare const STUDIO_ENTRY_ID = "vosso.studio";
|
|
2388
|
+
interface StudioEntry {
|
|
2389
|
+
id: string;
|
|
2390
|
+
data: Record<string, unknown>;
|
|
2391
|
+
setup: string;
|
|
2392
|
+
createContent: string;
|
|
2393
|
+
onFrame: string;
|
|
2394
|
+
}
|
|
2395
|
+
declare function studioEntry(data: Record<string, unknown>): StudioEntry;
|
|
2396
|
+
|
|
2397
|
+
interface EnvelopePoint {
|
|
2398
|
+
/** output-timeline seconds. */
|
|
2399
|
+
t: number;
|
|
2400
|
+
/** linear gain 0..1. */
|
|
2401
|
+
g: number;
|
|
2402
|
+
}
|
|
2403
|
+
declare function clipEnvelope(clip: Pick<AudioClip, 'start' | 'in' | 'out' | 'gain' | 'fadeIn' | 'fadeOut' | 'loop' | 'loopLen'>): EnvelopePoint[];
|
|
2404
|
+
/** Envelope value at output time `t` (linear interpolation; 0 outside the clip). */
|
|
2405
|
+
declare function envelopeValueAt(env: EnvelopePoint[], t: number): number;
|
|
2406
|
+
|
|
2407
|
+
/**
|
|
2408
|
+
* The studio's audio clips as an ENGINE audio plan:
|
|
2409
|
+
* the shape `@vosjs/core/audio`'s `mixAudio` renders. One builder for every
|
|
2410
|
+
* export path (the device exporter, the fleet's audio page) from the same
|
|
2411
|
+
* lowered data the preview scheduler plays, so what you hear is what exports.
|
|
2412
|
+
*
|
|
2413
|
+
* A clip is OUTPUT-anchored: it plays from `start` for `len` seconds, reading
|
|
2414
|
+
* the source from `in` (looping over `[in, out]` when `loop`), at its gain
|
|
2415
|
+
* envelope (`env`, absolute output seconds, fades included) times the duck
|
|
2416
|
+
* curve when it ducks. The plan samples that at `step` (240/s, the engine's
|
|
2417
|
+
* default); the mixer interpolates between points and treats the loop's
|
|
2418
|
+
* wrap as a seek.
|
|
2419
|
+
*
|
|
2420
|
+
* Structurally typed: the lowered clip, not the doc — this runs on the fleet
|
|
2421
|
+
* from stored config data as well as in the studio.
|
|
2422
|
+
*/
|
|
2423
|
+
interface LoweredAudioClip {
|
|
2424
|
+
key: string;
|
|
2425
|
+
start: number;
|
|
2426
|
+
in: number;
|
|
2427
|
+
out: number;
|
|
2428
|
+
gain: number;
|
|
2429
|
+
loop: boolean;
|
|
2430
|
+
len: number;
|
|
2431
|
+
duck: boolean;
|
|
2432
|
+
env: EnvelopePoint[];
|
|
2433
|
+
}
|
|
2434
|
+
interface AudioPlanPoint {
|
|
2435
|
+
t: number;
|
|
2436
|
+
on: boolean;
|
|
2437
|
+
pos: number;
|
|
2438
|
+
gain: number;
|
|
2439
|
+
}
|
|
2440
|
+
interface AudioPlanTrack {
|
|
2441
|
+
id: string;
|
|
2442
|
+
src: string;
|
|
2443
|
+
loop: boolean;
|
|
2444
|
+
points: AudioPlanPoint[];
|
|
2445
|
+
}
|
|
2446
|
+
interface StudioAudioPlan {
|
|
2447
|
+
duration: number;
|
|
2448
|
+
step: number;
|
|
2449
|
+
tracks: AudioPlanTrack[];
|
|
2450
|
+
}
|
|
2451
|
+
declare const AUDIO_PLAN_STEP: number;
|
|
2452
|
+
/** Linear interpolation over absolute-time envelope points; `def` outside an empty one. */
|
|
2453
|
+
declare function envelopeAt(env: readonly EnvelopePoint[], t: number, def: number): number;
|
|
2454
|
+
declare function studioAudioPlan(clips: readonly LoweredAudioClip[], duckEnv: readonly EnvelopePoint[], duration: number, step?: number): StudioAudioPlan;
|
|
2455
|
+
|
|
2456
|
+
/**
|
|
2457
|
+
* Compositor v2 — the layer stage geometry.
|
|
2458
|
+
*
|
|
2459
|
+
* The stage turns the studio's single fullscreen-ortho quad into a three-layer mesh stack
|
|
2460
|
+
* under ONE perspective camera:
|
|
2461
|
+
*
|
|
2462
|
+
* overlay quad screen-space cam bubble + text/image/video overlays
|
|
2463
|
+
* card mesh world-space the existing 2D card painting, on a plane
|
|
2464
|
+
* that can TILT (doc.tilt spans)
|
|
2465
|
+
* background quad screen-space CSS fill + vos background loop
|
|
2466
|
+
*
|
|
2467
|
+
* Every layer is a plane placed perpendicular to the camera axis and centered
|
|
2468
|
+
* on it, sized to exactly fill the camera frustum at its depth. Perpendicular +
|
|
2469
|
+
* centered + frustum-filling ⇒ it projects to the full viewport regardless of
|
|
2470
|
+
* depth, so the background/overlay read as flat screen-space and the CARD, at
|
|
2471
|
+
* `tilt = 0`, projects PIXEL-IDENTICALLY to today's ortho fullscreen quad. Only
|
|
2472
|
+
* the card ever rotates; the perspective camera then gives it real
|
|
2473
|
+
* foreshortening (an ortho camera would only skew it).
|
|
2474
|
+
*
|
|
2475
|
+
* These are pure helpers (no THREE dependency) so the host can mirror the exact
|
|
2476
|
+
* projection the runtime draws with — the world-unit basis that
|
|
2477
|
+
* on-canvas picking builds on (host picks / instance renders). `stage.test.ts`
|
|
2478
|
+
* pins the math; the runtime (lowerToComposition CREATE_CONTENT/ON_FRAME) must
|
|
2479
|
+
* use these SAME constants — change them together.
|
|
2480
|
+
*/
|
|
2481
|
+
/**
|
|
2482
|
+
* Camera field of view (degrees). Deliberately gentle (telephoto-ish product
|
|
2483
|
+
* shot) so a card tilt reads as a premium 3D lean, not a fisheye warp. Parity
|
|
2484
|
+
* at tilt = 0 is INDEPENDENT of this value (every layer is sized to fill the
|
|
2485
|
+
* frustum), so it is a pure aesthetic dial for how dramatic tilt looks.
|
|
2486
|
+
*/
|
|
2487
|
+
declare const CARD_FOV = 30;
|
|
2488
|
+
/**
|
|
2489
|
+
* Layer depths (world units in front of a camera at the origin looking down
|
|
2490
|
+
* −z). Absolute values are arbitrary — only the ORDER matters (painter's order
|
|
2491
|
+
* is set by renderOrder, not depth) and that near/far bracket them. The card
|
|
2492
|
+
* sits between the background (behind) and the overlay (in front).
|
|
2493
|
+
*/
|
|
2494
|
+
declare const OVERLAY_Z = -2;
|
|
2495
|
+
declare const CARD_Z = -4;
|
|
2496
|
+
declare const BACKGROUND_Z = -6;
|
|
2497
|
+
declare const CAMERA_NEAR = 0.1;
|
|
2498
|
+
declare const CAMERA_FAR = 100;
|
|
2499
|
+
interface PlaneSize {
|
|
2500
|
+
width: number;
|
|
2501
|
+
height: number;
|
|
2502
|
+
}
|
|
2503
|
+
/**
|
|
2504
|
+
* The world-space size of a plane that exactly fills a perspective camera's
|
|
2505
|
+
* frustum at `|distance|` in front of it. Height subtends the full vertical FOV;
|
|
2506
|
+
* width follows the viewport aspect. This is the one sizing primitive the whole
|
|
2507
|
+
* stack shares — every layer plane, and the host-side projection basis for
|
|
2508
|
+
* picking, derive from it.
|
|
2509
|
+
*/
|
|
2510
|
+
declare function planeSizeAtDepth(distance: number, fovDeg: number, aspect: number): PlaneSize;
|
|
2511
|
+
/**
|
|
2512
|
+
* Project a point on the (untilted) card plane, given in normalized card-canvas
|
|
2513
|
+
* coordinates (u, v ∈ [0,1], v measured from the TOP like a canvas), to
|
|
2514
|
+
* normalized screen coordinates (sx, sy ∈ [0,1], sy from the top). At tilt = 0
|
|
2515
|
+
* this is the identity — the card fills the viewport — so it is exact for the
|
|
2516
|
+
* common case and the basis the tilt/camera matrices extend for
|
|
2517
|
+
* on-canvas picking. Kept here so host and runtime never disagree on
|
|
2518
|
+
* where the card is.
|
|
2519
|
+
*/
|
|
2520
|
+
declare function cardPointToScreen(u: number, v: number): {
|
|
2521
|
+
sx: number;
|
|
2522
|
+
sy: number;
|
|
2523
|
+
};
|
|
2524
|
+
|
|
2525
|
+
/** Baked pill geometry: design px at the clip's resolved font size. */
|
|
2526
|
+
interface ResolvedOverlayBox {
|
|
2527
|
+
color: string;
|
|
2528
|
+
opacity: number;
|
|
2529
|
+
/** Paddings/radius in design px (em multiples × resolved size). */
|
|
2530
|
+
padX: number;
|
|
2531
|
+
padY: number;
|
|
2532
|
+
radius: number;
|
|
2533
|
+
}
|
|
2534
|
+
/**
|
|
2535
|
+
* Resolve a clip's background pill (null when absent). Mirrored by
|
|
2536
|
+
* `overlayRect`'s inflation and ON_FRAME's pill draw — change together.
|
|
2537
|
+
*/
|
|
2538
|
+
declare function resolveOverlayBox(clip: TextOverlayClip): ResolvedOverlayBox | null;
|
|
2539
|
+
/** A preset's base values (the 5-field house style). */
|
|
2540
|
+
interface OverlayPresetStyle {
|
|
2541
|
+
/** Full CSS font-family stack (primary + fallbacks). */
|
|
2542
|
+
stack: string;
|
|
2543
|
+
weight: number;
|
|
2544
|
+
/** Font size in design px (H = 1080 space), before transform.scale. */
|
|
2545
|
+
size: number;
|
|
2546
|
+
color: string;
|
|
2547
|
+
/** Legibility shadow strength 0..1 (0 = none). */
|
|
2548
|
+
shadow: number;
|
|
2549
|
+
}
|
|
2550
|
+
/** Preset base + the full override surface, resolved to concrete values. */
|
|
2551
|
+
interface ResolvedOverlayStyle extends OverlayPresetStyle {
|
|
2552
|
+
fontStyle: 'normal' | 'italic';
|
|
2553
|
+
align: 'left' | 'center' | 'right';
|
|
2554
|
+
/** Design px at the resolved size. */
|
|
2555
|
+
letterSpacing: number;
|
|
2556
|
+
/** Multiplier (default OVERLAY_LINE_HEIGHT). */
|
|
2557
|
+
lineHeight: number;
|
|
2558
|
+
stroke: TextOverlayStroke | null;
|
|
2559
|
+
}
|
|
2560
|
+
/** The house text styles. Sizes in design px; colors are ink-on-footage. */
|
|
2561
|
+
declare const TEXT_PRESETS: Record<TextOverlayPreset, OverlayPresetStyle>;
|
|
2562
|
+
/** Size override bounds (design px) — same range the inspector slider offers. */
|
|
2563
|
+
declare const OVERLAY_SIZE_MIN = 12;
|
|
2564
|
+
declare const OVERLAY_SIZE_MAX = 200;
|
|
2565
|
+
/**
|
|
2566
|
+
* woff2 faces SETUP preloads when the doc has overlays (latin subset only —
|
|
2567
|
+
* overlay text is product UI copy). URLs are the self-hosted catalog on
|
|
2568
|
+
* assets.vos.so; studio-core stays dependency-free, so the three base faces
|
|
2569
|
+
* are literals — keep them within the catalog `@vosjs/shared` hosts.
|
|
2570
|
+
*/
|
|
2571
|
+
declare const OVERLAY_FONT_FACES: {
|
|
2572
|
+
family: string;
|
|
2573
|
+
weight: number;
|
|
2574
|
+
url: string;
|
|
2575
|
+
}[];
|
|
2576
|
+
/** Preset + per-clip overrides → the concrete style baked into ctx.data. */
|
|
2577
|
+
declare function resolveOverlayStyle(clip: TextOverlayClip): ResolvedOverlayStyle;
|
|
2578
|
+
interface OverlayFontFace {
|
|
2579
|
+
family: string;
|
|
2580
|
+
weight: number;
|
|
2581
|
+
url: string;
|
|
2582
|
+
}
|
|
2583
|
+
/**
|
|
2584
|
+
* The hosted face a clip's family/weight overrides resolve to, when it is
|
|
2585
|
+
* NOT one of the three base preset faces (null otherwise — parity: preset
|
|
2586
|
+
* clips carry nothing). Baked per-overlay so ON_FRAME can lazy-load it on a
|
|
2587
|
+
* live style edit (SET_DATA never re-runs SETUP); SETUP awaits the full list
|
|
2588
|
+
* from ctx.data on cold load, which is what export parity rides on.
|
|
2589
|
+
*/
|
|
2590
|
+
declare function overlayFaceFor(clip: TextOverlayClip): OverlayFontFace | null;
|
|
2591
|
+
/**
|
|
2592
|
+
* Every woff2 face a doc's overlays need (SETUP await on cold load, and the
|
|
2593
|
+
* host document for measurement): the three base preset faces — ALWAYS, byte
|
|
2594
|
+
* parity for preset-only docs — plus one face per override.
|
|
2595
|
+
*/
|
|
2596
|
+
declare function overlayFontFaces(doc: Pick<ProjectDoc, 'overlays'>): OverlayFontFace[];
|
|
2597
|
+
declare function overlayLines(text: string): string[];
|
|
2598
|
+
/**
|
|
2599
|
+
* The canvas font string at a given comp scale — MIRRORS ON_FRAME's
|
|
2600
|
+
* `olW + ' ' + olPx + 'px ' + olStack` (pinned by test).
|
|
2601
|
+
*/
|
|
2602
|
+
declare function overlayFontString(style: ResolvedOverlayStyle, scale: number, s: number): string;
|
|
2603
|
+
interface OverlayRect {
|
|
2604
|
+
/** Center-anchored box in DESIGN px (pre-rotation). */
|
|
2605
|
+
cx: number;
|
|
2606
|
+
cy: number;
|
|
2607
|
+
w: number;
|
|
2608
|
+
h: number;
|
|
2609
|
+
/** Rotation in degrees (the caller rotates points into local space to hit-test). */
|
|
2610
|
+
rotation: number;
|
|
2611
|
+
}
|
|
2612
|
+
/**
|
|
2613
|
+
* The drawn bounding box of a text overlay in DESIGN px — the picking
|
|
2614
|
+
* geometry. `measure(text, font)` returns the text width in px for a font
|
|
2615
|
+
* string (the host passes a scratch-canvas measureText; tests stub it).
|
|
2616
|
+
* `frameW`/`frameH` are the design frame size (docCardLayout's W/H) — the
|
|
2617
|
+
* clip's transform.x/y are FRACTIONS of the frame, so the anchor is
|
|
2618
|
+
* x·frameW / y·frameH. Measured at s = 1 (design space), so the result maps
|
|
2619
|
+
* to the canvas by ·s and to CSS by the player's display scale.
|
|
2620
|
+
*/
|
|
2621
|
+
declare function overlayRect(clip: OverlayClip, measure: (text: string, font: string, letterSpacingPx?: number) => number, frameW: number, frameH?: number,
|
|
2622
|
+
/** Media kinds: natural aspect (w/h) once known — null/absent = assume 16:9. */
|
|
2623
|
+
mediaAspect?: number | null): OverlayRect;
|
|
2624
|
+
/** Point-in-overlay test (design px), rotation-aware (point → local space). */
|
|
2625
|
+
declare function overlayHit(rect: OverlayRect, px: number, py: number, padPx?: number): boolean;
|
|
2626
|
+
|
|
2627
|
+
declare const TEXT3D_DEPTH_DEFAULT = 0.25;
|
|
2628
|
+
declare const TEXT3D_DEPTH_MIN = 0.02;
|
|
2629
|
+
declare const TEXT3D_DEPTH_MAX = 1;
|
|
2630
|
+
interface BakedText3dMaterial {
|
|
2631
|
+
/** THREE constructor family: MeshStandardMaterial | MeshPhysicalMaterial. */
|
|
2632
|
+
type: 'standard' | 'physical';
|
|
2633
|
+
params: Record<string, unknown>;
|
|
2634
|
+
}
|
|
2635
|
+
interface BakedText3dAsset {
|
|
2636
|
+
kind: 'text3d';
|
|
2637
|
+
text: string;
|
|
2638
|
+
/** Resolved typeface JSON URL (assets.vos.so — the fleet's one origin). */
|
|
2639
|
+
url: string;
|
|
2640
|
+
/** Extrusion depth as a fraction of the glyph height. */
|
|
2641
|
+
depth: number;
|
|
2642
|
+
bevel: boolean;
|
|
2643
|
+
mat: BakedText3dMaterial;
|
|
2644
|
+
}
|
|
2645
|
+
/** Normalize a doc text3d asset into the baked payload (pure, deterministic). */
|
|
2646
|
+
declare function resolveText3dAsset(asset: Extract<ObjectAsset, {
|
|
2647
|
+
kind: 'text3d';
|
|
2648
|
+
}>): BakedText3dAsset;
|
|
2649
|
+
|
|
2650
|
+
/**
|
|
2651
|
+
* Idle cursor fade — the dwell detector behind `CursorStyle.hideWhenIdle`.
|
|
2652
|
+
*
|
|
2653
|
+
* A parked cursor is the most common blemish in screen footage: the dot sits in
|
|
2654
|
+
* frame through every scroll, every typing passage, every pause, drawing the eye
|
|
2655
|
+
* to nothing. This bakes a sparse opacity curve so ON_FRAME can fade it out
|
|
2656
|
+
* during dwells and bring it back the moment the cursor moves again.
|
|
2657
|
+
*
|
|
2658
|
+
* Pure and deterministic — seek must stay a pure function of `t`, so there are
|
|
2659
|
+
* no springs and no state. Output is SOURCE-anchored (like the cursor samples
|
|
2660
|
+
* themselves), which is what makes trims, cuts and speed spans inherit the fade
|
|
2661
|
+
* from one seam.
|
|
2662
|
+
*
|
|
2663
|
+
* Detection runs on the RAW track, not the smoothed path. The smoothed path is
|
|
2664
|
+
* resampled at a fixed cadence and linearly interpolates across gaps, so during
|
|
2665
|
+
* a park it creeps toward wherever the cursor goes next — ground truth for "the
|
|
2666
|
+
* user isn't moving" is the absence of raw events. The smoothing settle after
|
|
2667
|
+
* the last real move is bounded and well under `CURSOR_IDLE_HOLD`, so the dot
|
|
2668
|
+
* is always at rest before the fade begins.
|
|
2669
|
+
*
|
|
2670
|
+
* Two things deliberately do NOT count as movement:
|
|
2671
|
+
*
|
|
2672
|
+
* - **Scrolling.** `scroll` events re-emit the last known position, so a reading
|
|
2673
|
+
* pause looks "active" if you detect idleness from sample gaps. They carry no
|
|
2674
|
+
* real cursor motion and are ignored here, so a long scroll correctly fades
|
|
2675
|
+
* the cursor away — it isn't doing anything.
|
|
2676
|
+
* - **Focus jumps and typing.** `focus`/`key` events synthesize a position at
|
|
2677
|
+
* the focused element's centre, which the cursor never visited — and during
|
|
2678
|
+
* typing the caret is the actor, so the parked dot SHOULD fade.
|
|
2679
|
+
*
|
|
2680
|
+
* Clicks DO break a dwell: a press is not idle. The window before a click ends
|
|
2681
|
+
* `CURSOR_IDLE_FADE_IN` early so the dot is back at full opacity when the ring
|
|
2682
|
+
* blooms under it, rather than ghosting in behind its own click effect.
|
|
2683
|
+
*/
|
|
2684
|
+
|
|
2685
|
+
/** Seconds of stillness before the cursor starts fading out. */
|
|
2686
|
+
declare const CURSOR_IDLE_HOLD = 1;
|
|
2687
|
+
/** Fade-out ramp, seconds. */
|
|
2688
|
+
declare const CURSOR_IDLE_FADE_OUT = 0.35;
|
|
2689
|
+
/** Fade-in ramp, seconds. Snappier than the way out — motion draws the eye. */
|
|
2690
|
+
declare const CURSOR_IDLE_FADE_IN = 0.18;
|
|
2691
|
+
/**
|
|
2692
|
+
* Movement epsilon as a fraction of the capture's short edge, floored at 2px
|
|
2693
|
+
* (the recorder's own distance gate). Relative so a 4K take isn't held to a
|
|
2694
|
+
* 1080p take's pixel budget.
|
|
2695
|
+
*/
|
|
2696
|
+
declare const CURSOR_IDLE_EPS_FRAC = 0.0025;
|
|
2697
|
+
interface CursorIdleOptions {
|
|
2698
|
+
/** Capture space, for the movement epsilon. */
|
|
2699
|
+
space: {
|
|
2700
|
+
w: number;
|
|
2701
|
+
h: number;
|
|
2702
|
+
};
|
|
2703
|
+
/** SOURCE seconds; the trailing dwell runs to here. */
|
|
2704
|
+
sourceDuration: number;
|
|
2705
|
+
}
|
|
2706
|
+
/** One point on the baked opacity curve. SOURCE seconds → alpha 0..1. */
|
|
2707
|
+
interface CursorFadeKey {
|
|
2708
|
+
t: number;
|
|
2709
|
+
a: number;
|
|
2710
|
+
}
|
|
2711
|
+
/**
|
|
2712
|
+
* Bake the opacity curve for a cursor track. Returns an empty array when
|
|
2713
|
+
* nothing dwells long enough to be worth hiding — callers then emit no
|
|
2714
|
+
* `cursorFade` key at all, so a take with a busy cursor lowers byte-identically
|
|
2715
|
+
* to the pre-feature lowering.
|
|
2716
|
+
*/
|
|
2717
|
+
declare function cursorIdleFade(track: CursorTrack, o: CursorIdleOptions): CursorFadeKey[];
|
|
2718
|
+
|
|
2719
|
+
/** Anticipation lead — effects start this many output seconds before the click. */
|
|
2720
|
+
declare const CLICK_FX_PRE = 0.06;
|
|
2721
|
+
/** Base effect durations in output seconds (× the intensity's `dur`). */
|
|
2722
|
+
declare const CLICK_RIPPLE_DUR = 0.45;
|
|
2723
|
+
declare const CLICK_PULSE_DUR = 0.35;
|
|
2724
|
+
declare const CLICK_HIGHLIGHT_FADE = 0.35;
|
|
2725
|
+
/** Synthetic press length when the matching `up` is missing (nav killed it). */
|
|
2726
|
+
declare const CLICK_SYNTH_RELEASE = 0.12;
|
|
2727
|
+
/** A down→up pair longer than this is treated as unmatched (lost `up`). */
|
|
2728
|
+
declare const CLICK_PAIR_MAX = 10;
|
|
2729
|
+
/** Highlight uses the element rect only when it covers ≤ this viewport fraction. */
|
|
2730
|
+
declare const CLICK_RECT_MAX_FRAC = 0.35;
|
|
2731
|
+
interface LoweredClick {
|
|
2732
|
+
/** OUTPUT seconds of mousedown. */
|
|
2733
|
+
ot: number;
|
|
2734
|
+
/** OUTPUT seconds of release (real up, clamped into kept footage). */
|
|
2735
|
+
up: number;
|
|
2736
|
+
/** SOURCE seconds of mousedown — ON_FRAME's cross-cut proximity guard. */
|
|
2737
|
+
st: number;
|
|
2738
|
+
/** click point in cursorSpace px. */
|
|
2739
|
+
x: number;
|
|
2740
|
+
y: number;
|
|
2741
|
+
/** pointer button (0=left). */
|
|
2742
|
+
b: number;
|
|
2743
|
+
/** element rect [x,y,w,h] in cursorSpace px — present only when the
|
|
2744
|
+
* highlight style wants it AND it passed the size/containment gates
|
|
2745
|
+
* (ON_FRAME stays branch-light: r present = draw highlight). */
|
|
2746
|
+
r?: [number, number, number, number];
|
|
2747
|
+
}
|
|
2748
|
+
interface ExtractClickOptions {
|
|
2749
|
+
/** attach gated element rects (highlight style). */
|
|
2750
|
+
rects?: boolean;
|
|
2751
|
+
/** cursorSpace dims — the rect-size gate's denominator. */
|
|
2752
|
+
space: {
|
|
2753
|
+
w: number;
|
|
2754
|
+
h: number;
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
/**
|
|
2758
|
+
* Extract OUTPUT-anchored clicks from a raw cursor track. Downs in trimmed-away
|
|
2759
|
+
* footage are dropped (they follow their footage, like every source-anchored
|
|
2760
|
+
* feature); a press pairs with the next `up` of the same button unless another
|
|
2761
|
+
* `down` of that button intervenes (a lost `up` must not chain two presses).
|
|
2762
|
+
*/
|
|
2763
|
+
declare function extractClicks(track: CursorTrack, segments: Segment[], opts: ExtractClickOptions): LoweredClick[];
|
|
2764
|
+
/** '#rgb'/'#rrggbb' → [r,g,b] for ctx.data (ON_FRAME composes rgba() per frame). */
|
|
2765
|
+
declare function hexToRgbTriplet(hex: string): [number, number, number] | null;
|
|
2766
|
+
|
|
2767
|
+
/** The doc's segments in canonical explicit form (empty = one full-source span). */
|
|
2768
|
+
declare function effectiveSegments(doc: StudioDoc): Segment[];
|
|
2769
|
+
declare const videoLane: LaneAdapter<ProjectDoc>;
|
|
2770
|
+
/**
|
|
2771
|
+
* Zoom lane — zoom regions as clips ("1.80×"). Spans are SOURCE-anchored
|
|
2772
|
+
* (footage-anchored like speed spans and the cam window); the lane displays
|
|
2773
|
+
* the output extent of each span's KEPT footage (spanOutputExtent — partial
|
|
2774
|
+
* cuts snap the clip's edges, full cuts hide it until the trim is undone).
|
|
2775
|
+
* Move/resize are pointer-true through the FULL rated map — zoom never alters
|
|
2776
|
+
* rates, so no exclusion trick is needed (unlike speedLane). Spans never
|
|
2777
|
+
* overlap: create no-ops inside an existing span, move pushes out of
|
|
2778
|
+
* collisions (or no-ops), resize clamps against neighbors. Level/focus are
|
|
2779
|
+
* edited in the toolbar/inspector, not by gesture. Any gesture promotes the
|
|
2780
|
+
* span to source:'manual' — it survives an auto-zoom regenerate.
|
|
2781
|
+
*/
|
|
2782
|
+
declare const zoomLane: LaneAdapter<ProjectDoc>;
|
|
2783
|
+
/**
|
|
2784
|
+
* Tilt lane — card-pose regions as clips (label = "rx°/ry°"). SOURCE-anchored
|
|
2785
|
+
* like zoom spans (footage-anchored through trims and speed changes; the full
|
|
2786
|
+
* rated map applies — tilt doesn't alter rates); non-overlapping. The pose
|
|
2787
|
+
* itself is edited in the span editor (like zoom level), not by gesture. Any
|
|
2788
|
+
* gesture promotes the span to source:'manual' — it survives a Dynamic-tilt
|
|
2789
|
+
* regenerate (the auto-zoom wand contract).
|
|
2790
|
+
*/
|
|
2791
|
+
declare const tiltLane: LaneAdapter<ProjectDoc>;
|
|
2792
|
+
/**
|
|
2793
|
+
* Cam-move lane — animated cam layout regions as clips (label = the
|
|
2794
|
+
* pose size as a percent when set). SOURCE-anchored like tilt spans (the
|
|
2795
|
+
* full rated map applies); non-overlapping. The pose itself is edited on the
|
|
2796
|
+
* canvas or in the span editor, never by lane gesture. Structurally the tilt
|
|
2797
|
+
* lane with a different payload; kept separate so neither lane's clamps can
|
|
2798
|
+
* drift the other's.
|
|
2799
|
+
*/
|
|
2800
|
+
declare const camMoveLane: LaneAdapter<ProjectDoc>;
|
|
2801
|
+
/**
|
|
2802
|
+
* Webcam lane — the Cam member row of the take group. Its clips are the
|
|
2803
|
+
* visibility window INTERSECTED with each kept segment, at the video lane's
|
|
2804
|
+
* exact output positions, so a split on the Video row visibly splits this row
|
|
2805
|
+
* too. Gestures still edit only the WINDOW (`cam.window`, SOURCE time,
|
|
2806
|
+
* footage-anchored): move slides it (dragging any of its clips moves the one
|
|
2807
|
+
* window), resize lives on the window's REAL edges — the first clip's start
|
|
2808
|
+
* and the last clip's end; the cut boundaries between them belong to the
|
|
2809
|
+
* Video row. There is no remove — hide the bubble via its panel.
|
|
2810
|
+
*/
|
|
2811
|
+
declare const camLane: LaneAdapter<ProjectDoc>;
|
|
2812
|
+
/**
|
|
2813
|
+
* Mic sub-row of the take group: mirrors the video lane's cut boundaries
|
|
2814
|
+
* EXACTLY — one clip per kept segment at the same output positions — so the
|
|
2815
|
+
* voice visibly cuts and splits with the footage. VIEW-ONLY by design: cutting
|
|
2816
|
+
* happens on the Video row, because the take has ONE shared `segments` list
|
|
2817
|
+
* (that is what makes sub-track desync structurally impossible; per-row
|
|
2818
|
+
* segments would only buy bugs). Selecting a clip opens the Voice panel
|
|
2819
|
+
* (level/mute); the waveform is the row's content, so items carry no label.
|
|
2820
|
+
*/
|
|
2821
|
+
declare const micLane: LaneAdapter<ProjectDoc>;
|
|
2822
|
+
/**
|
|
2823
|
+
* Speed lane — rate spans as clips ("2×"). Spans are SOURCE-anchored (footage
|
|
2824
|
+
* follows them through trims); the lane displays them at the output positions
|
|
2825
|
+
* of the rated pieces they produce, so a span visually contracts as its rate
|
|
2826
|
+
* grows. Spans never overlap: create no-ops inside an existing span, move
|
|
2827
|
+
* pushes out of collisions (or no-ops), resize clamps against neighbors.
|
|
2828
|
+
* The rate itself is edited in the toolbar (like zoom level), not by gesture.
|
|
2829
|
+
* Move/resize are POINTER-TRUE: the dragged edge's resulting output position
|
|
2830
|
+
* is exactly the pointer's (mapped through the rate map without this span),
|
|
2831
|
+
* so edges never lag the pointer at 1/rate speed.
|
|
2832
|
+
*/
|
|
2833
|
+
declare const speedLane: LaneAdapter<ProjectDoc>;
|
|
2834
|
+
/**
|
|
2835
|
+
* Music/SFX lane — clips are OUTPUT-anchored (`clip.start` is final-cut
|
|
2836
|
+
* seconds; they do NOT follow footage through trims — see AudioClip). Move
|
|
2837
|
+
* retimes `start`; resizing trims into the source file: the start edge shifts
|
|
2838
|
+
* `in` and `start` together (content stays put under the untouched edge), the
|
|
2839
|
+
* end edge adjusts `out`. Clips are created from the audio inspector, not by
|
|
2840
|
+
* double-click (there is no meaningful "blank" audio clip).
|
|
2841
|
+
*/
|
|
2842
|
+
declare const audioLane: LaneAdapter<ProjectDoc>;
|
|
2843
|
+
declare function parsePoseId(id: string): {
|
|
2844
|
+
clipId: string;
|
|
2845
|
+
index: number;
|
|
2846
|
+
} | null;
|
|
2847
|
+
/**
|
|
2848
|
+
* Text-overlay lane (compositor v2) — clips are OUTPUT-anchored like audio
|
|
2849
|
+
* (`start` is final-cut seconds; a title never retimes with trims/speed).
|
|
2850
|
+
* Overlaps are allowed (two titles can coexist — z-order is array order).
|
|
2851
|
+
* Create adds a house 'title' clip at the playhead, centered, lower-third.
|
|
2852
|
+
*/
|
|
2853
|
+
declare const overlaysLane: LaneAdapter<ProjectDoc>;
|
|
2854
|
+
/**
|
|
2855
|
+
* Object lane — world-space props. Clips show the span (objects with no
|
|
2856
|
+
* span render as a full-length block and don't move — the span IS the lane's
|
|
2857
|
+
* noun). Created from the toolbar; asset/transform edited in the inspector.
|
|
2858
|
+
*/
|
|
2859
|
+
declare const objectsLane: LaneAdapter<ProjectDoc>;
|
|
2860
|
+
|
|
2861
|
+
/**
|
|
2862
|
+
* Waveform peaks — pure downsampling for timeline clip rendering. The host
|
|
2863
|
+
* decodes the file (Web Audio) and hands channel data here; the result is one
|
|
2864
|
+
* max-|sample| value per bucket in [0..1], drawn as symmetric bars.
|
|
2865
|
+
*/
|
|
2866
|
+
declare function computePeaks(channels: Float32Array[], buckets: number): Float32Array;
|
|
2867
|
+
|
|
2868
|
+
/** SOURCE-time loudness grid (RMS per window). */
|
|
2869
|
+
interface MicRms {
|
|
2870
|
+
/** RMS value per window, linear 0..1. */
|
|
2871
|
+
values: Float32Array;
|
|
2872
|
+
/** windows per second. */
|
|
2873
|
+
rate: number;
|
|
2874
|
+
}
|
|
2875
|
+
interface DuckOptions {
|
|
2876
|
+
/** RMS above this counts as speech. */
|
|
2877
|
+
threshold: number;
|
|
2878
|
+
/** gain while ducked (≈ -12 dB). */
|
|
2879
|
+
duckTo: number;
|
|
2880
|
+
/** seconds to reach the ducked level once speech starts. */
|
|
2881
|
+
attack: number;
|
|
2882
|
+
/** seconds to recover after speech stops. */
|
|
2883
|
+
release: number;
|
|
2884
|
+
/** output grid resolution, points per second. */
|
|
2885
|
+
gridHz: number;
|
|
2886
|
+
}
|
|
2887
|
+
declare const DEFAULT_DUCK: DuckOptions;
|
|
2888
|
+
/** RMS windows from raw PCM — the host runs this once per recording (cached). */
|
|
2889
|
+
declare function computeMicRms(channels: Float32Array[], sampleRate: number, windowSec?: number): MicRms;
|
|
2890
|
+
/**
|
|
2891
|
+
* The OUTPUT-time duck multiplier curve: walk an output grid, look up the mic
|
|
2892
|
+
* loudness at the mapped SOURCE moment, and smooth engage/recover with
|
|
2893
|
+
* attack/release one-poles. Points are thinned (emitted on ≥1% change).
|
|
2894
|
+
*/
|
|
2895
|
+
declare function duckCurve(rms: MicRms, segments: Segment[], durationSec: number, opts?: DuckOptions): EnvelopePoint[];
|
|
2896
|
+
|
|
2897
|
+
/** Rate-aware OUTPUT duration of a doc (what the viewer experiences). */
|
|
2898
|
+
/**
|
|
2899
|
+
* The OUTPUT length of either document: a recording's kept footage
|
|
2900
|
+
* through its speed spans, a program's own length (its `program.duration`,
|
|
2901
|
+
* else the config's).
|
|
2902
|
+
*/
|
|
2903
|
+
declare function docOutputDuration(doc: StudioDoc): number;
|
|
2904
|
+
interface MusicBedInput {
|
|
2905
|
+
id: string;
|
|
2906
|
+
/** Durable URL of the track (assets.vos.so catalog or an owned asset). */
|
|
2907
|
+
key: string;
|
|
2908
|
+
name: string;
|
|
2909
|
+
/** Full source-file length, seconds. */
|
|
2910
|
+
trackDuration: number;
|
|
2911
|
+
/** Current output duration of the doc, seconds. */
|
|
2912
|
+
outputDuration: number;
|
|
2913
|
+
/** Whether the recording carries a mic track (ducking default). */
|
|
2914
|
+
hasMic: boolean;
|
|
2915
|
+
}
|
|
2916
|
+
/** A catalog track placed as the doc's background music bed. */
|
|
2917
|
+
declare function musicBedClip(input: MusicBedInput): AudioClip;
|
|
2918
|
+
/**
|
|
2919
|
+
* Is this clip a music BED — background music covering the cut? Beds are
|
|
2920
|
+
* what "add a track" replaces (one bed at a time; trying another vibe must
|
|
2921
|
+
* not stack). A clip the user moved off 0 or shortened mid-cut stopped
|
|
2922
|
+
* being a bed on purpose, so it is theirs to manage and never auto-replaced.
|
|
2923
|
+
*/
|
|
2924
|
+
declare function isMusicBed(clip: AudioClip, outputDuration: number): boolean;
|
|
2925
|
+
/**
|
|
2926
|
+
* Re-fit clips that tracked the output end after the duration changed from
|
|
2927
|
+
* `prevDuration` to `nextDuration` (both OUTPUT seconds). Mutates `doc`
|
|
2928
|
+
* (an immer draft in practice); returns whether anything changed.
|
|
2929
|
+
*/
|
|
2930
|
+
declare function refillAudioBeds(doc: ProjectDoc, prevDuration: number, nextDuration: number): boolean;
|
|
2931
|
+
/**
|
|
2932
|
+
* The take's VOICE source key, or null when it has none: the mic sidecar when
|
|
2933
|
+
* the take was recorded split (AT), else the legacy mixed track (pre-split
|
|
2934
|
+
* takes carried mic+system in the recording's own file). Ducking, the duck-RMS
|
|
2935
|
+
* decode and every "has a voice?" UI gate share this one derivation.
|
|
2936
|
+
*/
|
|
2937
|
+
declare function voiceKey(doc: Pick<ProjectDoc, 'source'> | StudioDoc): string | null;
|
|
2938
|
+
|
|
2939
|
+
/**
|
|
2940
|
+
* The DEFAULT backdrop: the house loop every NEW take opens on
|
|
2941
|
+
* once the flag below is on. A committed constant, not a live vos, the same
|
|
2942
|
+
* way Home's door tiles are a deliberate asset push (decided 2026-08-17):
|
|
2943
|
+
* changing it is a code change with a review, never something an autosave
|
|
2944
|
+
* can re-skin.
|
|
2945
|
+
*
|
|
2946
|
+
* `ground` is the loop's average colour, written into `frame.background` so
|
|
2947
|
+
* the frame before the first decoded frame, the reduced-motion still and
|
|
2948
|
+
* the offline fail-open all land on the loop's own colour. The
|
|
2949
|
+
* keys are the pre-registry bucket objects, which stay in the bucket
|
|
2950
|
+
* forever; the registry's `backdrops/{slug}/…` keys replace them when the
|
|
2951
|
+
* house loop is featured through the verb.
|
|
2952
|
+
*
|
|
2953
|
+
* BACKDROP_DEFAULT_ON is the flip. It stays OFF until the set has a signed-off
|
|
2954
|
+
* seed and the fleet measurement has run: a switch, not a surprise.
|
|
2955
|
+
* Docs already carrying a frame are never touched either way.
|
|
2956
|
+
*/
|
|
2957
|
+
|
|
2958
|
+
declare const DEFAULT_BACKDROP: {
|
|
2959
|
+
readonly slug: "soft-beams";
|
|
2960
|
+
readonly title: "Soft Beams";
|
|
2961
|
+
readonly key: "https://assets.vos.so/backgrounds/soft-beams-1080p.webm";
|
|
2962
|
+
readonly key2k: "https://assets.vos.so/backgrounds/soft-beams-2k.webm";
|
|
2963
|
+
readonly poster: "https://assets.vos.so/backgrounds/soft-beams-poster.jpg";
|
|
2964
|
+
readonly duration: 10;
|
|
2965
|
+
readonly ground: "#a7b2d1";
|
|
2966
|
+
};
|
|
2967
|
+
declare const BACKDROP_DEFAULT_ON: boolean;
|
|
2968
|
+
/** The default backdrop as a doc's `frame.backgroundMedia`. */
|
|
2969
|
+
declare function defaultBackdropMedia(): BackgroundMedia;
|
|
2970
|
+
/** A frame style opening on the default backdrop (media + its ground). */
|
|
2971
|
+
declare function withDefaultBackdrop(frame: FrameStyle): FrameStyle;
|
|
2972
|
+
|
|
2973
|
+
export { ASPECT_RATIOS, AUDIO_PLAN_STEP, type AnchorKind, type AspectRatioOption, type AudioClip, type AudioPlanPoint, type AudioPlanTrack, BACKDROP_DEFAULT_ON, BACKGROUND_Z, type BackgroundMedia, type BakedText3dAsset, type BakedText3dMaterial, type BrowserBarStyle, type BuildDigestInput, CAMERA_FAR, CAMERA_NEAR, CAM_CHAIN_GAP, CAM_EASE, CAM_PAN, CAM_PAN_EASE, CAM_RAMP_IN, CAM_RAMP_OUT, CAM_SIZE_MAX, CAM_SIZE_MIN, CAM_SPAN_MIN, CAPTURE_COVERAGE_MIN, CARD_FOV, CARD_Z, CHANNEL_SPECS_HASH, CHANNEL_SPECS_VERIFIED, CLICK_FX_INTENSITY, CLICK_FX_PRE, CLICK_HIGHLIGHT_FADE, CLICK_PAIR_MAX, CLICK_PULSE_DUR, CLICK_RECT_MAX_FRAC, CLICK_RIPPLE_DUR, CLICK_SYNTH_RELEASE, CROP_MAX_PX, CROP_MIN_FRAC, CROP_MIN_PX, CROP_PAD, CURSOR_IDLE_EPS_FRAC, CURSOR_IDLE_FADE_IN, CURSOR_IDLE_FADE_OUT, CURSOR_IDLE_HOLD, type CamBubbleRect, type CamPoseSpan, type CamStyle, type CaptureNormalization, type CardLayout, type ClickFxStyle, type CursorEvent, type CursorFadeKey, type CursorIdleOptions, type CursorStyle, type CursorTrack, DEFAULT_BACKDROP, DEFAULT_BROWSER_BAR, DEFAULT_CAM_POSE, DEFAULT_CAM_STYLE, DEFAULT_CLICK_FX, DEFAULT_CURSOR_STYLE, DEFAULT_DUCK, DEFAULT_FRAME_STYLE, DEFAULT_SPEED_PARAMS, DEFAULT_TILT_POSE, DEFAULT_ZOOM_LEVEL, DEFAULT_ZOOM_STYLE, DESTINATIONS, DIGEST_CROP_MAX, DIGEST_FULL_MAX, DIGEST_VERSION, DOC_SCHEMA_VERSION, DRAG_FIT_LEVEL, type Destination, type Digest, type DigestImageRef, type DigestPlan, type DigestTakeFacts, type DuckOptions, EXPORT_RESOLUTION_OPTIONS, EXPORT_SHORT_EDGE, type EnvelopePoint, type ExportResolution, type ExtractClickOptions, FOLLOW_RECENTER, FOLLOW_SAFE_RATIO, FRAME_BORDER_COLOR_DEFAULT, FRAME_BORDER_DEFAULT, FRAME_BORDER_WIDTH_DEFAULT, type FocusBounds, type FollowEvent, type FollowOptions, type FrameGeometry, type FrameStyle, type LowerProgramOptions, type LoweredAudioClip, type LoweredClick, type LoweredComposition, type LoweredZoomSpan, MINIMAL_BAR_THEMES, MOTION_DELTA, MOTION_EASE, type MediaOverlayClip, type MicRms, type MinimalBarTheme, type Moment, type MomentKind, type MomentsOptions, type MotionKey, type MotionPose, type MotionPose3D, type MusicBedInput, type NormRect, OBJECT_DEFAULT_SCALE, OVERLAY_FONT_FACES, OVERLAY_LINE_HEIGHT, OVERLAY_MEDIA_DEFAULT_RADIUS, OVERLAY_MEDIA_DEFAULT_WIDTH, OVERLAY_MIN_DURATION, OVERLAY_SCALE_MAX, OVERLAY_SIZE_MAX, OVERLAY_SIZE_MIN, OVERLAY_TRANSITION_DUR, OVERLAY_Z, type ObjectAnimation, type ObjectAsset, type ObjectClip, type ObjectPrimitiveShape, type OverlayClip, type OverlayFontFace, type OverlayKind, type OverlayPresetStyle, type OverlayRect, type OverlayTransform, type OverlayTransition, PLAYBACK_ACTIVITY, PROGRAM_RETIME, type PlanOptions, type PlanTiltOptions, type PlaneSize, type ProgramAnchorDoc, type ProgramTweenEdit, type ProjectDoc, type PxRect, type RecordingArtifact, type RecordingMeta, type Rect, type ResolvedOverlayBox, type ResolvedOverlayStyle, SCENE_MOTION, SCENE_QUIET, SPEED_RATE_MAX, SPEED_RATE_MIN, SPEED_SPAN_MIN, STUDIO_ENTRY_ID, STYLE_FIELDS, type SmoothOptions, type SmoothPoint, type SpeedParams, type SpeedSpan, type StepAnchor, type StepSpan, type StudioAudioPlan, type StudioDoc, type StudioEntry, type StyleField, TEXT3D_DEPTH_DEFAULT, TEXT3D_DEPTH_MAX, TEXT3D_DEPTH_MIN, TEXT_PRESETS, TILT_AUTO_DEAD_ZONE, TILT_AUTO_MIN, TILT_CHAIN_GAP, TILT_DEG_MAX, TILT_EASE, TILT_INTENSITY_MAX, TILT_PAN, TILT_PAN_EASE, TILT_RAMP_IN, TILT_RAMP_OUT, TILT_SPAN_MIN, TILT_UI_DEG_MAX, TRANSITION_SPEED_MULT, type Text3dMaterial, type TextFxDirection, type TextFxKind, type TextFxSpec, type TextFxUnit, type TextOverlayBox, type TextOverlayClip, type TextOverlayPreset, type TextOverlayStroke, type TiltPersonality, type TiltSpan, type TiltStyleName, type TranscriptSegment, type TransitionSpeed, WINDOW_FOCUS_MIN, ZOOM_CHAIN_GAP, ZOOM_EASE, ZOOM_LEVELS, ZOOM_LEVEL_MAX, ZOOM_LEVEL_MIN, ZOOM_PAN, ZOOM_PAN_EASE, ZOOM_RAMP_IN, ZOOM_RAMP_IN_OVERLAP, ZOOM_RAMP_OUT, ZOOM_SPAN_MIN, ZOOM_STYLES, ZOOM_STYLE_OPTIONS, type ZoomSpan, type ZoomStyleName, type ZoomStyleParams, type ZoomWindow, anchorKindOf, anchorSourceDuration, aspectRatioValue, audioLane, buildDigest, camBubbleRect, camBubbleRectAt, camLane, camMoveLane, camRectFromPose, camRestPose, camTrackFromDoc, cardPointToScreen, clampCamFrac, clampCamSize, clampFocus, clampSpeedRate, clampTiltDeg, clampZoomLevel, clipEnvelope, clipLength, computeCardLayout, computeMicRms, computePeaks, copyStyle, cropBox, cursorIdleFade, defaultBackdropMedia, deriveViewportCrop, destinationById, destinationsForChannel, docCardLayout, docOutputDuration, docToCropSpace, docToFullSpace, duckCurve, effectiveSegments, envelopeAt, envelopeValueAt, expectedFrameSize, exportSizeFor, extractClicks, focusBounds, followFocusEvents, frameGeometry, hexToRgbTriplet, idleGaps, isMusicBed, isPlayback, isProgramDoc, isRecordingDoc, levelForFocusFraction, lowerProgramDoc, lowerStudioDoc, lowerToComposition, micLane, migrateHostedDoc, momentsFromDoc, motionTrack, musicBedClip, normalizeCaptureSpace, objectMotionBase, objectMotionPoseAt, objectsLane, outputDurationOf, outputRangeToSource, overlayFaceFor, overlayFontFaces, overlayFontString, overlayHit, overlayLines, overlayMotionBase, overlayMotionPoseAt, overlayRect, overlaysLane, pageDisplayUrl, parsePoseId, pickStyle, planAutoSpeed, planAutoTilt, planAutoZoom, planForDigest, planeSizeAtDepth, platformBarKind, programDuration, projectFromArtifact, ratedSegments, recommendedExportResolution, refillAudioBeds, removeSourceRange, removeSpeedInRange, resolveExportSize, resolveOverlayBox, resolveOverlayStyle, resolveText3dAsset, resolveZoomStyle, sceneChanges, scrollRuns, setSpeedInRange, smoothCursor, spanOutputExtent, speedLane, studioAudioPlan, studioEntry, studioLayerData, tiltLane, tiltTrackFromDoc, transitionMult, videoLane, voiceKey, withDefaultBackdrop, wrapProgramLength, zoomCoversRect, zoomLane, zoomSpanForRange, zoomTrackFromDoc, zoomWindow };
|