@open-take/compositor 0.1.2 → 0.2.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.
@@ -4,17 +4,11 @@
4
4
  import { makeScene2D, Rect, Video, Line, Circle, Node, Gradient, Txt } from "@revideo/2d";
5
5
  import { createSignal, tween, linear } from "@revideo/core";
6
6
  import {
7
- buildStageKeyframes,
7
+ stageCamera,
8
8
  buildLegs,
9
9
  cursorPos,
10
10
  isDragging,
11
- keyvalN,
12
- keyvalP,
13
- clampCenter,
14
- stageEasing,
15
- panEasing,
16
11
  gradientEndpoints,
17
- restStageScale,
18
12
  smoother,
19
13
  } from "../math";
20
14
  import comp from "./.composition.json";
@@ -23,9 +17,9 @@ const vW = comp.source.videoWidth,
23
17
  vH = comp.source.videoHeight;
24
18
  const oW = comp.output.width,
25
19
  oH = comp.output.height;
26
- const stage = buildStageKeyframes(comp);
20
+ const cam = stageCamera(comp);
27
21
  const legs = buildLegs(comp);
28
- const rest = restStageScale(vW, vH, oW, oH, comp.framing.insetFrac);
22
+ const rest = cam.rest;
29
23
 
30
24
  // video-px -> stage-local coords (stage local origin = video centre)
31
25
  const lx = (px) => px - vW / 2;
