@vosjs/studio-core 0.23.1 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1828,8 +1828,8 @@ function round(v) {
1828
1828
  // src/lower/lowerToComposition.ts
1829
1829
  import {
1830
1830
  EASINGS,
1831
- lerpArray,
1832
- sample,
1831
+ lerpArray as lerpArray2,
1832
+ sample as sample2,
1833
1833
  segmentRate as segmentRate2,
1834
1834
  sortKeyframes as sortKeyframes2,
1835
1835
  sourceToTimeline,
@@ -2661,14 +2661,14 @@ function resolveOverlayFx(clip, clipDuration) {
2661
2661
  const maxTotal = Math.max(dur, clipDuration * 0.9);
2662
2662
  st = Math.min(st, Math.max(0, (maxTotal - dur) / (upperN - 1)));
2663
2663
  }
2664
- const round12 = (v) => Math.round(v * 1e4) / 1e4;
2664
+ const round13 = (v) => Math.round(v * 1e4) / 1e4;
2665
2665
  return {
2666
2666
  k: spec.fx,
2667
2667
  u: unit,
2668
2668
  d: FX_DIR[spec.direction ?? "forward"] ?? 0,
2669
- st: round12(st),
2670
- dur: round12(dur),
2671
- tt: round12(st * (upperN - 1) + dur),
2669
+ st: round13(st),
2670
+ dur: round13(dur),
2671
+ tt: round13(st * (upperN - 1) + dur),
2672
2672
  units,
2673
2673
  n
2674
2674
  };
@@ -3057,12 +3057,340 @@ function round2(v) {
3057
3057
  return Math.round(v * 1e3) / 1e3;
3058
3058
  }
3059
3059
 
3060
+ // src/lower/pin.ts
3061
+ import { lerpArray, mapTime, sample } from "@vosjs/timeline";
3062
+ var PIN_GAP = 24;
3063
+ var PIN_MARGIN = 24;
3064
+ var PIN_SAMPLE_HZ = 30;
3065
+ var PIN_SIMPLIFY_PX = 0.5;
3066
+ var PIN_EDGE_S = 0.35;
3067
+ var PIN_PRESS_WINDOW_S = 0.5;
3068
+ var PIN_TEXT_ADVANCE = 0.55;
3069
+ var SIDE_ORDER = ["right", "left", "below", "above"];
3070
+ function pinReferent(doc, pin) {
3071
+ const meta = doc.source.meta;
3072
+ const w = meta.width || 0;
3073
+ const h = meta.height || 0;
3074
+ if (pin.rect && validRect(pin.rect)) {
3075
+ return { rect: { ...pin.rect }, by: "rect", at: null };
3076
+ }
3077
+ if (pin.step !== void 0) {
3078
+ const step = findStep(meta.steps ?? [], pin.step);
3079
+ if (!step || step.skipped) return null;
3080
+ const carry = dragCarry(doc, step.tStart - 0.05, step.tEnd + 0.05);
3081
+ if (step.rect && validRect(step.rect) && w > 0 && h > 0) {
3082
+ return {
3083
+ rect: normRect(step.rect, w, h),
3084
+ by: "step",
3085
+ step,
3086
+ at: step.tStart,
3087
+ ...carry ? { carry } : {}
3088
+ };
3089
+ }
3090
+ const union = unionRects(
3091
+ pressRects(doc, step.tStart - 0.05, step.tEnd + 0.05)
3092
+ );
3093
+ if (!union || w <= 0 || h <= 0) return null;
3094
+ return {
3095
+ rect: normRect(union, w, h),
3096
+ by: "step",
3097
+ step,
3098
+ at: step.tStart,
3099
+ ...carry ? { carry } : {}
3100
+ };
3101
+ }
3102
+ if (pin.press !== void 0 && Number.isFinite(pin.press)) {
3103
+ let best = null;
3104
+ for (const e of doc.source.cursor) {
3105
+ if (e.type !== "down" || !e.rect || !validRect(e.rect)) continue;
3106
+ const t = e.t / 1e3;
3107
+ const d = Math.abs(t - pin.press);
3108
+ if (d > PIN_PRESS_WINDOW_S) continue;
3109
+ if (!best || d < Math.abs(best.t - pin.press)) best = { t, rect: e.rect };
3110
+ }
3111
+ if (!best || w <= 0 || h <= 0) return null;
3112
+ const carry = dragCarry(doc, best.t - 0.01, best.t + 0.01);
3113
+ return {
3114
+ rect: normRect(best.rect, w, h),
3115
+ by: "press",
3116
+ at: best.t,
3117
+ ...carry ? { carry } : {}
3118
+ };
3119
+ }
3120
+ return null;
3121
+ }
3122
+ function dragCarry(doc, from, to) {
3123
+ const meta = doc.source.meta;
3124
+ if (!(meta.width > 0) || !(meta.height > 0)) return null;
3125
+ const d = dragsFromTrack(doc.source.cursor, meta.width, meta.height).find(
3126
+ (g) => g.t0 >= from && g.t0 <= to
3127
+ );
3128
+ if (!d) return null;
3129
+ return { t0: d.t0, t1: d.t1, x0: d.nx, y0: d.ny };
3130
+ }
3131
+ function cursorAt(cursor, t, space) {
3132
+ let prev = null;
3133
+ for (const e of cursor) {
3134
+ if (e.type !== "move" && e.type !== "down" && e.type !== "up") continue;
3135
+ const cur = { t: e.t / 1e3, nx: e.x / space.w, ny: e.y / space.h };
3136
+ if (cur.t >= t) {
3137
+ if (!prev) return { nx: cur.nx, ny: cur.ny };
3138
+ const k = cur.t > prev.t ? (t - prev.t) / (cur.t - prev.t) : 1;
3139
+ return {
3140
+ nx: prev.nx + (cur.nx - prev.nx) * k,
3141
+ ny: prev.ny + (cur.ny - prev.ny) * k
3142
+ };
3143
+ }
3144
+ prev = cur;
3145
+ }
3146
+ return prev ? { nx: prev.nx, ny: prev.ny } : null;
3147
+ }
3148
+ function findStep(steps, ref) {
3149
+ const byId = steps.find((s) => s.id !== void 0 && s.id === ref);
3150
+ if (byId) return byId;
3151
+ const idx = typeof ref === "number" ? ref : Number(ref);
3152
+ if (!Number.isInteger(idx)) return void 0;
3153
+ return steps.find((s) => s.step === idx);
3154
+ }
3155
+ function pressRects(doc, from, to) {
3156
+ const out = [];
3157
+ for (const e of doc.source.cursor) {
3158
+ if (e.type !== "down" && e.type !== "key") continue;
3159
+ if (!e.rect || !validRect(e.rect)) continue;
3160
+ const t = e.t / 1e3;
3161
+ if (t < from || t > to) continue;
3162
+ out.push(e.rect);
3163
+ }
3164
+ return out;
3165
+ }
3166
+ function unionRects(rects) {
3167
+ if (!rects.length) return null;
3168
+ let x0 = Infinity;
3169
+ let y0 = Infinity;
3170
+ let x1 = -Infinity;
3171
+ let y1 = -Infinity;
3172
+ for (const r of rects) {
3173
+ x0 = Math.min(x0, r.x);
3174
+ y0 = Math.min(y0, r.y);
3175
+ x1 = Math.max(x1, r.x + r.w);
3176
+ y1 = Math.max(y1, r.y + r.h);
3177
+ }
3178
+ return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
3179
+ }
3180
+ function validRect(r) {
3181
+ return Number.isFinite(r.x) && Number.isFinite(r.y) && Number.isFinite(r.w) && Number.isFinite(r.h) && r.w > 0 && r.h > 0;
3182
+ }
3183
+ function normRect(r, w, h) {
3184
+ return { x: r.x / w, y: r.y / h, w: r.w / w, h: r.h / h };
3185
+ }
3186
+ function pinBox(clip, layout) {
3187
+ const scale = clip.transform.scale || 1;
3188
+ if (clip.kind === "html") {
3189
+ const pb = htmlLayerPictureBox(clip);
3190
+ const wPic = htmlLayerWidth(clip) * layout.W * scale;
3191
+ const unit = wPic / pb.width;
3192
+ return { w: clip.box.width * unit, h: clip.box.height * unit };
3193
+ }
3194
+ if (clip.kind !== "text") {
3195
+ const w = (clip.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH) * layout.W * scale;
3196
+ return { w, h: w / (16 / 9) };
3197
+ }
3198
+ const r = overlayRect(clip, estimateMeasure, layout.W, layout.H);
3199
+ return { w: r.w, h: r.h };
3200
+ }
3201
+ function estimateMeasure(text, font, letterSpacingPx = 0) {
3202
+ const m = /(\d+(?:\.\d+)?)px/.exec(font);
3203
+ const px = m ? Number(m[1]) : 16;
3204
+ return text.length * (px * PIN_TEXT_ADVANCE + letterSpacingPx);
3205
+ }
3206
+ function pinRectOnScreen(rect, t, layout, camera, zoomTrack) {
3207
+ let level = 1;
3208
+ let cx = 0.5;
3209
+ let cy = 0.5;
3210
+ let centring;
3211
+ if (zoomTrack && zoomTrack.keyframes.length) {
3212
+ const z = sample(zoomTrack, t, lerpArray);
3213
+ level = z[0];
3214
+ cx = z[1];
3215
+ cy = z[2];
3216
+ if (z.length > 3) centring = z[3];
3217
+ }
3218
+ const v = zoomView(level, cx, cy, layout, camera, centring);
3219
+ const map = (nx, ny) => {
3220
+ const px = layout.dx + nx * layout.dw;
3221
+ const py = layout.dy + ny * layout.dh;
3222
+ return {
3223
+ x: v.ox + (px - v.wcx) * v.level,
3224
+ y: v.oy + (py - v.wcy) * v.level
3225
+ };
3226
+ };
3227
+ const a = map(rect.x, rect.y);
3228
+ const b = map(rect.x + rect.w, rect.y + rect.h);
3229
+ return { x: a.x, y: a.y, w: b.x - a.x, h: b.y - a.y };
3230
+ }
3231
+ function pinReferentAt(referent, t, input) {
3232
+ const { carry, cursor, space, rated } = input;
3233
+ if (!carry || !cursor || !space || !rated) return referent;
3234
+ const s = mapTime(rated, t);
3235
+ const sc = Math.max(carry.t0, Math.min(carry.t1, s));
3236
+ const p = cursorAt(cursor, sc, space);
3237
+ if (!p) return referent;
3238
+ return {
3239
+ x: referent.x + (p.nx - carry.x0),
3240
+ y: referent.y + (p.ny - carry.y0),
3241
+ w: referent.w,
3242
+ h: referent.h
3243
+ };
3244
+ }
3245
+ function pinPlacement(input) {
3246
+ const { clip, referent, layout, camera, zoomTrack, base, motion } = input;
3247
+ const dur = Math.max(1e-3, clip.duration);
3248
+ const gap = clip.pin?.gap ?? PIN_GAP;
3249
+ const box = pinBox(clip, layout);
3250
+ const W = layout.W;
3251
+ const H = layout.H;
3252
+ const refAt = (local) => pinRectOnScreen(
3253
+ pinReferentAt(referent, clip.start + local, input),
3254
+ clip.start + local,
3255
+ layout,
3256
+ camera,
3257
+ zoomTrack
3258
+ );
3259
+ const centreFor = (r, side2) => {
3260
+ switch (side2) {
3261
+ case "right":
3262
+ return { x: r.x + r.w + gap + box.w / 2, y: r.y + r.h / 2 };
3263
+ case "left":
3264
+ return { x: r.x - gap - box.w / 2, y: r.y + r.h / 2 };
3265
+ case "below":
3266
+ return { x: r.x + r.w / 2, y: r.y + r.h + gap + box.h / 2 };
3267
+ case "above":
3268
+ return { x: r.x + r.w / 2, y: r.y - gap - box.h / 2 };
3269
+ }
3270
+ };
3271
+ const fits = (c) => c.x - box.w / 2 >= PIN_MARGIN && c.x + box.w / 2 <= W - PIN_MARGIN && c.y - box.h / 2 >= PIN_MARGIN && c.y + box.h / 2 <= H - PIN_MARGIN;
3272
+ const room = (c) => {
3273
+ const x0 = Math.max(PIN_MARGIN, c.x - box.w / 2);
3274
+ const x1 = Math.min(W - PIN_MARGIN, c.x + box.w / 2);
3275
+ const y0 = Math.max(PIN_MARGIN, c.y - box.h / 2);
3276
+ const y1 = Math.min(H - PIN_MARGIN, c.y + box.h / 2);
3277
+ return Math.max(0, x1 - x0) * Math.max(0, y1 - y0);
3278
+ };
3279
+ let side;
3280
+ const asked = clip.pin?.side ?? "auto";
3281
+ if (asked !== "auto") side = asked;
3282
+ else {
3283
+ const edge = Math.min(PIN_EDGE_S, dur / 3);
3284
+ const probes = [edge, dur / 2, dur - edge].map(refAt);
3285
+ const fitting = SIDE_ORDER.find(
3286
+ (s) => probes.every((r) => fits(centreFor(r, s)))
3287
+ );
3288
+ if (fitting) side = fitting;
3289
+ else {
3290
+ let best = SIDE_ORDER[0];
3291
+ let bestRoom = -1;
3292
+ for (const s of SIDE_ORDER) {
3293
+ const total = probes.reduce((acc, r) => acc + room(centreFor(r, s)), 0);
3294
+ if (total > bestRoom) {
3295
+ bestRoom = total;
3296
+ best = s;
3297
+ }
3298
+ }
3299
+ side = best;
3300
+ }
3301
+ }
3302
+ const clampC = (c) => {
3303
+ const minX = Math.min(W / 2, PIN_MARGIN + box.w / 2);
3304
+ const maxX = Math.max(W / 2, W - PIN_MARGIN - box.w / 2);
3305
+ const minY = Math.min(H / 2, PIN_MARGIN + box.h / 2);
3306
+ const maxY = Math.max(H / 2, H - PIN_MARGIN - box.h / 2);
3307
+ return {
3308
+ x: Math.max(minX, Math.min(maxX, c.x)),
3309
+ y: Math.max(minY, Math.min(maxY, c.y))
3310
+ };
3311
+ };
3312
+ const tipFor = (c) => {
3313
+ switch (side) {
3314
+ case "right":
3315
+ return { x: c.x - box.w / 2, y: c.y };
3316
+ case "left":
3317
+ return { x: c.x + box.w / 2, y: c.y };
3318
+ case "below":
3319
+ return { x: c.x, y: c.y - box.h / 2 };
3320
+ case "above":
3321
+ return { x: c.x, y: c.y + box.h / 2 };
3322
+ }
3323
+ };
3324
+ const n = Math.max(1, Math.ceil(dur * PIN_SAMPLE_HZ));
3325
+ const poses = [];
3326
+ const tips = [];
3327
+ let clamped = 0;
3328
+ for (let i = 0; i <= n; i++) {
3329
+ const local = i === n ? dur : i / PIN_SAMPLE_HZ;
3330
+ const want = centreFor(refAt(local), side);
3331
+ const c = clampC(want);
3332
+ clamped = Math.max(clamped, Math.hypot(c.x - want.x, c.y - want.y));
3333
+ const pose = motion?.keyframes.length ? sample(motion, local, lerpArray) : base;
3334
+ poses.push({
3335
+ t: local,
3336
+ value: [c.x / W, c.y / H, pose[2], pose[3], pose[4]],
3337
+ ease: "linear"
3338
+ });
3339
+ const tp = tipFor(c);
3340
+ tips.push({ t: local, value: [tp.x / W, tp.y / H], ease: "linear" });
3341
+ }
3342
+ return {
3343
+ side,
3344
+ track: { keyframes: simplify(poses, [W, H, 1, 1, 1]) },
3345
+ tip: { keyframes: simplify(tips, [W, H]) },
3346
+ clamped: round4(clamped),
3347
+ box
3348
+ };
3349
+ }
3350
+ function simplify(keys, scalePx) {
3351
+ if (keys.length <= 2) return keys.map(roundKey);
3352
+ const out = [roundKey(keys[0])];
3353
+ let a = 0;
3354
+ for (let i = 2; i <= keys.length - 1; i++) {
3355
+ const ka = keys[a];
3356
+ const ki = keys[i];
3357
+ let ok = true;
3358
+ for (let j = a + 1; j < i && ok; j++) {
3359
+ const kj = keys[j];
3360
+ const u = (kj.t - ka.t) / (ki.t - ka.t || 1);
3361
+ for (let k = 0; k < kj.value.length; k++) {
3362
+ const pred = ka.value[k] + (ki.value[k] - ka.value[k]) * u;
3363
+ if (Math.abs(pred - kj.value[k]) * (scalePx[k] ?? 1) > PIN_SIMPLIFY_PX) {
3364
+ ok = false;
3365
+ break;
3366
+ }
3367
+ }
3368
+ }
3369
+ if (!ok) {
3370
+ a = i - 1;
3371
+ out.push(roundKey(keys[a]));
3372
+ }
3373
+ }
3374
+ const last = keys[keys.length - 1];
3375
+ if (out[out.length - 1].t !== round4(last.t)) out.push(roundKey(last));
3376
+ return out;
3377
+ }
3378
+ function roundKey(k) {
3379
+ return { t: round4(k.t), value: k.value.map(round42), ease: k.ease };
3380
+ }
3381
+ function round4(v) {
3382
+ return Math.round(v * 1e3) / 1e3;
3383
+ }
3384
+ function round42(v) {
3385
+ return Math.round(v * 1e5) / 1e5;
3386
+ }
3387
+
3060
3388
  // src/lower/cursorIdle.ts
3061
3389
  var CURSOR_IDLE_HOLD = 1;
3062
3390
  var CURSOR_IDLE_FADE_OUT = 0.35;
3063
3391
  var CURSOR_IDLE_FADE_IN = 0.18;
3064
3392
  var CURSOR_IDLE_EPS_FRAC = 25e-4;
3065
- var round4 = (n) => Math.round(n * 1e3) / 1e3;
3393
+ var round5 = (n) => Math.round(n * 1e3) / 1e3;
3066
3394
  function cursorIdleFade(track, o) {
3067
3395
  const moves = track.filter((e) => e.type === "move" || e.type === "down" || e.type === "up").map((e) => ({ t: e.t / 1e3, x: e.x, y: e.y }));
3068
3396
  if (moves.length === 0) return [];
@@ -3114,7 +3442,7 @@ function emit(keys, s, e) {
3114
3442
  push(keys, e + CURSOR_IDLE_FADE_IN, 1);
3115
3443
  }
3116
3444
  function push(keys, t, a) {
3117
- const rt = round4(t);
3445
+ const rt = round5(t);
3118
3446
  if (keys.length > 0 && rt <= keys[keys.length - 1].t) {
3119
3447
  keys[keys.length - 1].a = a;
3120
3448
  return;
@@ -4622,7 +4950,7 @@ function gatedRect(rect, x, y, space) {
4622
4950
  return null;
4623
4951
  const m = RECT_CONTAIN_MARGIN;
4624
4952
  const inside = x >= rect.x - m && x <= rect.x + rect.w + m && y >= rect.y - m && y <= rect.y + rect.h + m;
4625
- return inside ? [round5(rect.x), round5(rect.y), round5(rect.w), round5(rect.h)] : null;
4953
+ return inside ? [round6(rect.x), round6(rect.y), round6(rect.w), round6(rect.h)] : null;
4626
4954
  }
4627
4955
  function extractClicks(track, segments, opts) {
4628
4956
  const out = [];
@@ -4647,11 +4975,11 @@ function extractClicks(track, segments, opts) {
4647
4975
  const upSrc = st + Math.max(pressLen, 0.02);
4648
4976
  const up = keptOutputEnd(segments, st, upSrc, opts.media) ?? ot + CLICK_SYNTH_RELEASE;
4649
4977
  const click = {
4650
- ot: round5(ot),
4651
- up: round5(Math.max(up, ot + 0.02)),
4652
- st: round5(st),
4653
- x: round5(e.x),
4654
- y: round5(e.y),
4978
+ ot: round6(ot),
4979
+ up: round6(Math.max(up, ot + 0.02)),
4980
+ st: round6(st),
4981
+ x: round6(e.x),
4982
+ y: round6(e.y),
4655
4983
  b: button
4656
4984
  };
4657
4985
  if (opts.rects && e.rect) {
@@ -4670,7 +4998,7 @@ function hexToRgbTriplet(hex) {
4670
4998
  const n = parseInt(h, 16);
4671
4999
  return [n >> 16, n >> 8 & 255, n & 255];
4672
5000
  }
4673
- function round5(v) {
5001
+ function round6(v) {
4674
5002
  return Math.round(v * 1e3) / 1e3;
4675
5003
  }
4676
5004
 
@@ -4746,7 +5074,7 @@ function trackEmitter() {
4746
5074
  if (tt <= prev.t + 1e-6 && sameVec(prev.value, value)) return prev.t;
4747
5075
  if (tt <= prev.t + 1e-6) tt = prev.t + 1e-3;
4748
5076
  }
4749
- keyframes.push({ t: round6(tt), value: value.map(round6), ease });
5077
+ keyframes.push({ t: round7(tt), value: value.map(round7), ease });
4750
5078
  return tt;
4751
5079
  };
4752
5080
  return { keyframes, push: push2 };
@@ -4923,7 +5251,7 @@ function camBubbleRectAt(doc, t) {
4923
5251
  H
4924
5252
  );
4925
5253
  if (!track.keyframes.length) return camBubbleRect(doc.cam, W, H);
4926
- return camRectFromPose(sample(track, t, lerpArray), doc.cam, W, H);
5254
+ return camRectFromPose(sample2(track, t, lerpArray2), doc.cam, W, H);
4927
5255
  }
4928
5256
  function motionTrack(base, keys, dur) {
4929
5257
  const sorted = keys.filter((k) => Number.isFinite(k.at)).sort((a, b) => a.at - b.at);
@@ -4971,7 +5299,7 @@ function overlayMotionPoseAt(o, t) {
4971
5299
  Math.max(OVERLAY_MIN_DURATION, o.duration)
4972
5300
  );
4973
5301
  if (!track.keyframes.length) return null;
4974
- return [...sample(track, t, lerpArray)];
5302
+ return [...sample2(track, t, lerpArray2)];
4975
5303
  }
4976
5304
  function objectMotionBase(o) {
4977
5305
  const t = o.transform3d;
@@ -5001,7 +5329,7 @@ function objectMotionPoseAt(o, t, clipDur) {
5001
5329
  o.span?.duration ?? clipDur
5002
5330
  );
5003
5331
  if (!track.keyframes.length) return null;
5004
- return [...sample(track, t, lerpArray)];
5332
+ return [...sample2(track, t, lerpArray2)];
5005
5333
  }
5006
5334
  var FRAME_STEP_SRC = `
5007
5335
  function stepPaused(vid, srcT) {
@@ -6383,6 +6711,59 @@ var ON_FRAME = `(ctx, content, dt) => {
6383
6711
  }
6384
6712
  }
6385
6713
 
6714
+ // pinned layers' marks \u2014 a standing ring or underline on a layer's
6715
+ // REFERENT for the layer's life (d.pins: OUTPUT-anchored records baked at
6716
+ // lowering, the referent in normalised video fractions). Painted inside the
6717
+ // zoom transform so the mark sits on the element under any level or pose,
6718
+ // at a constant on-screen weight (\xF7 ceZs), with the layer's own entrance
6719
+ // and exit. Locals are pn-prefixed (one var scope). Skipped on the ghost
6720
+ // pass like the effects.
6721
+ var pns = d.pins || []
6722
+ if (pns.length && !tpG) {
6723
+ for (var pi = 0; pi < pns.length; pi++) {
6724
+ var pn = pns[pi]
6725
+ if (!pn.mark || pn.mark === 'none') continue
6726
+ var pnT = t - pn.start
6727
+ if (pnT < 0 || pnT > pn.dur) continue
6728
+ var pnA = 1
6729
+ if (pnT < 0.35) { var pnU = pnT / 0.35; pnA = 1 - Math.pow(1 - pnU, 3) }
6730
+ if (pn.dur - pnT < 0.35) { var pnV = (pn.dur - pnT) / 0.35; pnA = Math.min(pnA, 1 - Math.pow(1 - pnV, 3)) }
6731
+ if (pnA <= 0.004) continue
6732
+ var pnX = dx + pn.x * dw, pnY = dy + pn.y * dh, pnW = pn.w * dw, pnH = pn.h * dh
6733
+ // a carried referent (a drag): moved by where the pointer has taken
6734
+ // it at the on-screen source moment, held at the release
6735
+ if (pn.carry) {
6736
+ var pcC = (actM ? actM.cursor : d.cursor) || []
6737
+ var pcT = Math.max(pn.carry.t0, Math.min(pn.carry.t1, srcT))
6738
+ var pcx = -1, pcy = -1
6739
+ for (var pk = 0; pk < pcC.length; pk++) { if (pcC[pk].t <= pcT) { pcx = pcC[pk].x; pcy = pcC[pk].y } }
6740
+ if (pcx >= 0) { pnX += (pcx / (space.w || vw) - pn.carry.x0) * dw; pnY += (pcy / (space.h || vh) - pn.carry.y0) * dh }
6741
+ }
6742
+ var pnLw = 2 * s2 / ceZs, pnPad = 6 * s2 / ceZs
6743
+ var pnCol = pn.color || 'rgba(255,255,255,0.95)'
6744
+ c.save()
6745
+ c.globalAlpha = pnA
6746
+ if (pn.mark === 'underline') {
6747
+ // a bar under the referent: dark rim, then the ink
6748
+ var pnBy = pnY + pnH + pnPad * 0.5
6749
+ c.fillStyle = 'rgba(0,0,0,0.35)'
6750
+ c.fillRect(pnX - pnLw, pnBy - pnLw, pnW + pnLw * 2, pnLw * 1.5 + pnLw * 2)
6751
+ c.fillStyle = pnCol
6752
+ c.fillRect(pnX, pnBy, pnW, pnLw * 1.5)
6753
+ } else {
6754
+ // a ring around the referent: the cursor's dual stroke, a dark rim
6755
+ // under the ink, legible on any content
6756
+ c.lineWidth = pnLw + 2 * s2 / ceZs
6757
+ c.strokeStyle = 'rgba(0,0,0,0.35)'
6758
+ rr(pnX - pnPad, pnY - pnPad, pnW + pnPad * 2, pnH + pnPad * 2, pnPad, c); c.stroke()
6759
+ c.lineWidth = pnLw
6760
+ c.strokeStyle = pnCol
6761
+ rr(pnX - pnPad, pnY - pnPad, pnW + pnPad * 2, pnH + pnPad * 2, pnPad, c); c.stroke()
6762
+ }
6763
+ c.restore()
6764
+ }
6765
+ }
6766
+
6386
6767
  // cursor (SOURCE-anchored samples, read at the on-screen source moment).
6387
6768
  // The dot is the only thing cursorStyle.visible hides \u2014 the track still drives
6388
6769
  // cursor-follow zoom, and click effects draw above on their own switch.
@@ -6445,8 +6826,15 @@ var ON_FRAME = `(ctx, content, dt) => {
6445
6826
  // Wait only for the FIRST decoded frame, then keep drawing through seeks.
6446
6827
  if (camV && camV.readyState >= 2) r.camHasFrame = true
6447
6828
  var camActiveNow = !!(camV && camOn && camS.visible !== false && r.camHasFrame)
6829
+ // A pinned layer's leader is live for the layer's life: the overlay layer
6830
+ // repaints while one is up (the referent moves with the camera).
6831
+ var pqs = d.pins || [], pqLive = false
6832
+ for (var pl = 0; pl < pqs.length; pl++) {
6833
+ var pq0 = pqs[pl]
6834
+ if (pq0.leader && t >= pq0.start && t <= pq0.start + pq0.dur) { pqLive = true; break }
6835
+ }
6448
6836
  var ovSig = W + 'x' + H
6449
- var ovDirty = ov.sig !== ovSig || camActiveNow || ov.active || !!trOut
6837
+ var ovDirty = ov.sig !== ovSig || camActiveNow || ov.active || !!trOut || pqLive
6450
6838
  if (ovDirty) {
6451
6839
  ovC.clearRect(0, 0, W, H)
6452
6840
  // webcam bubble \u2014 pinned to the frame corner regardless of card tilt/zoom.
@@ -6496,6 +6884,66 @@ var ON_FRAME = `(ctx, content, dt) => {
6496
6884
  ovC.restore()
6497
6885
  }
6498
6886
  }
6887
+ // pinned layers' leaders \u2014 a hairline from the layer's near edge (the
6888
+ // baked tip track, clip-local frame fractions) to the referent's near
6889
+ // edge, the referent mapped through the camera the way the boundary dot
6890
+ // is, with a dot at the referent. Fades with the layer; rides the rise
6891
+ // entrance so the line meets the card as it lands.
6892
+ if (pqLive) {
6893
+ for (var pj = 0; pj < pqs.length; pj++) {
6894
+ var pq = pqs[pj]
6895
+ if (!pq.leader) continue
6896
+ var pqT = t - pq.start
6897
+ if (pqT < 0 || pqT > pq.dur) continue
6898
+ var pqA = 1, pqRise = 0
6899
+ if (pqT < 0.35) {
6900
+ var pqU = 1 - Math.pow(1 - pqT / 0.35, 3)
6901
+ pqA = pqU
6902
+ if (pq.enter === 'rise') pqRise = (1 - pqU) * 24 * s
6903
+ }
6904
+ if (pq.dur - pqT < 0.35) { var pqV = (pq.dur - pqT) / 0.35; pqA = Math.min(pqA, 1 - Math.pow(1 - pqV, 3)) }
6905
+ if (pqA <= 0.004 || !pq.tip || !pq.tip.keyframes || !pq.tip.keyframes.length) continue
6906
+ var pqTip = TL.sample(pq.tip, pqT, TL.lerpArray)
6907
+ var pqTx = pqTip[0] * W, pqTy = pqTip[1] * H + pqRise
6908
+ // the referent's corners in frame space under the live camera
6909
+ var pqAx = dx + pq.x * dw, pqAy = dy + pq.y * dh
6910
+ var pqBx = dx + (pq.x + pq.w) * dw, pqBy = dy + (pq.y + pq.h) * dh
6911
+ if (pq.carry) {
6912
+ var pqcT = Math.max(pq.carry.t0, Math.min(pq.carry.t1, srcT))
6913
+ var pqcx = -1, pqcy = -1
6914
+ for (var pqk = 0; pqk < cur.length; pqk++) { if (cur[pqk].t <= pqcT) { pqcx = cur[pqk].x; pqcy = cur[pqk].y } }
6915
+ if (pqcx >= 0) {
6916
+ var pqOx = (pqcx / (space.w || vw) - pq.carry.x0) * dw, pqOy = (pqcy / (space.h || vh) - pq.carry.y0) * dh
6917
+ pqAx += pqOx; pqBx += pqOx; pqAy += pqOy; pqBy += pqOy
6918
+ }
6919
+ }
6920
+ if (lvl > 1.001 && !d.zoomSuppressed) {
6921
+ if (camStage) {
6922
+ pqAx = W / 2 + (pqAx - wcx) * lvl; pqAy = H / 2 + (pqAy - wcy) * lvl
6923
+ pqBx = W / 2 + (pqBx - wcx) * lvl; pqBy = H / 2 + (pqBy - wcy) * lvl
6924
+ } else {
6925
+ pqAx = fx + (pqAx - fx) * lvl; pqAy = fy + (pqAy - fy) * lvl
6926
+ pqBx = fx + (pqBx - fx) * lvl; pqBy = fy + (pqBy - fy) * lvl
6927
+ }
6928
+ }
6929
+ var pqRx = pq.side === 'right' ? pqBx : pq.side === 'left' ? pqAx : (pqAx + pqBx) / 2
6930
+ var pqRy = pq.side === 'below' ? pqBy : pq.side === 'above' ? pqAy : (pqAy + pqBy) / 2
6931
+ var pqCol = pq.color || 'rgba(255,255,255,0.95)'
6932
+ var pqLw = 2 * s
6933
+ ovC.save()
6934
+ ovC.globalAlpha = pqA
6935
+ ovC.lineCap = 'round'
6936
+ ovC.strokeStyle = 'rgba(0,0,0,0.35)'; ovC.lineWidth = pqLw + 2 * s
6937
+ ovC.beginPath(); ovC.moveTo(pqTx, pqTy); ovC.lineTo(pqRx, pqRy); ovC.stroke()
6938
+ ovC.strokeStyle = pqCol; ovC.lineWidth = pqLw
6939
+ ovC.beginPath(); ovC.moveTo(pqTx, pqTy); ovC.lineTo(pqRx, pqRy); ovC.stroke()
6940
+ ovC.fillStyle = 'rgba(0,0,0,0.35)'
6941
+ ovC.beginPath(); ovC.arc(pqRx, pqRy, 5 * s, 0, Math.PI * 2); ovC.fill()
6942
+ ovC.fillStyle = pqCol
6943
+ ovC.beginPath(); ovC.arc(pqRx, pqRy, 4 * s, 0, Math.PI * 2); ovC.fill()
6944
+ ovC.restore()
6945
+ }
6946
+ }
6499
6947
  // The boundary dot: the outgoing card's last cursor point and the
6500
6948
  // incoming's current one, each carried by its card's move, one dot on
6501
6949
  // the eased lerp between them (the idle fade never fires across a page
@@ -6510,7 +6958,7 @@ var ON_FRAME = `(ctx, content, dt) => {
6510
6958
  drawCur(ovC, tqX2, tqY2, tqR, 1, 2 * (trQw || s))
6511
6959
  }
6512
6960
  ov.sig = ovSig
6513
- ov.active = camActiveNow || !!trOut
6961
+ ov.active = camActiveNow || !!trOut || pqLive
6514
6962
  if (ov.texture) ov.texture.needsUpdate = true
6515
6963
  }
6516
6964
 
@@ -6604,7 +7052,7 @@ var ON_FRAME = `(ctx, content, dt) => {
6604
7052
  window.__voilaDebug = { cv: cv, bgCv: bg.canvas, ovCv: ov.canvas, W: W, H: H, glW: gl && gl.width, glH: gl && gl.height, t: t, canvases: document.querySelectorAll('canvas').length }
6605
7053
  }
6606
7054
  }`;
6607
- function studioLayerData(layers, duration) {
7055
+ function studioLayerData(layers, duration, pins) {
6608
7056
  const rec = "source" in layers ? layers : null;
6609
7057
  return {
6610
7058
  // Music/SFX clips with their gain envelopes baked (shared truth for the
@@ -6613,14 +7061,14 @@ function studioLayerData(layers, duration) {
6613
7061
  // needs a decoded recording, which a sync lowering can't produce.
6614
7062
  audio: (layers.audio ?? []).map((c) => ({
6615
7063
  key: c.key,
6616
- start: round6(c.start),
6617
- in: round6(c.in),
6618
- out: round6(c.out),
6619
- gain: round6(c.gain),
7064
+ start: round7(c.start),
7065
+ in: round7(c.in),
7066
+ out: round7(c.out),
7067
+ gain: round7(c.gain),
6620
7068
  loop: !!c.loop,
6621
- len: round6(clipLength(c)),
7069
+ len: round7(clipLength(c)),
6622
7070
  duck: !!c.duck,
6623
- env: clipEnvelope(c).map((p) => ({ t: round6(p.t), g: round6(p.g) }))
7071
+ env: clipEnvelope(c).map((p) => ({ t: round7(p.t), g: round7(p.g) }))
6624
7072
  })),
6625
7073
  // Object clips: numbers resolved HERE; shapes are the drafted engine spec.
6626
7074
  ...layers.objects && layers.objects.length ? {
@@ -6633,17 +7081,17 @@ function studioLayerData(layers, duration) {
6633
7081
  } : o.asset.kind === "text3d" ? resolveText3dAsset(o.asset) : { kind: "gltf", key: o.asset.key },
6634
7082
  ...o.span ? {
6635
7083
  span: {
6636
- start: round6(o.span.start),
6637
- duration: round6(o.span.duration)
7084
+ start: round7(o.span.start),
7085
+ duration: round7(o.span.duration)
6638
7086
  }
6639
7087
  } : {},
6640
- x: round6(o.transform3d.x),
6641
- y: round6(o.transform3d.y),
6642
- z: round6(o.transform3d.z),
6643
- rx: round6(o.transform3d.rx),
6644
- ry: round6(o.transform3d.ry),
6645
- rz: round6(o.transform3d.rz),
6646
- scale: round6(o.transform3d.scale || OBJECT_DEFAULT_SCALE),
7088
+ x: round7(o.transform3d.x),
7089
+ y: round7(o.transform3d.y),
7090
+ z: round7(o.transform3d.z),
7091
+ rx: round7(o.transform3d.rx),
7092
+ ry: round7(o.transform3d.ry),
7093
+ rz: round7(o.transform3d.rz),
7094
+ scale: round7(o.transform3d.scale || OBJECT_DEFAULT_SCALE),
6647
7095
  anim: idleKey(o),
6648
7096
  // Pose keyframes: clip-local [x,y,z,rx,ry,rz,scale] track,
6649
7097
  // sampled at t − span.start. Omitted when absent — parity.
@@ -6670,18 +7118,20 @@ function studioLayerData(layers, duration) {
6670
7118
  const base = {
6671
7119
  id: o.id,
6672
7120
  kind: o.kind,
6673
- start: round6(o.start),
6674
- dur: round6(Math.max(OVERLAY_MIN_DURATION, o.duration)),
6675
- x: round6(o.transform.x),
6676
- y: round6(o.transform.y),
6677
- scale: round6(o.transform.scale || 1),
6678
- rot: round6(o.transform.rotation || 0),
7121
+ start: round7(o.start),
7122
+ dur: round7(Math.max(OVERLAY_MIN_DURATION, o.duration)),
7123
+ x: round7(o.transform.x),
7124
+ y: round7(o.transform.y),
7125
+ scale: round7(o.transform.scale || 1),
7126
+ rot: round7(o.transform.rotation || 0),
6679
7127
  enter: enterKey(o),
6680
7128
  exit: exitKey(o),
6681
7129
  // Pose keyframes: a CLIP-LOCAL [x, y, scale, rot, opacity]
6682
7130
  // track, sampled in ON_FRAME at t − start. Omitted when the clip
6683
7131
  // has no motion — data byte parity.
6684
7132
  ...(() => {
7133
+ const pinned = pins?.get(o.id);
7134
+ if (pinned) return { track: pinned.track };
6685
7135
  if (!o.motion || !o.motion.length) return {};
6686
7136
  const mb = overlayMotionBase(o);
6687
7137
  const track = motionTrack(
@@ -6698,10 +7148,10 @@ function studioLayerData(layers, duration) {
6698
7148
  ...base,
6699
7149
  kind: "image",
6700
7150
  key: hl.key,
6701
- w: round6(hl.width),
7151
+ w: round7(hl.width),
6702
7152
  radius: 0,
6703
7153
  shadow: "none",
6704
- opacity: round6(o.opacity ?? 1),
7154
+ opacity: round7(o.opacity ?? 1),
6705
7155
  html: hl.html
6706
7156
  };
6707
7157
  }
@@ -6718,8 +7168,8 @@ function studioLayerData(layers, duration) {
6718
7168
  ...o.frame.browserBar?.theme ? { theme: o.frame.browserBar.theme } : {}
6719
7169
  },
6720
7170
  lean: [
6721
- round6(o.frame.lean?.rx ?? 0),
6722
- round6(o.frame.lean?.ry ?? 0)
7171
+ round7(o.frame.lean?.rx ?? 0),
7172
+ round7(o.frame.lean?.ry ?? 0)
6723
7173
  ],
6724
7174
  shadow: o.frame.shadow ?? LAYER_CARD_SHADOW,
6725
7175
  shadowContact: o.frame.shadowContact ?? 0,
@@ -6735,17 +7185,17 @@ function studioLayerData(layers, duration) {
6735
7185
  for (const ev of lm.cursor) {
6736
7186
  if (ev.type === "down")
6737
7187
  clicks.push({
6738
- t: round6(ev.t / 1e3),
6739
- x: round6(ev.x),
6740
- y: round6(ev.y)
7188
+ t: round7(ev.t / 1e3),
7189
+ x: round7(ev.x),
7190
+ y: round7(ev.y)
6741
7191
  });
6742
7192
  }
6743
7193
  return {
6744
7194
  cur: {
6745
7195
  pts: sm.map((p) => ({
6746
- t: round6(p.t),
6747
- x: round6(p.x),
6748
- y: round6(p.y)
7196
+ t: round7(p.t),
7197
+ x: round7(p.x),
7198
+ y: round7(p.y)
6749
7199
  })),
6750
7200
  space: { w: lm.meta.width, h: lm.meta.height },
6751
7201
  size: rec.cursor.size,
@@ -6760,7 +7210,7 @@ function studioLayerData(layers, duration) {
6760
7210
  ...lm && lm.crop ? { crop: lm.crop } : {},
6761
7211
  ...lmCard ? { card: lmCard } : {},
6762
7212
  ...lmFacts,
6763
- w: round6(o.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH),
7213
+ w: round7(o.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH),
6764
7214
  radius: o.radius ?? OVERLAY_MEDIA_DEFAULT_RADIUS,
6765
7215
  opacity: o.opacity ?? 1,
6766
7216
  loop: !!o.loop,
@@ -6770,7 +7220,7 @@ function studioLayerData(layers, duration) {
6770
7220
  ...o.shadow ? { shadow: o.shadow } : {},
6771
7221
  ...o.border && o.border.width > 0 ? {
6772
7222
  border: {
6773
- width: round6(o.border.width),
7223
+ width: round7(o.border.width),
6774
7224
  color: o.border.color
6775
7225
  }
6776
7226
  } : {}
@@ -6790,11 +7240,11 @@ function studioLayerData(layers, duration) {
6790
7240
  // Style-v2 fields bake only when non-default (byte parity for
6791
7241
  // older docs); ON_FRAME reads them unconditionally.
6792
7242
  ...st.fontStyle === "italic" ? { sty: "italic" } : {},
6793
- ...o.maxWidth ? { mw: round6(o.maxWidth) } : {},
6794
- ...st.letterSpacing ? { ls: round6(st.letterSpacing) } : {},
6795
- ...st.lineHeight !== OVERLAY_LINE_HEIGHT ? { lh: round6(st.lineHeight) } : {},
7243
+ ...o.maxWidth ? { mw: round7(o.maxWidth) } : {},
7244
+ ...st.letterSpacing ? { ls: round7(st.letterSpacing) } : {},
7245
+ ...st.lineHeight !== OVERLAY_LINE_HEIGHT ? { lh: round7(st.lineHeight) } : {},
6796
7246
  ...st.align !== "center" ? { align: st.align } : {},
6797
- ...st.stroke ? { stroke: { c: st.stroke.color, w: round6(st.stroke.width) } } : {},
7247
+ ...st.stroke ? { stroke: { c: st.stroke.color, w: round7(st.stroke.width) } } : {},
6798
7248
  // Hosted face behind an override: ON_FRAME lazy-loads it so a
6799
7249
  // live family/weight edit paints without a LOAD (SETUP only
6800
7250
  // runs on cold load).
@@ -6807,10 +7257,10 @@ function studioLayerData(layers, duration) {
6807
7257
  ...bx ? {
6808
7258
  box: {
6809
7259
  c: bx.color,
6810
- o: round6(bx.opacity),
6811
- px: round6(bx.padX),
6812
- py: round6(bx.padY),
6813
- r: round6(bx.radius)
7260
+ o: round7(bx.opacity),
7261
+ px: round7(bx.padX),
7262
+ py: round7(bx.padY),
7263
+ r: round7(bx.radius)
6814
7264
  }
6815
7265
  } : {},
6816
7266
  // Entrance animation: segmentation + timing normalized
@@ -6844,36 +7294,22 @@ function lowerToComposition(input) {
6844
7294
  sourceDuration: (doc.source.meta.durationMs || 0) / 1e3
6845
7295
  }) : [];
6846
7296
  const layout = docCardLayout(doc);
6847
- const meta = doc.source.meta;
6848
7297
  const zoomStyle = resolveZoomStyle(doc.zoomStyle, doc.zoomParams);
6849
7298
  const camera = cameraModel(doc.frame);
6850
- const zoomSpans = restSpansThroughTransitions(
6851
- doc.zoom,
7299
+ const zoomSpans = loweredZoomSpans(
7300
+ doc,
6852
7301
  rated,
6853
7302
  transitions,
6854
- ZOOM_SPAN_MIN
6855
- ).map((z) => {
6856
- if (z.focusMode === "auto") {
6857
- const f = followFocusEvents(
6858
- z,
6859
- doc.source.cursor,
6860
- { w: meta.width, h: meta.height },
6861
- layout,
6862
- {
6863
- safeRatio: zoomStyle.followSafeRatio,
6864
- recenter: zoomStyle.followRecenter,
6865
- lookahead: zoomStyle.followLookahead,
6866
- camera
6867
- }
6868
- );
6869
- if (f.entry)
6870
- return { ...z, cx: f.entry.cx, cy: f.entry.cy, followEvents: f.events };
6871
- }
6872
- return {
6873
- ...z,
6874
- ...clampFocus(z.cx, z.cy, clampZoomLevel(z.level), layout, camera)
6875
- };
6876
- });
7303
+ layout,
7304
+ camera,
7305
+ zoomStyle
7306
+ );
7307
+ const zoomTrack = prependEntrance(
7308
+ zoomTrackFromDoc(zoomSpans, rated, zoomStyle),
7309
+ entranceZoomKeyframes(cardEnter(doc.frame))
7310
+ );
7311
+ const pins = resolvePins(doc, layout, camera, zoomTrack);
7312
+ const pinsData = pinsDataOf(doc, pins);
6877
7313
  const data = {
6878
7314
  videoSrc: doc.source.videoKey,
6879
7315
  isImage: doc.source.sourceKind === "image",
@@ -6896,17 +7332,17 @@ function lowerToComposition(input) {
6896
7332
  // Rated segments (speed spans pre-intersected): ON_FRAME's mapTime and the
6897
7333
  // export's audio splice read the rate straight off each segment.
6898
7334
  segments: rated.map((seg) => ({
6899
- in: round6(seg.in),
6900
- out: round6(seg.out),
6901
- ...seg.rate !== void 0 && seg.rate !== 1 ? { rate: round6(seg.rate) } : {},
7335
+ in: round7(seg.in),
7336
+ out: round7(seg.out),
7337
+ ...seg.rate !== void 0 && seg.rate !== 1 ? { rate: round7(seg.rate) } : {},
6902
7338
  ...seg.media ? { media: seg.media } : {}
6903
7339
  })),
6904
7340
  frame: doc.frame,
6905
7341
  micGain: doc.micGain ?? 1,
6906
7342
  cursor: smoothed.map((p) => ({
6907
- t: round6(p.t),
6908
- x: round6(p.x),
6909
- y: round6(p.y)
7343
+ t: round7(p.t),
7344
+ x: round7(p.x),
7345
+ y: round7(p.y)
6910
7346
  })),
6911
7347
  cursorStyle: doc.cursor,
6912
7348
  cursorSpace: { w: doc.source.meta.width, h: doc.source.meta.height },
@@ -6929,9 +7365,9 @@ function lowerToComposition(input) {
6929
7365
  crop: m.crop ?? null,
6930
7366
  hasAudio: !!m.meta.hasAudio,
6931
7367
  cursor: sm.map((p) => ({
6932
- t: round6(p.t),
6933
- x: round6(p.x),
6934
- y: round6(p.y)
7368
+ t: round7(p.t),
7369
+ x: round7(p.x),
7370
+ y: round7(p.y)
6935
7371
  })),
6936
7372
  cursorSpace: { w: m.meta.width, h: m.meta.height },
6937
7373
  // The card this media wears (its own placement, bar, corner,
@@ -6950,11 +7386,11 @@ function lowerToComposition(input) {
6950
7386
  // intensities/colors become numbers HERE — ON_FRAME reads no registry).
6951
7387
  ...clickFxData(doc, rated),
6952
7388
  // Rated segments so zoom spans land at their speed-adjusted output times.
6953
- // A pull-out entrance writes the track's head.
6954
- zoomTrack: prependEntrance(
6955
- zoomTrackFromDoc(zoomSpans, rated, zoomStyle),
6956
- entranceZoomKeyframes(cardEnter(doc.frame))
6957
- ),
7389
+ zoomTrack,
7390
+ // Pinned layers' marks and leaders: the referent (normalised video
7391
+ // fractions) plus the leader's tip track, painted by ON_FRAME beside the
7392
+ // camera. Absent when no pinned layer asks for either — byte parity.
7393
+ ...pinsData.length ? { pins: pinsData } : {},
6958
7394
  // Tilt spans: OUTPUT-time [rx, ry] degree track. The rest pose is
6959
7395
  // FLAT — there is no static card tilt any more — and the motion constants
6960
7396
  // come from the camera style's tilt personality ('drift' slows its
@@ -6987,7 +7423,7 @@ function lowerToComposition(input) {
6987
7423
  };
6988
7424
  const entryData = {
6989
7425
  lights: true,
6990
- ...studioLayerData(doc, duration)
7426
+ ...studioLayerData(doc, duration, pins)
6991
7427
  };
6992
7428
  const config = {
6993
7429
  version: 2,
@@ -7013,6 +7449,144 @@ function lowerToComposition(input) {
7013
7449
  };
7014
7450
  return { config, data, stack: { [STUDIO_ENTRY_ID]: entryData }, duration };
7015
7451
  }
7452
+ function loweredZoomSpans(doc, rated, transitions, layout, camera, zoomStyle) {
7453
+ const meta = doc.source.meta;
7454
+ return restSpansThroughTransitions(
7455
+ doc.zoom,
7456
+ rated,
7457
+ transitions,
7458
+ ZOOM_SPAN_MIN
7459
+ ).map((z) => {
7460
+ if (z.focusMode === "auto") {
7461
+ const f = followFocusEvents(
7462
+ z,
7463
+ doc.source.cursor,
7464
+ { w: meta.width, h: meta.height },
7465
+ layout,
7466
+ {
7467
+ safeRatio: zoomStyle.followSafeRatio,
7468
+ recenter: zoomStyle.followRecenter,
7469
+ lookahead: zoomStyle.followLookahead,
7470
+ camera
7471
+ }
7472
+ );
7473
+ if (f.entry)
7474
+ return { ...z, cx: f.entry.cx, cy: f.entry.cy, followEvents: f.events };
7475
+ }
7476
+ return {
7477
+ ...z,
7478
+ ...clampFocus(z.cx, z.cy, clampZoomLevel(z.level), layout, camera)
7479
+ };
7480
+ });
7481
+ }
7482
+ function docZoomTrack(input) {
7483
+ const doc = migrateMotion(input);
7484
+ const rated = ratedSegments(doc);
7485
+ const transitions = docTransitions(doc);
7486
+ const layout = docCardLayout(doc);
7487
+ const camera = cameraModel(doc.frame);
7488
+ const zoomStyle = resolveZoomStyle(doc.zoomStyle, doc.zoomParams);
7489
+ const spans = loweredZoomSpans(
7490
+ doc,
7491
+ rated,
7492
+ transitions,
7493
+ layout,
7494
+ camera,
7495
+ zoomStyle
7496
+ );
7497
+ return prependEntrance(
7498
+ zoomTrackFromDoc(spans, rated, zoomStyle),
7499
+ entranceZoomKeyframes(cardEnter(doc.frame))
7500
+ );
7501
+ }
7502
+ function resolvePins(doc, layout, camera, zoomTrack) {
7503
+ const out = /* @__PURE__ */ new Map();
7504
+ const rated = ratedSegments(doc);
7505
+ const space = { w: doc.source.meta.width, h: doc.source.meta.height };
7506
+ for (const o of doc.overlays ?? []) {
7507
+ if (!o.pin) continue;
7508
+ const ref = pinReferent(doc, o.pin);
7509
+ if (!ref) continue;
7510
+ const base = overlayMotionBase(o);
7511
+ const motion = o.motion && o.motion.length ? motionTrack(
7512
+ base,
7513
+ overlayMotionKeys(o, base),
7514
+ Math.max(OVERLAY_MIN_DURATION, o.duration)
7515
+ ) : null;
7516
+ out.set(
7517
+ o.id,
7518
+ pinPlacement({
7519
+ clip: o,
7520
+ referent: ref.rect,
7521
+ layout,
7522
+ camera,
7523
+ zoomTrack,
7524
+ base,
7525
+ motion,
7526
+ carry: ref.carry ?? null,
7527
+ cursor: doc.source.cursor,
7528
+ space,
7529
+ rated
7530
+ })
7531
+ );
7532
+ }
7533
+ return out;
7534
+ }
7535
+ function overlayPinPoseAt(doc, o, t) {
7536
+ if (!o.pin) return null;
7537
+ const layout = docCardLayout(doc);
7538
+ const camera = cameraModel(doc.frame);
7539
+ const pins = resolvePins(
7540
+ { ...doc, overlays: [o] },
7541
+ layout,
7542
+ camera,
7543
+ docZoomTrack(doc)
7544
+ );
7545
+ const p = pins.get(o.id);
7546
+ if (!p) return null;
7547
+ return [...sample2(p.track, t, lerpArray2)];
7548
+ }
7549
+ function pinsDataOf(doc, pins) {
7550
+ const out = [];
7551
+ for (const o of doc.overlays ?? []) {
7552
+ const p = o.pin;
7553
+ const placed = p ? pins.get(o.id) : void 0;
7554
+ if (!p || !placed) continue;
7555
+ const mark = p.mark ?? "none";
7556
+ if (mark === "none" && !p.leader) continue;
7557
+ const ref = pinReferent(doc, p);
7558
+ if (!ref) continue;
7559
+ out.push({
7560
+ id: o.id,
7561
+ start: round7(o.start),
7562
+ dur: round7(Math.max(OVERLAY_MIN_DURATION, o.duration)),
7563
+ x: round43(ref.rect.x),
7564
+ y: round43(ref.rect.y),
7565
+ w: round43(ref.rect.w),
7566
+ h: round43(ref.rect.h),
7567
+ side: placed.side,
7568
+ mark,
7569
+ leader: !!p.leader,
7570
+ ...p.color ? { color: p.color } : {},
7571
+ // A carried referent (a drag): ON_FRAME moves the mark and the
7572
+ // leader's end with the pointer through [t0, t1], source seconds.
7573
+ ...ref.carry ? {
7574
+ carry: {
7575
+ t0: round7(ref.carry.t0),
7576
+ t1: round7(ref.carry.t1),
7577
+ x0: round43(ref.carry.x0),
7578
+ y0: round43(ref.carry.y0)
7579
+ }
7580
+ } : {},
7581
+ enter: enterKey(o),
7582
+ tip: placed.tip
7583
+ });
7584
+ }
7585
+ return out;
7586
+ }
7587
+ function round43(v) {
7588
+ return Math.round(v * 1e5) / 1e5;
7589
+ }
7016
7590
  function clickFxData(doc, rated) {
7017
7591
  const fx = doc.cursor.clickFx;
7018
7592
  const on = fx.style !== "none" || fx.press;
@@ -7046,7 +7620,7 @@ function clickFxData(doc, rated) {
7046
7620
  }
7047
7621
  };
7048
7622
  }
7049
- function round6(v) {
7623
+ function round7(v) {
7050
7624
  return Math.round(v * 1e3) / 1e3;
7051
7625
  }
7052
7626
 
@@ -7136,8 +7710,8 @@ function planAutoSpeed(track, opts) {
7136
7710
  accepted.sort((a, b) => a.in - b.in);
7137
7711
  return accepted.map((c, i) => ({
7138
7712
  id: `s${i}`,
7139
- in: round7(c.in),
7140
- out: round7(c.out),
7713
+ in: round8(c.in),
7714
+ out: round8(c.out),
7141
7715
  rate: clampSpeedRate(c.rate),
7142
7716
  source: "auto"
7143
7717
  }));
@@ -7190,7 +7764,7 @@ function collectRuns(evs, gap, minLen, emit2) {
7190
7764
  }
7191
7765
  flush();
7192
7766
  }
7193
- function round7(v) {
7767
+ function round8(v) {
7194
7768
  return Math.round(v * 1e3) / 1e3;
7195
7769
  }
7196
7770
 
@@ -7371,16 +7945,16 @@ function momentsFromDoc(doc, plan, opts = {}) {
7371
7945
  return {
7372
7946
  id: `m${String(i + 1).padStart(2, "0")}`,
7373
7947
  kind: d.kind,
7374
- source: { in: round8(d.in), out: round8(d.out) },
7375
- output: output ? { in: round8(output.start), out: round8(output.end) } : null,
7376
- at: d.at === null ? null : round8(d.at),
7377
- outputAt: outAt ? round8(outAt.start) : null,
7378
- focus: d.focus ? { cx: round8(d.focus.cx), cy: round8(d.focus.cy) } : null,
7948
+ source: { in: round9(d.in), out: round9(d.out) },
7949
+ output: output ? { in: round9(output.start), out: round9(output.end) } : null,
7950
+ at: d.at === null ? null : round9(d.at),
7951
+ outputAt: outAt ? round9(outAt.start) : null,
7952
+ focus: d.focus ? { cx: round9(d.focus.cx), cy: round9(d.focus.cy) } : null,
7379
7953
  rect: d.rect ? {
7380
- x: round8(d.rect.x),
7381
- y: round8(d.rect.y),
7382
- w: round8(d.rect.w),
7383
- h: round8(d.rect.h)
7954
+ x: round9(d.rect.x),
7955
+ y: round9(d.rect.y),
7956
+ w: round9(d.rect.w),
7957
+ h: round9(d.rect.h)
7384
7958
  } : null,
7385
7959
  ...d.clicks !== void 0 ? { clicks: d.clicks } : {},
7386
7960
  ...d.surface ? { surface: true } : {},
@@ -7401,7 +7975,7 @@ function activityOf(bins, d) {
7401
7975
  sum += bins[i];
7402
7976
  n++;
7403
7977
  }
7404
- return n ? round8(sum / n) : null;
7978
+ return n ? round9(sum / n) : null;
7405
7979
  }
7406
7980
  function saidOver(transcript, d) {
7407
7981
  if (!transcript?.length) return null;
@@ -7410,7 +7984,7 @@ function saidOver(transcript, d) {
7410
7984
  const text = transcript.filter((s) => s.start < hi && s.end > lo).map((s) => s.text.trim()).filter(Boolean).join(" ");
7411
7985
  return text || null;
7412
7986
  }
7413
- function round8(v) {
7987
+ function round9(v) {
7414
7988
  return Math.round(v * 1e3) / 1e3;
7415
7989
  }
7416
7990
 
@@ -7904,8 +8478,8 @@ function buildDigest(input) {
7904
8478
  return {
7905
8479
  digestVersion: DIGEST_VERSION,
7906
8480
  take: {
7907
- sourceDuration: round9(meta.durationMs / 1e3),
7908
- outputDuration: round9(input.outputDuration),
8481
+ sourceDuration: round10(meta.durationMs / 1e3),
8482
+ outputDuration: round10(input.outputDuration),
7909
8483
  width: meta.width,
7910
8484
  height: meta.height,
7911
8485
  captureWidth: meta.captureWidth ?? null,
@@ -7954,17 +8528,17 @@ function buildDigest(input) {
7954
8528
  function outputDurationOf(doc) {
7955
8529
  return docOutputDuration(doc);
7956
8530
  }
7957
- function round9(v) {
8531
+ function round10(v) {
7958
8532
  return Math.round(v * 1e3) / 1e3;
7959
8533
  }
7960
8534
 
7961
8535
  // src/timeline/rangeActions.ts
7962
- import { mapTime } from "@vosjs/timeline";
8536
+ import { mapTime as mapTime2 } from "@vosjs/timeline";
7963
8537
  function outputRangeToSource(doc, t0, t1) {
7964
8538
  const rated = ratedSegments(doc);
7965
8539
  return {
7966
- srcIn: mapTime(rated, Math.max(0, t0)),
7967
- srcOut: mapTime(rated, Math.max(0, t1))
8540
+ srcIn: mapTime2(rated, Math.max(0, t0)),
8541
+ srcOut: mapTime2(rated, Math.max(0, t1))
7968
8542
  };
7969
8543
  }
7970
8544
  var EPS = 1e-6;
@@ -7980,20 +8554,20 @@ function setSpeedInRange(spans, srcIn, srcOut, rate) {
7980
8554
  continue;
7981
8555
  }
7982
8556
  if (srcIn - s.in >= MIN_REMAINDER)
7983
- kept.push({ ...s, out: round10(srcIn), source: "manual" });
8557
+ kept.push({ ...s, out: round11(srcIn), source: "manual" });
7984
8558
  if (s.out - srcOut >= MIN_REMAINDER)
7985
8559
  kept.push({
7986
8560
  ...s,
7987
8561
  id: mintId(used),
7988
- in: round10(srcOut),
8562
+ in: round11(srcOut),
7989
8563
  source: "manual"
7990
8564
  });
7991
8565
  }
7992
8566
  if (rate != null)
7993
8567
  kept.push({
7994
8568
  id: mintId(used),
7995
- in: round10(srcIn),
7996
- out: round10(srcOut),
8569
+ in: round11(srcIn),
8570
+ out: round11(srcOut),
7997
8571
  rate: clampSpeedRate(rate),
7998
8572
  source: "manual"
7999
8573
  });
@@ -8009,9 +8583,9 @@ function removeSourceRange(segments, srcIn, srcOut) {
8009
8583
  next.push(seg);
8010
8584
  continue;
8011
8585
  }
8012
- if (srcIn - seg.in >= MIN_SEGMENT) next.push({ ...seg, out: round10(srcIn) });
8586
+ if (srcIn - seg.in >= MIN_SEGMENT) next.push({ ...seg, out: round11(srcIn) });
8013
8587
  if (seg.out - srcOut >= MIN_SEGMENT)
8014
- next.push({ ...seg, in: round10(srcOut) });
8588
+ next.push({ ...seg, in: round11(srcOut) });
8015
8589
  }
8016
8590
  return next.length ? next : [...segments];
8017
8591
  }
@@ -8029,8 +8603,8 @@ function zoomSpanForRange(zoom, srcIn, srcOut) {
8029
8603
  while (used.has(`u${n}`)) n++;
8030
8604
  return {
8031
8605
  id: `u${n}`,
8032
- in: round10(start),
8033
- out: round10(end),
8606
+ in: round11(start),
8607
+ out: round11(end),
8034
8608
  level: DEFAULT_ZOOM_LEVEL,
8035
8609
  cx: 0.5,
8036
8610
  cy: 0.5,
@@ -8044,7 +8618,7 @@ function mintId(used) {
8044
8618
  used.add(id);
8045
8619
  return id;
8046
8620
  }
8047
- function round10(v) {
8621
+ function round11(v) {
8048
8622
  return Math.round(v * 1e3) / 1e3;
8049
8623
  }
8050
8624
 
@@ -8729,7 +9303,7 @@ function applyLook(frame, look, size, video, placement) {
8729
9303
 
8730
9304
  // src/timeline/lanes.ts
8731
9305
  import {
8732
- mapTime as mapTime2,
9306
+ mapTime as mapTime3,
8733
9307
  removeSegment,
8734
9308
  segmentRate as segmentRate3,
8735
9309
  sourceToTimeline as sourceToTimeline2,
@@ -8748,8 +9322,8 @@ var videoLane = {
8748
9322
  return segments.map((s, i) => ({
8749
9323
  id: `seg-${i}`,
8750
9324
  kind: "clip",
8751
- t: round11(starts[i]),
8752
- duration: round11(
9325
+ t: round12(starts[i]),
9326
+ duration: round12(
8753
9327
  outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i))
8754
9328
  )
8755
9329
  }));
@@ -8797,7 +9371,7 @@ var videoLane = {
8797
9371
  index === 0 || !contiguous(segments, index)
8798
9372
  );
8799
9373
  const anchorOut = sourceToTimeline2(fullMap, Math.min(edgeSrc, sourceDuration)) ?? edgeSrc;
8800
- const sourceT = mapTime2(fullMap, anchorOut + (g.t - edgeOutNow));
9374
+ const sourceT = mapTime3(fullMap, anchorOut + (g.t - edgeOutNow));
8801
9375
  const next = trimSegment(
8802
9376
  segments,
8803
9377
  index,
@@ -8832,8 +9406,8 @@ var videoLane = {
8832
9406
  if (local >= m.t - 1e-9 && local <= m.t + m.duration + 1e-9)
8833
9407
  return null;
8834
9408
  }
8835
- const sourceT = round11(
8836
- mapTime2(segmentPieces(s, speeds, freezes, leading), local)
9409
+ const sourceT = round12(
9410
+ mapTime3(segmentPieces(s, speeds, freezes, leading), local)
8837
9411
  );
8838
9412
  if (sourceT - s.in < 0.05 || s.out - sourceT < 0.05) return null;
8839
9413
  const next = [
@@ -8873,7 +9447,7 @@ var videoLane = {
8873
9447
  n === 1 || !contiguous(segments, n - 1)
8874
9448
  )
8875
9449
  ] : []
8876
- ].map(round11);
9450
+ ].map(round12);
8877
9451
  }
8878
9452
  };
8879
9453
  function nextFreezeId(doc) {
@@ -8900,8 +9474,8 @@ var freezeLane = {
8900
9474
  {
8901
9475
  id: f.id,
8902
9476
  kind: "clip",
8903
- t: round11(m.t),
8904
- duration: round11(m.duration),
9477
+ t: round12(m.t),
9478
+ duration: round12(m.duration),
8905
9479
  label: `${trimZeros(f.seconds)}s`
8906
9480
  }
8907
9481
  ] : [];
@@ -8923,7 +9497,7 @@ var freezeLane = {
8923
9497
  if (g.t >= m.t - 1e-9 && g.t <= m.t + m.duration + 1e-9) return null;
8924
9498
  }
8925
9499
  if (g.t > totalDuration7(rated) + 1e-9) return null;
8926
- const srcT = round11(mapTime2(rated, Math.max(0, g.t)));
9500
+ const srcT = round12(mapTime3(rated, Math.max(0, g.t)));
8927
9501
  if (!kept(srcT) || !clear(srcT)) return null;
8928
9502
  const id = nextFreezeId(doc);
8929
9503
  const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
@@ -8965,8 +9539,8 @@ var freezeLane = {
8965
9539
  const marks = freezeOutputExtents(splitBySpeed4(segs, speeds), freezes);
8966
9540
  const mine = marks.get(f.id);
8967
9541
  const endOut = mine ? mine.t + mine.duration : null;
8968
- const srcT = round11(
8969
- Math.min(sourceDuration, mapTime2(base, Math.max(0, g.t)))
9542
+ const srcT = round12(
9543
+ Math.min(sourceDuration, mapTime3(base, Math.max(0, g.t)))
8970
9544
  );
8971
9545
  if (!kept(srcT) || !clear(srcT, f.id)) return null;
8972
9546
  return (d) => {
@@ -9025,8 +9599,8 @@ var zoomLane = {
9025
9599
  {
9026
9600
  id: z.id,
9027
9601
  kind: "clip",
9028
- t: round11(ext.start),
9029
- duration: round11(ext.end - ext.start),
9602
+ t: round12(ext.start),
9603
+ duration: round12(ext.end - ext.start),
9030
9604
  label: `${z.level.toFixed(2)}\xD7`
9031
9605
  }
9032
9606
  ];
@@ -9037,7 +9611,7 @@ var zoomLane = {
9037
9611
  const rated = ratedSegments(doc);
9038
9612
  const sourceDuration = anchorSourceDuration(doc);
9039
9613
  if (g.type === "create") {
9040
- const srcT = mapTime2(rated, Math.max(0, g.t));
9614
+ const srcT = mapTime3(rated, Math.max(0, g.t));
9041
9615
  const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
9042
9616
  const here = spans.filter((z) => (z.media ?? "") === mediaHere);
9043
9617
  const mediaLen = mediaDuration(mediaSource(doc, mediaHere) ?? doc.source);
@@ -9053,8 +9627,8 @@ var zoomLane = {
9053
9627
  ...d.zoom,
9054
9628
  {
9055
9629
  id,
9056
- in: round11(srcT),
9057
- out: round11(srcT + len),
9630
+ in: round12(srcT),
9631
+ out: round12(srcT + len),
9058
9632
  level: DEFAULT_ZOOM_LEVEL,
9059
9633
  cx,
9060
9634
  cy,
@@ -9071,7 +9645,7 @@ var zoomLane = {
9071
9645
  const len = sp.out - sp.in;
9072
9646
  let newIn = clampToKept(
9073
9647
  effectiveSegments(doc),
9074
- mapTime2(rated, Math.max(0, g.t)),
9648
+ mapTime3(rated, Math.max(0, g.t)),
9075
9649
  len
9076
9650
  );
9077
9651
  for (const o of others) {
@@ -9086,8 +9660,8 @@ var zoomLane = {
9086
9660
  return (d) => {
9087
9661
  const z = d.zoom.find((x) => x.id === g.id);
9088
9662
  if (!z) return;
9089
- z.in = round11(newIn);
9090
- z.out = round11(newIn + len);
9663
+ z.in = round12(newIn);
9664
+ z.out = round12(newIn + len);
9091
9665
  z.source = "manual";
9092
9666
  d.zoom.sort((a, b) => a.in - b.in);
9093
9667
  };
@@ -9101,7 +9675,7 @@ var zoomLane = {
9101
9675
  sourceDuration,
9102
9676
  ...others.filter((o) => o.in >= sp.out).map((o) => o.in)
9103
9677
  );
9104
- const sourceT = mapTime2(rated, Math.max(0, g.t));
9678
+ const sourceT = mapTime3(rated, Math.max(0, g.t));
9105
9679
  const minSrc = Math.min(
9106
9680
  ZOOM_SPAN_MIN * rateAt(rated, sp.in),
9107
9681
  sp.out - sp.in
@@ -9116,8 +9690,8 @@ var zoomLane = {
9116
9690
  return (d) => {
9117
9691
  const z = d.zoom.find((x) => x.id === g.id);
9118
9692
  if (!z) return;
9119
- z.in = round11(next.in);
9120
- z.out = round11(next.out);
9693
+ z.in = round12(next.in);
9694
+ z.out = round12(next.out);
9121
9695
  z.source = "manual";
9122
9696
  };
9123
9697
  }
@@ -9139,8 +9713,8 @@ function cursorFocusAt(doc, srcT) {
9139
9713
  if (Math.abs(e.t - ms) < Math.abs(best.t - ms)) best = e;
9140
9714
  const clamp015 = (v) => Math.max(0, Math.min(1, v));
9141
9715
  return {
9142
- cx: round11(clamp015(best.x / width)),
9143
- cy: round11(clamp015(best.y / height))
9716
+ cx: round12(clamp015(best.x / width)),
9717
+ cy: round12(clamp015(best.y / height))
9144
9718
  };
9145
9719
  }
9146
9720
  var tiltLane = {
@@ -9154,8 +9728,8 @@ var tiltLane = {
9154
9728
  {
9155
9729
  id: z.id,
9156
9730
  kind: "clip",
9157
- t: round11(ext.start),
9158
- duration: round11(ext.end - ext.start),
9731
+ t: round12(ext.start),
9732
+ duration: round12(ext.end - ext.start),
9159
9733
  label: `${formatDeg(z.rx)}\xB0/${formatDeg(z.ry)}\xB0`
9160
9734
  }
9161
9735
  ];
@@ -9166,7 +9740,7 @@ var tiltLane = {
9166
9740
  const rated = ratedSegments(doc);
9167
9741
  const sourceDuration = anchorSourceDuration(doc);
9168
9742
  if (g.type === "create") {
9169
- const srcT = mapTime2(rated, Math.max(0, g.t));
9743
+ const srcT = mapTime3(rated, Math.max(0, g.t));
9170
9744
  const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
9171
9745
  const here = spans.filter((z) => (z.media ?? "") === mediaHere);
9172
9746
  const mediaLen = mediaDuration(mediaSource(doc, mediaHere) ?? doc.source);
@@ -9181,8 +9755,8 @@ var tiltLane = {
9181
9755
  ...d.tilt ?? [],
9182
9756
  {
9183
9757
  id,
9184
- in: round11(srcT),
9185
- out: round11(srcT + len),
9758
+ in: round12(srcT),
9759
+ out: round12(srcT + len),
9186
9760
  rx: DEFAULT_TILT_POSE.rx,
9187
9761
  ry: DEFAULT_TILT_POSE.ry,
9188
9762
  source: "manual",
@@ -9198,7 +9772,7 @@ var tiltLane = {
9198
9772
  const len = sp.out - sp.in;
9199
9773
  let newIn = clampToKept(
9200
9774
  effectiveSegments(doc),
9201
- mapTime2(rated, Math.max(0, g.t)),
9775
+ mapTime3(rated, Math.max(0, g.t)),
9202
9776
  len
9203
9777
  );
9204
9778
  for (const o of others) {
@@ -9213,8 +9787,8 @@ var tiltLane = {
9213
9787
  return (d) => {
9214
9788
  const z = (d.tilt ?? []).find((x) => x.id === g.id);
9215
9789
  if (!z || !d.tilt) return;
9216
- z.in = round11(newIn);
9217
- z.out = round11(newIn + len);
9790
+ z.in = round12(newIn);
9791
+ z.out = round12(newIn + len);
9218
9792
  z.source = "manual";
9219
9793
  d.tilt.sort((a, b) => a.in - b.in);
9220
9794
  };
@@ -9228,7 +9802,7 @@ var tiltLane = {
9228
9802
  sourceDuration,
9229
9803
  ...others.filter((o) => o.in >= sp.out).map((o) => o.in)
9230
9804
  );
9231
- const sourceT = mapTime2(rated, Math.max(0, g.t));
9805
+ const sourceT = mapTime3(rated, Math.max(0, g.t));
9232
9806
  const minSrc = Math.min(
9233
9807
  TILT_SPAN_MIN * rateAt(rated, sp.in),
9234
9808
  sp.out - sp.in
@@ -9243,8 +9817,8 @@ var tiltLane = {
9243
9817
  return (d) => {
9244
9818
  const z = (d.tilt ?? []).find((x) => x.id === g.id);
9245
9819
  if (!z) return;
9246
- z.in = round11(next.in);
9247
- z.out = round11(next.out);
9820
+ z.in = round12(next.in);
9821
+ z.out = round12(next.out);
9248
9822
  z.source = "manual";
9249
9823
  };
9250
9824
  }
@@ -9271,8 +9845,8 @@ var camMoveLane = {
9271
9845
  {
9272
9846
  id: z.id,
9273
9847
  kind: "clip",
9274
- t: round11(ext.start),
9275
- duration: round11(ext.end - ext.start),
9848
+ t: round12(ext.start),
9849
+ duration: round12(ext.end - ext.start),
9276
9850
  label: z.size != null ? `${Math.round(z.size * 100)}%` : "Move"
9277
9851
  }
9278
9852
  ];
@@ -9284,7 +9858,7 @@ var camMoveLane = {
9284
9858
  const sourceDuration = anchorSourceDuration(doc);
9285
9859
  if (g.type === "create") {
9286
9860
  if (!doc.source.camKey) return null;
9287
- const srcT = mapTime2(rated, Math.max(0, g.t));
9861
+ const srcT = mapTime3(rated, Math.max(0, g.t));
9288
9862
  if (spans.some((z) => srcT >= z.in && srcT < z.out)) return null;
9289
9863
  const next = spans.filter((z) => z.in > srcT).sort((a, b) => a.in - b.in).at(0);
9290
9864
  const limit = Math.min(sourceDuration, next ? next.in : sourceDuration);
@@ -9296,8 +9870,8 @@ var camMoveLane = {
9296
9870
  ...d.camMotion ?? [],
9297
9871
  {
9298
9872
  id,
9299
- in: round11(srcT),
9300
- out: round11(srcT + len),
9873
+ in: round12(srcT),
9874
+ out: round12(srcT + len),
9301
9875
  ...DEFAULT_CAM_POSE,
9302
9876
  source: "manual"
9303
9877
  }
@@ -9311,7 +9885,7 @@ var camMoveLane = {
9311
9885
  const len = sp.out - sp.in;
9312
9886
  let newIn = clampToKept(
9313
9887
  effectiveSegments(doc),
9314
- mapTime2(rated, Math.max(0, g.t)),
9888
+ mapTime3(rated, Math.max(0, g.t)),
9315
9889
  len
9316
9890
  );
9317
9891
  for (const o of others) {
@@ -9326,8 +9900,8 @@ var camMoveLane = {
9326
9900
  return (d) => {
9327
9901
  const z = (d.camMotion ?? []).find((x) => x.id === g.id);
9328
9902
  if (!z || !d.camMotion) return;
9329
- z.in = round11(newIn);
9330
- z.out = round11(newIn + len);
9903
+ z.in = round12(newIn);
9904
+ z.out = round12(newIn + len);
9331
9905
  z.source = "manual";
9332
9906
  d.camMotion.sort((a, b) => a.in - b.in);
9333
9907
  };
@@ -9341,7 +9915,7 @@ var camMoveLane = {
9341
9915
  sourceDuration,
9342
9916
  ...others.filter((o) => o.in >= sp.out).map((o) => o.in)
9343
9917
  );
9344
- const sourceT = mapTime2(rated, Math.max(0, g.t));
9918
+ const sourceT = mapTime3(rated, Math.max(0, g.t));
9345
9919
  const minSrc = Math.min(
9346
9920
  CAM_SPAN_MIN * rateAt(rated, sp.in),
9347
9921
  sp.out - sp.in
@@ -9356,8 +9930,8 @@ var camMoveLane = {
9356
9930
  return (d) => {
9357
9931
  const z = (d.camMotion ?? []).find((x) => x.id === g.id);
9358
9932
  if (!z) return;
9359
- z.in = round11(next.in);
9360
- z.out = round11(next.out);
9933
+ z.in = round12(next.in);
9934
+ z.out = round12(next.out);
9361
9935
  z.source = "manual";
9362
9936
  };
9363
9937
  }
@@ -9390,10 +9964,10 @@ var camLane = {
9390
9964
  items.push({
9391
9965
  id: `cam-${i}`,
9392
9966
  kind: "clip",
9393
- t: round11(
9967
+ t: round12(
9394
9968
  starts[i] + outputLen({ in: s.in, out: a }, speeds, freezes, leading)
9395
9969
  ),
9396
- duration: round11(
9970
+ duration: round12(
9397
9971
  outputLen({ in: a, out: b }, speeds, freezes, a === s.in && leading)
9398
9972
  )
9399
9973
  });
@@ -9415,14 +9989,14 @@ var camLane = {
9415
9989
  if (!seg) return null;
9416
9990
  const clipSrcStart = Math.max(seg.in, Math.max(current.in, first.in));
9417
9991
  const span = Math.max(0.05, current.out - current.in);
9418
- const delta = mapTime2(rated, Math.max(0, g.t)) - clipSrcStart;
9992
+ const delta = mapTime3(rated, Math.max(0, g.t)) - clipSrcStart;
9419
9993
  const base = Math.max(first.in, current.in);
9420
9994
  const newIn = Math.min(
9421
9995
  Math.max(first.in, base + delta),
9422
9996
  Math.max(first.in, last.out - span)
9423
9997
  );
9424
9998
  return (d) => {
9425
- d.cam.window = { in: round11(newIn), out: round11(newIn + span) };
9999
+ d.cam.window = { in: round12(newIn), out: round12(newIn + span) };
9426
10000
  };
9427
10001
  }
9428
10002
  const overlapping = eff.map((s, i) => ({
@@ -9431,12 +10005,12 @@ var camLane = {
9431
10005
  })).filter((x) => x.len > 1e-6);
9432
10006
  const edgeIdx = g.edge === "start" ? overlapping.at(0)?.i : overlapping.at(-1)?.i;
9433
10007
  if (edgeIdx === void 0 || g.id !== `cam-${edgeIdx}`) return null;
9434
- const sourceT = Math.min(Math.max(mapTime2(rated, g.t), 0), sourceDuration);
10008
+ const sourceT = Math.min(Math.max(mapTime3(rated, g.t), 0), sourceDuration);
9435
10009
  const next = g.edge === "start" ? { in: Math.min(sourceT, current.out - 0.05), out: current.out } : { in: current.in, out: Math.max(sourceT, current.in + 0.05) };
9436
10010
  return (d) => {
9437
10011
  d.cam.window = {
9438
- in: round11(Math.max(0, next.in)),
9439
- out: round11(Math.min(sourceDuration, next.out))
10012
+ in: round12(Math.max(0, next.in)),
10013
+ out: round12(Math.min(sourceDuration, next.out))
9440
10014
  };
9441
10015
  };
9442
10016
  },
@@ -9456,8 +10030,8 @@ var micLane = {
9456
10030
  return segments.map((s, i) => ({
9457
10031
  id: `mic-${i}`,
9458
10032
  kind: "clip",
9459
- t: round11(starts[i]),
9460
- duration: round11(
10033
+ t: round12(starts[i]),
10034
+ duration: round12(
9461
10035
  outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i))
9462
10036
  )
9463
10037
  }));
@@ -9513,8 +10087,8 @@ var speedLane = {
9513
10087
  {
9514
10088
  id: sp.id,
9515
10089
  kind: "clip",
9516
- t: round11(start),
9517
- duration: round11(end - start),
10090
+ t: round12(start),
10091
+ duration: round12(end - start),
9518
10092
  label: `${sp.rate}\xD7`
9519
10093
  }
9520
10094
  ];
@@ -9525,7 +10099,7 @@ var speedLane = {
9525
10099
  const rated = ratedSegments(doc);
9526
10100
  const sourceDuration = anchorSourceDuration(doc);
9527
10101
  if (g.type === "create") {
9528
- const srcT = mapTime2(rated, Math.max(0, g.t));
10102
+ const srcT = mapTime3(rated, Math.max(0, g.t));
9529
10103
  if (spans.some((s) => srcT >= s.in && srcT < s.out)) return null;
9530
10104
  const next = spans.filter((s) => s.in > srcT).sort((a, b) => a.in - b.in).at(0);
9531
10105
  const limit = Math.min(sourceDuration, next ? next.in : sourceDuration);
@@ -9538,8 +10112,8 @@ var speedLane = {
9538
10112
  ...d.speed ?? [],
9539
10113
  {
9540
10114
  id,
9541
- in: round11(srcT),
9542
- out: round11(srcT + len),
10115
+ in: round12(srcT),
10116
+ out: round12(srcT + len),
9543
10117
  rate: DEFAULT_SPEED_RATE,
9544
10118
  source: "manual",
9545
10119
  ...mediaHere ? { media: mediaHere } : {}
@@ -9555,7 +10129,7 @@ var speedLane = {
9555
10129
  const len = sp.out - sp.in;
9556
10130
  let newIn = clampToKept(
9557
10131
  effectiveSegments(doc),
9558
- mapTime2(base, Math.max(0, g.t)),
10132
+ mapTime3(base, Math.max(0, g.t)),
9559
10133
  len
9560
10134
  );
9561
10135
  for (const o of others) {
@@ -9570,8 +10144,8 @@ var speedLane = {
9570
10144
  return (d) => {
9571
10145
  const s = d.speed?.find((x) => x.id === g.id);
9572
10146
  if (!s) return;
9573
- s.in = round11(newIn);
9574
- s.out = round11(newIn + len);
10147
+ s.in = round12(newIn);
10148
+ s.out = round12(newIn + len);
9575
10149
  s.source = "manual";
9576
10150
  d.speed.sort((a, b) => a.in - b.in);
9577
10151
  };
@@ -9584,7 +10158,7 @@ var speedLane = {
9584
10158
  const minSrc = Math.min(SPEED_SPAN_MIN * sp.rate, sp.out - sp.in);
9585
10159
  let next;
9586
10160
  if (g.edge === "start") {
9587
- const sourceT = mapTime2(base, Math.max(0, g.t));
10161
+ const sourceT = mapTime3(base, Math.max(0, g.t));
9588
10162
  next = {
9589
10163
  in: Math.min(Math.max(lo, sourceT), sp.out - minSrc),
9590
10164
  out: sp.out
@@ -9600,8 +10174,8 @@ var speedLane = {
9600
10174
  return (d) => {
9601
10175
  const s = d.speed?.find((x) => x.id === g.id);
9602
10176
  if (!s) return;
9603
- s.in = round11(next.in);
9604
- s.out = round11(next.out);
10177
+ s.in = round12(next.in);
10178
+ s.out = round12(next.out);
9605
10179
  s.source = "manual";
9606
10180
  };
9607
10181
  }
@@ -9620,8 +10194,8 @@ var audioLane = {
9620
10194
  return doc.audio.map((c) => ({
9621
10195
  id: c.id,
9622
10196
  kind: "clip",
9623
- t: round11(c.start),
9624
- duration: round11(clipLength(c)),
10197
+ t: round12(c.start),
10198
+ duration: round12(clipLength(c)),
9625
10199
  label: c.name
9626
10200
  }));
9627
10201
  },
@@ -9632,7 +10206,7 @@ var audioLane = {
9632
10206
  const start = Math.max(0, g.t);
9633
10207
  return (d) => {
9634
10208
  const c = d.audio.find((x) => x.id === g.id);
9635
- if (c) c.start = round11(start);
10209
+ if (c) c.start = round12(start);
9636
10210
  };
9637
10211
  }
9638
10212
  if (g.type === "resize") {
@@ -9645,8 +10219,8 @@ var audioLane = {
9645
10219
  return (d) => {
9646
10220
  const c = d.audio.find((x) => x.id === g.id);
9647
10221
  if (!c) return;
9648
- c.start = round11(newStart2);
9649
- c.loopLen = round11(end - newStart2);
10222
+ c.start = round12(newStart2);
10223
+ c.loopLen = round12(end - newStart2);
9650
10224
  };
9651
10225
  }
9652
10226
  const delta = g.t - clip.start;
@@ -9655,22 +10229,22 @@ var audioLane = {
9655
10229
  return (d) => {
9656
10230
  const c = d.audio.find((x) => x.id === g.id);
9657
10231
  if (!c) return;
9658
- c.in = round11(newIn);
9659
- c.start = round11(newStart);
10232
+ c.in = round12(newIn);
10233
+ c.start = round12(newStart);
9660
10234
  };
9661
10235
  }
9662
10236
  if (clip.loop) {
9663
10237
  const newLen = Math.max(0.1, g.t - clip.start);
9664
10238
  return (d) => {
9665
10239
  const c = d.audio.find((x) => x.id === g.id);
9666
- if (c) c.loopLen = round11(newLen);
10240
+ if (c) c.loopLen = round12(newLen);
9667
10241
  };
9668
10242
  }
9669
10243
  const span = Math.max(0.05, g.t - clip.start);
9670
10244
  const newOut = Math.min(clip.in + span, clip.duration);
9671
10245
  return (d) => {
9672
10246
  const c = d.audio.find((x) => x.id === g.id);
9673
- if (c) c.out = round11(Math.max(c.in + 0.05, newOut));
10247
+ if (c) c.out = round12(Math.max(c.in + 0.05, newOut));
9674
10248
  };
9675
10249
  }
9676
10250
  if (g.type === "remove") {
@@ -9683,8 +10257,8 @@ var audioLane = {
9683
10257
  },
9684
10258
  magnets(doc) {
9685
10259
  return doc.audio.flatMap((c) => [
9686
- round11(c.start),
9687
- round11(c.start + clipLength(c))
10260
+ round12(c.start),
10261
+ round12(c.start + clipLength(c))
9688
10262
  ]);
9689
10263
  }
9690
10264
  };
@@ -9702,7 +10276,7 @@ function poseItems(clipId, start, duration, motion) {
9702
10276
  return (motion ?? []).map((p, i) => ({
9703
10277
  id: `${clipId}::k${i}`,
9704
10278
  kind: "keyframe",
9705
- t: round11(start + Math.min(Math.max(0, p.at), duration))
10279
+ t: round12(start + Math.min(Math.max(0, p.at), duration))
9706
10280
  }));
9707
10281
  }
9708
10282
  var overlaysLane = {
@@ -9713,8 +10287,8 @@ var overlaysLane = {
9713
10287
  {
9714
10288
  id: o.id,
9715
10289
  kind: "clip",
9716
- t: round11(o.start),
9717
- duration: round11(o.duration),
10290
+ t: round12(o.start),
10291
+ duration: round12(o.duration),
9718
10292
  label: o.kind === "text" ? o.text.split("\n")[0].slice(0, 24) || "Text" : o.kind === "html" ? htmlLayerLabel(o) : o.kind === "image" ? "Image" : "Video"
9719
10293
  },
9720
10294
  // Pose diamonds render after the clips, so they sit on top.
@@ -9734,7 +10308,7 @@ var overlaysLane = {
9734
10308
  return (d) => {
9735
10309
  const o = (d.overlays ?? []).find((x) => x.id === kf.clipId);
9736
10310
  const p = o?.motion?.[kf.index];
9737
- if (p) p.at = round11(at);
10311
+ if (p) p.at = round12(at);
9738
10312
  };
9739
10313
  }
9740
10314
  if (g.type === "remove") {
@@ -9763,8 +10337,8 @@ var overlaysLane = {
9763
10337
  {
9764
10338
  id,
9765
10339
  kind: "text",
9766
- start: round11(start),
9767
- duration: round11(len),
10340
+ start: round12(start),
10341
+ duration: round12(len),
9768
10342
  text: "Title",
9769
10343
  preset: "title",
9770
10344
  // Frame fractions: centered, lower-third — at any aspect ratio.
@@ -9779,7 +10353,7 @@ var overlaysLane = {
9779
10353
  const start = Math.max(0, g.t);
9780
10354
  return (d) => {
9781
10355
  const o = (d.overlays ?? []).find((x) => x.id === g.id);
9782
- if (o) o.start = round11(start);
10356
+ if (o) o.start = round12(start);
9783
10357
  };
9784
10358
  }
9785
10359
  if (g.type === "resize") {
@@ -9789,14 +10363,14 @@ var overlaysLane = {
9789
10363
  return (d) => {
9790
10364
  const o = (d.overlays ?? []).find((x) => x.id === g.id);
9791
10365
  if (!o) return;
9792
- o.start = round11(newStart);
9793
- o.duration = round11(end - newStart);
10366
+ o.start = round12(newStart);
10367
+ o.duration = round12(end - newStart);
9794
10368
  };
9795
10369
  }
9796
10370
  const newLen = Math.max(OVERLAY_MIN_DURATION, g.t - clip.start);
9797
10371
  return (d) => {
9798
10372
  const o = (d.overlays ?? []).find((x) => x.id === g.id);
9799
- if (o) o.duration = round11(newLen);
10373
+ if (o) o.duration = round12(newLen);
9800
10374
  };
9801
10375
  }
9802
10376
  return (d) => {
@@ -9805,8 +10379,8 @@ var overlaysLane = {
9805
10379
  },
9806
10380
  magnets(doc) {
9807
10381
  return (doc.overlays ?? []).flatMap((o) => [
9808
- round11(o.start),
9809
- round11(o.start + o.duration)
10382
+ round12(o.start),
10383
+ round12(o.start + o.duration)
9810
10384
  ]);
9811
10385
  }
9812
10386
  };
@@ -9819,8 +10393,8 @@ var objectsLane = {
9819
10393
  {
9820
10394
  id: o.id,
9821
10395
  kind: "clip",
9822
- t: round11(o.span?.start ?? 0),
9823
- duration: round11(o.span?.duration ?? outDur),
10396
+ t: round12(o.span?.start ?? 0),
10397
+ duration: round12(o.span?.duration ?? outDur),
9824
10398
  label: o.asset.kind === "primitive" ? o.asset.shape : o.asset.kind === "text3d" ? o.asset.text : "model"
9825
10399
  },
9826
10400
  // Pose diamonds: clip-local `at` from the span start (0 span-less).
@@ -9848,7 +10422,7 @@ var objectsLane = {
9848
10422
  return (d) => {
9849
10423
  const o = (d.objects ?? []).find((x) => x.id === kf.clipId);
9850
10424
  const p = o?.motion?.[kf.index];
9851
- if (p) p.at = round11(at);
10425
+ if (p) p.at = round12(at);
9852
10426
  };
9853
10427
  }
9854
10428
  if (g.type === "remove") {
@@ -9869,7 +10443,7 @@ var objectsLane = {
9869
10443
  const start = Math.max(0, g.t);
9870
10444
  return (d) => {
9871
10445
  const o = (d.objects ?? []).find((x) => x.id === g.id);
9872
- if (o?.span) o.span.start = round11(start);
10446
+ if (o?.span) o.span.start = round12(start);
9873
10447
  };
9874
10448
  }
9875
10449
  if (g.type === "resize") {
@@ -9880,14 +10454,14 @@ var objectsLane = {
9880
10454
  return (d) => {
9881
10455
  const o = (d.objects ?? []).find((x) => x.id === g.id);
9882
10456
  if (!o?.span) return;
9883
- o.span.start = round11(newStart);
9884
- o.span.duration = round11(end - newStart);
10457
+ o.span.start = round12(newStart);
10458
+ o.span.duration = round12(end - newStart);
9885
10459
  };
9886
10460
  }
9887
10461
  const newLen = Math.max(OVERLAY_MIN_DURATION, g.t - clip.span.start);
9888
10462
  return (d) => {
9889
10463
  const o = (d.objects ?? []).find((x) => x.id === g.id);
9890
- if (o?.span) o.span.duration = round11(newLen);
10464
+ if (o?.span) o.span.duration = round12(newLen);
9891
10465
  };
9892
10466
  }
9893
10467
  return (d) => {
@@ -9896,7 +10470,7 @@ var objectsLane = {
9896
10470
  },
9897
10471
  magnets(doc) {
9898
10472
  return (doc.objects ?? []).flatMap(
9899
- (o) => o.span ? [round11(o.span.start), round11(o.span.start + o.span.duration)] : []
10473
+ (o) => o.span ? [round12(o.span.start), round12(o.span.start + o.span.duration)] : []
9900
10474
  );
9901
10475
  }
9902
10476
  };
@@ -9923,7 +10497,7 @@ function nextSpeedId(doc) {
9923
10497
  function segIndex(id) {
9924
10498
  return Number(id.replace("seg-", ""));
9925
10499
  }
9926
- function round11(v) {
10500
+ function round12(v) {
9927
10501
  return Math.round(v * 1e3) / 1e3;
9928
10502
  }
9929
10503
 
@@ -9952,7 +10526,7 @@ function computePeaks(channels, buckets) {
9952
10526
  }
9953
10527
 
9954
10528
  // src/lower/duckCurve.ts
9955
- import { mapTime as mapTime3 } from "@vosjs/timeline";
10529
+ import { mapTime as mapTime4 } from "@vosjs/timeline";
9956
10530
  var DEFAULT_DUCK = {
9957
10531
  threshold: 0.02,
9958
10532
  duckTo: 0.25,
@@ -9988,7 +10562,7 @@ function duckCurve(rms, segments, durationSec2, opts = DEFAULT_DUCK) {
9988
10562
  const steps = Math.ceil(durationSec2 * opts.gridHz);
9989
10563
  for (let i = 0; i <= steps; i++) {
9990
10564
  const t = Math.min(durationSec2, i * dt);
9991
- const srcT = mapTime3(segments, t);
10565
+ const srcT = mapTime4(segments, t);
9992
10566
  const w = Math.min(
9993
10567
  rms.values.length - 1,
9994
10568
  Math.max(0, Math.floor(srcT * rms.rate))
@@ -10164,6 +10738,8 @@ export {
10164
10738
  OVERLAY_SIZE_MIN,
10165
10739
  OVERLAY_TRANSITION_DUR,
10166
10740
  OVERLAY_Z,
10741
+ PIN_GAP,
10742
+ PIN_MARGIN,
10167
10743
  PLATE_GROUND,
10168
10744
  PLAYBACK_ACTIVITY,
10169
10745
  PRIMARY_MEDIA,
@@ -10272,6 +10848,7 @@ export {
10272
10848
  docToCropSpace,
10273
10849
  docToFullSpace,
10274
10850
  docTransitions,
10851
+ docZoomTrack,
10275
10852
  dragsFromTrack,
10276
10853
  dropTemplate,
10277
10854
  duckCurve,
@@ -10289,6 +10866,7 @@ export {
10289
10866
  expectedFrameSize,
10290
10867
  exportSizeFor,
10291
10868
  extractClicks,
10869
+ findStep,
10292
10870
  focusBounds,
10293
10871
  focusForViewportCentre,
10294
10872
  followFocusEvents,
@@ -10360,12 +10938,18 @@ export {
10360
10938
  overlayLines,
10361
10939
  overlayMotionBase,
10362
10940
  overlayMotionPoseAt,
10941
+ overlayPinPoseAt,
10363
10942
  overlayRect,
10364
10943
  overlaysLane,
10365
10944
  ownFreezes,
10366
10945
  pageDisplayUrl,
10367
10946
  parsePoseId,
10368
10947
  pickStyle,
10948
+ pinBox,
10949
+ pinPlacement,
10950
+ pinRectOnScreen,
10951
+ pinReferent,
10952
+ pinReferentAt,
10369
10953
  placeFreezes,
10370
10954
  planAutoSpeed,
10371
10955
  planAutoTilt,
@@ -10384,6 +10968,7 @@ export {
10384
10968
  resolveExportSize,
10385
10969
  resolveOverlayBox,
10386
10970
  resolveOverlayStyle,
10971
+ resolvePins,
10387
10972
  resolveText3dAsset,
10388
10973
  resolveZoomStyle,
10389
10974
  restLeanOf,