@@ -46,12 +40,10 @@ const CURSOR = [
46
40
  export default makeScene2D("take", function* (view) {
47
41
  const t = createSignal(0);
48
42
 
49
- // zoom/pan stage easing (scale + center in unison): spring → bezier → smoother
50
- const scaleEase = stageEasing(comp.cursor); // zoom-IN: spring allowed
51
- const panEase = panEasing(comp.cursor); // zoom-OUT scale + centre: smooth bezier
52
- // spring in / bezier out (smooth settle); Math.max(rest,…) is a floor. Mirrors derive.ts.
53
- const scaleAt = () => Math.max(rest, keyvalN(t(), stage.z, scaleEase, panEase));
54
- const centerAt = () => clampCenter(keyvalP(t(), stage.c, panEase), scaleAt(), vW, vH, oW, oH);
43
+ // The camera: ONE eased viewport rect (centre + size in lockstep, targets
44
+ // pre-clamped at build) see math.ts stageCamera. Mirrors the editor preview.
45
+ const scaleAt = () => cam.at(t()).scale;
46
+ const centerAt = () => cam.at(t()).center;
55
47
 
56
48
  // Composition camera: ONE camera zooms the WHOLE
57
49
  // composition (backdrop + the inset framed screen) together. At rest the
@@ -228,5 +220,5 @@ export default makeScene2D("take", function* (view) {
228
220
  if (review.label) view.add(pill(review.label, 1, true));
229
221
  }
230
222
 
231
- yield* tween(stage.T, (v) => t(v * stage.T), linear);
223
+ yield* tween(cam.T, (v) => t(v * cam.T), linear);
232
224
  });
package/src/types.ts CHANGED
@@ -25,8 +25,22 @@ export type CaptureEventBase = {
25
25
  /** selector / note, kept for editability */
26
26
  sel?: string;
27
27
  note?: string;
28
- /** selective-zoom intent from the plan (default auto = heuristic) */
28
+ /** selective-zoom intent from the plan. Absent/"auto" the camera director
29
+ * decides from the ground-truth log; "always"/"never" hard-override it (and
30
+ * cut the beat out of any cluster — an override is a segment boundary). */
29
31
  zoom?: ZoomIntent;
32
+ /** CAPTURE-DERIVED (frame-diff / mutation pass — the `effectBox` seam): the
33
+ * region that actually CHANGED after the action, viewport CSS px. The
34
+ * director frames THIS over `box` when present — a `type`'s result region, or
35
+ * a payoff that lands somewhere other than where you clicked. Absent ⇒ the
36
+ * director shapes an ROI from `box`/kind instead. */
37
+ effectBox?: BBox;
38
+ /** CAPTURE-DERIVED (same pass): fraction of the frame that changed after the
39
+ * action, 0..1. ≥ camera.pullOutCoverage ⇒ the action repainted most of the
40
+ * frame (nav / global restyle) ⇒ pull out to full view. Absent ⇒ the pull-out
41
+ * branch is skipped (the director can't tell nav from popover on bbox alone —
42
+ * it says so in the beat's `reason`). */
43
+ changeCoverage?: number;
30
44
  };
31
45
 
32
46
  /** A click (or a type's focus-click): an instantaneous action at a point. */
@@ -187,23 +201,27 @@ export type CursorConfig = {
187
201
  rippleMs: number;
188
202
  /** ms to hold a zoom after the action settles, before zooming back out */
189
203
  holdMs: number;
190
- /** ms for the zoom-OUT ramp (back to rest) */
204
+ /** ms for a zoom-OUT / pull-out ramp (to rest OR to any wider framing).
205
+ * Measured on a reference export: the pull-out is ~1.8× slower than the
206
+ * punch-in (their zoom-out spring ω≈5.2 rad/s ⇒ ~1340ms of critically-damped
207
+ * settle) — a slow, soft release reads premium; a fast one reads like a
208
+ * flinch. */
191
209
  zoomOutMs: number;
192
210
  /** ms for the zoom-IN ramp (into a target). Decoupled from travelMs so the
193
- * zoom can be slower/gentler than the cursor (a cinematic ~1s zoom). */
211
+ * zoom can be slower/gentler than the cursor. Measured reference punch-in spring
212
+ * ω≈9.4 rad/s ⇒ ~730ms. */
194
213
  zoomInMs: number;
195
- /** Easing for the zoom/pan stage ramps (scale + center together), as
196
- * cubic-bezier control points. Absent ⇒ symmetric smootherstep — whose broad
197
- * near-constant-velocity middle reads a bit linear, esp. on the zoom-OUT
198
- * settle. A decel-biased curve gives a softer landing into rest. */
214
+ /** Optional cubic-bezier easing for the camera-rect ramps (centre + size in
215
+ * lockstep see math.ts stageCamera). Absent ⇒ the default critically-
216
+ * damped spring (the measured reference curve). Set this only to force a
217
+ * bezier feel; `zoomSpring` wins over it when both are set. */
199
218
  zoomEase?: [number, number, number, number];
200
- /** Spring easing for the zoom/pan stage ramps, as a `bounce` amount ∈ [0,~0.6):
201
- * 0 = critically damped (a soft physical ease-out), higher = more overshoot/
202
- * snap (the "silky" settle a premium screen-recorder has; ~0.06 for zoom). When
203
- * set, this WINS over `zoomEase` (see math.ts stageEasing). Absent ⇒ use
204
- * `zoomEase`/smootherstep. The segment duration stays zoomInMs/zoomOutMs;
205
- * bounce only shapes the curve. NB: large bounce can undershoot rest on the
206
- * zoom-OUT (momentary backdrop dead-space) — keep it small for zoom. */
219
+ /** Spring easing for the camera-rect ramps, as a `bounce` amount ∈ [0,~0.6):
220
+ * 0 = critically damped (the measured reference zoom curve also the
221
+ * default when neither zoomSpring nor zoomEase is set), higher = more
222
+ * overshoot/snap. The segment duration stays zoomInMs/zoomOutMs; bounce only
223
+ * shapes the curve. Bounce > 0 overshoots the RECT (a touch past the target
224
+ * frame, then settle) keep it small. */
207
225
  zoomSpring?: number;
208
226
  /** ms to delay the synthetic cursor along a DRAG stroke, compensating for the
209
227
  * capture pipeline latency: the captured ink appears ~this long after the pen
@@ -253,6 +271,53 @@ export type ReviewDecor = {
253
271
  label?: string;
254
272
  };
255
273
 
274
+ /** The auto-camera director's tuning. ON by default (a plan that specifies no
275
+ * zoom must still come out with sensible framing — the whole point). The
276
+ * numbers are the FEEL knobs; judge them by eye on a rendered clip, not on
277
+ * paper. `enabled: false` is the clean escape hatch: the director doesn't run
278
+ * and ONLY explicit `zoom: "always"/"never"` produce zoom (auto/absent hold
279
+ * full view). There is no legacy per-event heuristic to fall back to. */
280
+ export type CameraConfig = {
281
+ enabled: boolean;
282
+ /** an ROI fills this fraction of the frame when framed (bigger ⇒ tighter). */
283
+ fillFrac: number;
284
+ /** hard ceiling on scale. NOT the main lever — ROI SIZE drives the actual
285
+ * scale (a big type-ROI lands medium, a tiny icon lands tight); this just
286
+ * stops a pinpoint ROI zooming past legibility. */
287
+ maxScale: number;
288
+ /** a punch that can't stay on screen this long is dropped to full view (a
289
+ * sub-second punch reads as a flinch). Enforced AFTER hard breaks — the hold
290
+ * extends into the gap before the next break, never merges across one. */
291
+ minHoldMs: number;
292
+ /** a scale below this isn't worth a distinct frame: a single beat that fits
293
+ * under it holds full view, and a coalesced cluster whose UNION falls under
294
+ * it splits instead (the two are "different regions" → chain/pull, not hold). */
295
+ minZoomScale: number;
296
+ /** gap between two actions greater than this ⇒ they cannot share a frame. */
297
+ coalesceWindowMs: number;
298
+ /** two ROIs whose centres are closer than this (fraction of video width) MAY
299
+ * coalesce into one shared, held frame — the "cluster" (a thumbnail rail, a
300
+ * toolbar). Farther apart ⇒ a re-frame (progressive/deeper), not a hold. */
301
+ travelThreshold: number;
302
+ /** changeCoverage ≥ this ⇒ the action repainted most of the frame (nav /
303
+ * global) ⇒ pull out to full view. The nav-vs-popover divider — the one knob
304
+ * to tune by eye on real captures (frame-diff pass populates the input). */
305
+ pullOutCoverage: number;
306
+ };
307
+
308
+ // Defaults tuned as a STARTING point — every number here is meant to be judged
309
+ // on a rendered clip and A/B'd one at a time, not signed off on paper.
310
+ export const DEFAULT_CAMERA: CameraConfig = {
311
+ enabled: true,
312
+ fillFrac: 0.6,
313
+ maxScale: 2.4,
314
+ minHoldMs: 1200,
315
+ minZoomScale: 1.25,
316
+ coalesceWindowMs: 900,
317
+ travelThreshold: 0.18,
318
+ pullOutCoverage: 0.55,
319
+ };
320
+
256
321
  export type TakeComposition = {
257
322
  output: { width: number; height: number; fps: number };
258
323
  source: {
@@ -321,15 +386,13 @@ export const DEFAULT_CURSOR: CursorConfig = {
321
386
  arcMax: 24,
322
387
  rippleMs: 450,
323
388
  holdMs: 1100,
324
- // Gentle, cinematic zoom (a ~1s ramp reads as premium); our
325
- // old 600ms tied-to-travel zoom felt snappy/mechanical by comparison.
326
- zoomOutMs: 800,
327
- zoomInMs: 760,
328
- // Zoom/pan stage easing. Same decel-biased curve as the cursor symmetric
329
- // smootherstep (the fallback) has a broad near-constant-velocity middle that
330
- // reads a touch linear, especially as the zoom-OUT settles back to rest; this
331
- // gives a soft landing into rest. Applied to scale + center together.
332
- zoomEase: [0.3, 0.0, 0.2, 1.0],
389
+ // Camera ramp durations, measured off a reference export by frame-
390
+ // tracking (see math.ts springEase): punch-in spring ω≈9.4 rad/s 730ms,
391
+ // pull-out ω≈5.2 ≈ 1340ms. The slow soft release is half the premium feel.
392
+ // No zoomEase/zoomSpring set ⇒ stageEasing falls to springEase(0), the
393
+ // critically-damped spring that IS the measured SS curve over these windows.
394
+ zoomOutMs: 1340,
395
+ zoomInMs: 730,
333
396
  // The captured ink trails the pen by the screencast/encode pipeline latency τ;
334
397
  // delay the cursor by τ so its tip rides the ink front. Set to τ EXACTLY and
335
398
  // the cursor locks to the ink at ALL stroke speeds (both are the same time-
package/src/validate.ts CHANGED
@@ -187,6 +187,24 @@ export function validateComposition(
187
187
  }
188
188
  }
189
189
 
190
+ // camera-rect spring: bounce shapes the ONE ease driving centre+size, so an
191
+ // out-of-range value corrupts every camera move (see math.ts springEase).
192
+ const spring = comp.cursor.zoomSpring;
193
+ if (spring != null) {
194
+ if (!(spring >= 0 && spring < 0.6))
195
+ err(
196
+ "cursor.zoomSpring",
197
+ `zoomSpring ${spring} outside [0, 0.6)`,
198
+ "0 = critically damped (the default feel); keep bounce ≤ 0.3",
199
+ );
200
+ else if (spring > 0.3)
201
+ warn(
202
+ "cursor.zoomSpring",
203
+ `zoomSpring ${spring} is a lot of bounce — the rect overshoot can flash backdrop at edge-flush targets and over-zoom tight punches`,
204
+ "keep ≤ 0.3 (the editor slider's max) unless the flash is wanted",
205
+ );
206
+ }
207
+
190
208
  // tail: the composition must outlast the last action (+ a little settle)
191
209
  if (comp.durationMs < lastEnd)
192
210
  err(