@vosjs/studio-core 0.23.1 → 0.24.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/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,280 @@ function round2(v) {
3057
3057
  return Math.round(v * 1e3) / 1e3;
3058
3058
  }
3059
3059
 
3060
+ // src/lower/pin.ts
3061
+ import { lerpArray, 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
+ if (step.rect && validRect(step.rect) && w > 0 && h > 0) {
3081
+ return {
3082
+ rect: normRect(step.rect, w, h),
3083
+ by: "step",
3084
+ step,
3085
+ at: step.tStart
3086
+ };
3087
+ }
3088
+ const union = unionRects(
3089
+ pressRects(doc, step.tStart - 0.05, step.tEnd + 0.05)
3090
+ );
3091
+ if (!union || w <= 0 || h <= 0) return null;
3092
+ return { rect: normRect(union, w, h), by: "step", step, at: step.tStart };
3093
+ }
3094
+ if (pin.press !== void 0 && Number.isFinite(pin.press)) {
3095
+ let best = null;
3096
+ for (const e of doc.source.cursor) {
3097
+ if (e.type !== "down" || !e.rect || !validRect(e.rect)) continue;
3098
+ const t = e.t / 1e3;
3099
+ const d = Math.abs(t - pin.press);
3100
+ if (d > PIN_PRESS_WINDOW_S) continue;
3101
+ if (!best || d < Math.abs(best.t - pin.press)) best = { t, rect: e.rect };
3102
+ }
3103
+ if (!best || w <= 0 || h <= 0) return null;
3104
+ return { rect: normRect(best.rect, w, h), by: "press", at: best.t };
3105
+ }
3106
+ return null;
3107
+ }
3108
+ function findStep(steps, ref) {
3109
+ const byId = steps.find((s) => s.id !== void 0 && s.id === ref);
3110
+ if (byId) return byId;
3111
+ const idx = typeof ref === "number" ? ref : Number(ref);
3112
+ if (!Number.isInteger(idx)) return void 0;
3113
+ return steps.find((s) => s.step === idx);
3114
+ }
3115
+ function pressRects(doc, from, to) {
3116
+ const out = [];
3117
+ for (const e of doc.source.cursor) {
3118
+ if (e.type !== "down" && e.type !== "key") continue;
3119
+ if (!e.rect || !validRect(e.rect)) continue;
3120
+ const t = e.t / 1e3;
3121
+ if (t < from || t > to) continue;
3122
+ out.push(e.rect);
3123
+ }
3124
+ return out;
3125
+ }
3126
+ function unionRects(rects) {
3127
+ if (!rects.length) return null;
3128
+ let x0 = Infinity;
3129
+ let y0 = Infinity;
3130
+ let x1 = -Infinity;
3131
+ let y1 = -Infinity;
3132
+ for (const r of rects) {
3133
+ x0 = Math.min(x0, r.x);
3134
+ y0 = Math.min(y0, r.y);
3135
+ x1 = Math.max(x1, r.x + r.w);
3136
+ y1 = Math.max(y1, r.y + r.h);
3137
+ }
3138
+ return { x: x0, y: y0, w: x1 - x0, h: y1 - y0 };
3139
+ }
3140
+ function validRect(r) {
3141
+ return Number.isFinite(r.x) && Number.isFinite(r.y) && Number.isFinite(r.w) && Number.isFinite(r.h) && r.w > 0 && r.h > 0;
3142
+ }
3143
+ function normRect(r, w, h) {
3144
+ return { x: r.x / w, y: r.y / h, w: r.w / w, h: r.h / h };
3145
+ }
3146
+ function pinBox(clip, layout) {
3147
+ const scale = clip.transform.scale || 1;
3148
+ if (clip.kind === "html") {
3149
+ const pb = htmlLayerPictureBox(clip);
3150
+ const wPic = htmlLayerWidth(clip) * layout.W * scale;
3151
+ const unit = wPic / pb.width;
3152
+ return { w: clip.box.width * unit, h: clip.box.height * unit };
3153
+ }
3154
+ if (clip.kind !== "text") {
3155
+ const w = (clip.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH) * layout.W * scale;
3156
+ return { w, h: w / (16 / 9) };
3157
+ }
3158
+ const r = overlayRect(clip, estimateMeasure, layout.W, layout.H);
3159
+ return { w: r.w, h: r.h };
3160
+ }
3161
+ function estimateMeasure(text, font, letterSpacingPx = 0) {
3162
+ const m = /(\d+(?:\.\d+)?)px/.exec(font);
3163
+ const px = m ? Number(m[1]) : 16;
3164
+ return text.length * (px * PIN_TEXT_ADVANCE + letterSpacingPx);
3165
+ }
3166
+ function pinRectOnScreen(rect, t, layout, camera, zoomTrack) {
3167
+ let level = 1;
3168
+ let cx = 0.5;
3169
+ let cy = 0.5;
3170
+ let centring;
3171
+ if (zoomTrack && zoomTrack.keyframes.length) {
3172
+ const z = sample(zoomTrack, t, lerpArray);
3173
+ level = z[0];
3174
+ cx = z[1];
3175
+ cy = z[2];
3176
+ if (z.length > 3) centring = z[3];
3177
+ }
3178
+ const v = zoomView(level, cx, cy, layout, camera, centring);
3179
+ const map = (nx, ny) => {
3180
+ const px = layout.dx + nx * layout.dw;
3181
+ const py = layout.dy + ny * layout.dh;
3182
+ return {
3183
+ x: v.ox + (px - v.wcx) * v.level,
3184
+ y: v.oy + (py - v.wcy) * v.level
3185
+ };
3186
+ };
3187
+ const a = map(rect.x, rect.y);
3188
+ const b = map(rect.x + rect.w, rect.y + rect.h);
3189
+ return { x: a.x, y: a.y, w: b.x - a.x, h: b.y - a.y };
3190
+ }
3191
+ function pinPlacement(input) {
3192
+ const { clip, referent, layout, camera, zoomTrack, base, motion } = input;
3193
+ const dur = Math.max(1e-3, clip.duration);
3194
+ const gap = clip.pin?.gap ?? PIN_GAP;
3195
+ const box = pinBox(clip, layout);
3196
+ const W = layout.W;
3197
+ const H = layout.H;
3198
+ const refAt = (local) => pinRectOnScreen(referent, clip.start + local, layout, camera, zoomTrack);
3199
+ const centreFor = (r, side2) => {
3200
+ switch (side2) {
3201
+ case "right":
3202
+ return { x: r.x + r.w + gap + box.w / 2, y: r.y + r.h / 2 };
3203
+ case "left":
3204
+ return { x: r.x - gap - box.w / 2, y: r.y + r.h / 2 };
3205
+ case "below":
3206
+ return { x: r.x + r.w / 2, y: r.y + r.h + gap + box.h / 2 };
3207
+ case "above":
3208
+ return { x: r.x + r.w / 2, y: r.y - gap - box.h / 2 };
3209
+ }
3210
+ };
3211
+ 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;
3212
+ const room = (c) => {
3213
+ const x0 = Math.max(PIN_MARGIN, c.x - box.w / 2);
3214
+ const x1 = Math.min(W - PIN_MARGIN, c.x + box.w / 2);
3215
+ const y0 = Math.max(PIN_MARGIN, c.y - box.h / 2);
3216
+ const y1 = Math.min(H - PIN_MARGIN, c.y + box.h / 2);
3217
+ return Math.max(0, x1 - x0) * Math.max(0, y1 - y0);
3218
+ };
3219
+ let side;
3220
+ const asked = clip.pin?.side ?? "auto";
3221
+ if (asked !== "auto") side = asked;
3222
+ else {
3223
+ const edge = Math.min(PIN_EDGE_S, dur / 3);
3224
+ const probes = [edge, dur / 2, dur - edge].map(refAt);
3225
+ const fitting = SIDE_ORDER.find(
3226
+ (s) => probes.every((r) => fits(centreFor(r, s)))
3227
+ );
3228
+ if (fitting) side = fitting;
3229
+ else {
3230
+ let best = SIDE_ORDER[0];
3231
+ let bestRoom = -1;
3232
+ for (const s of SIDE_ORDER) {
3233
+ const total = probes.reduce((acc, r) => acc + room(centreFor(r, s)), 0);
3234
+ if (total > bestRoom) {
3235
+ bestRoom = total;
3236
+ best = s;
3237
+ }
3238
+ }
3239
+ side = best;
3240
+ }
3241
+ }
3242
+ const clampC = (c) => {
3243
+ const minX = Math.min(W / 2, PIN_MARGIN + box.w / 2);
3244
+ const maxX = Math.max(W / 2, W - PIN_MARGIN - box.w / 2);
3245
+ const minY = Math.min(H / 2, PIN_MARGIN + box.h / 2);
3246
+ const maxY = Math.max(H / 2, H - PIN_MARGIN - box.h / 2);
3247
+ return {
3248
+ x: Math.max(minX, Math.min(maxX, c.x)),
3249
+ y: Math.max(minY, Math.min(maxY, c.y))
3250
+ };
3251
+ };
3252
+ const tipFor = (c) => {
3253
+ switch (side) {
3254
+ case "right":
3255
+ return { x: c.x - box.w / 2, y: c.y };
3256
+ case "left":
3257
+ return { x: c.x + box.w / 2, y: c.y };
3258
+ case "below":
3259
+ return { x: c.x, y: c.y - box.h / 2 };
3260
+ case "above":
3261
+ return { x: c.x, y: c.y + box.h / 2 };
3262
+ }
3263
+ };
3264
+ const n = Math.max(1, Math.ceil(dur * PIN_SAMPLE_HZ));
3265
+ const poses = [];
3266
+ const tips = [];
3267
+ let clamped = 0;
3268
+ for (let i = 0; i <= n; i++) {
3269
+ const local = i === n ? dur : i / PIN_SAMPLE_HZ;
3270
+ const want = centreFor(refAt(local), side);
3271
+ const c = clampC(want);
3272
+ clamped = Math.max(clamped, Math.hypot(c.x - want.x, c.y - want.y));
3273
+ const pose = motion?.keyframes.length ? sample(motion, local, lerpArray) : base;
3274
+ poses.push({
3275
+ t: local,
3276
+ value: [c.x / W, c.y / H, pose[2], pose[3], pose[4]],
3277
+ ease: "linear"
3278
+ });
3279
+ const tp = tipFor(c);
3280
+ tips.push({ t: local, value: [tp.x / W, tp.y / H], ease: "linear" });
3281
+ }
3282
+ return {
3283
+ side,
3284
+ track: { keyframes: simplify(poses, [W, H, 1, 1, 1]) },
3285
+ tip: { keyframes: simplify(tips, [W, H]) },
3286
+ clamped: round4(clamped),
3287
+ box
3288
+ };
3289
+ }
3290
+ function simplify(keys, scalePx) {
3291
+ if (keys.length <= 2) return keys.map(roundKey);
3292
+ const out = [roundKey(keys[0])];
3293
+ let a = 0;
3294
+ for (let i = 2; i <= keys.length - 1; i++) {
3295
+ const ka = keys[a];
3296
+ const ki = keys[i];
3297
+ let ok = true;
3298
+ for (let j = a + 1; j < i && ok; j++) {
3299
+ const kj = keys[j];
3300
+ const u = (kj.t - ka.t) / (ki.t - ka.t || 1);
3301
+ for (let k = 0; k < kj.value.length; k++) {
3302
+ const pred = ka.value[k] + (ki.value[k] - ka.value[k]) * u;
3303
+ if (Math.abs(pred - kj.value[k]) * (scalePx[k] ?? 1) > PIN_SIMPLIFY_PX) {
3304
+ ok = false;
3305
+ break;
3306
+ }
3307
+ }
3308
+ }
3309
+ if (!ok) {
3310
+ a = i - 1;
3311
+ out.push(roundKey(keys[a]));
3312
+ }
3313
+ }
3314
+ const last = keys[keys.length - 1];
3315
+ if (out[out.length - 1].t !== round4(last.t)) out.push(roundKey(last));
3316
+ return out;
3317
+ }
3318
+ function roundKey(k) {
3319
+ return { t: round4(k.t), value: k.value.map(round42), ease: k.ease };
3320
+ }
3321
+ function round4(v) {
3322
+ return Math.round(v * 1e3) / 1e3;
3323
+ }
3324
+ function round42(v) {
3325
+ return Math.round(v * 1e5) / 1e5;
3326
+ }
3327
+
3060
3328
  // src/lower/cursorIdle.ts
3061
3329
  var CURSOR_IDLE_HOLD = 1;
3062
3330
  var CURSOR_IDLE_FADE_OUT = 0.35;
3063
3331
  var CURSOR_IDLE_FADE_IN = 0.18;
3064
3332
  var CURSOR_IDLE_EPS_FRAC = 25e-4;
3065
- var round4 = (n) => Math.round(n * 1e3) / 1e3;
3333
+ var round5 = (n) => Math.round(n * 1e3) / 1e3;
3066
3334
  function cursorIdleFade(track, o) {
3067
3335
  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
3336
  if (moves.length === 0) return [];
@@ -3114,7 +3382,7 @@ function emit(keys, s, e) {
3114
3382
  push(keys, e + CURSOR_IDLE_FADE_IN, 1);
3115
3383
  }
3116
3384
  function push(keys, t, a) {
3117
- const rt = round4(t);
3385
+ const rt = round5(t);
3118
3386
  if (keys.length > 0 && rt <= keys[keys.length - 1].t) {
3119
3387
  keys[keys.length - 1].a = a;
3120
3388
  return;
@@ -4622,7 +4890,7 @@ function gatedRect(rect, x, y, space) {
4622
4890
  return null;
4623
4891
  const m = RECT_CONTAIN_MARGIN;
4624
4892
  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;
4893
+ return inside ? [round6(rect.x), round6(rect.y), round6(rect.w), round6(rect.h)] : null;
4626
4894
  }
4627
4895
  function extractClicks(track, segments, opts) {
4628
4896
  const out = [];
@@ -4647,11 +4915,11 @@ function extractClicks(track, segments, opts) {
4647
4915
  const upSrc = st + Math.max(pressLen, 0.02);
4648
4916
  const up = keptOutputEnd(segments, st, upSrc, opts.media) ?? ot + CLICK_SYNTH_RELEASE;
4649
4917
  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),
4918
+ ot: round6(ot),
4919
+ up: round6(Math.max(up, ot + 0.02)),
4920
+ st: round6(st),
4921
+ x: round6(e.x),
4922
+ y: round6(e.y),
4655
4923
  b: button
4656
4924
  };
4657
4925
  if (opts.rects && e.rect) {
@@ -4670,7 +4938,7 @@ function hexToRgbTriplet(hex) {
4670
4938
  const n = parseInt(h, 16);
4671
4939
  return [n >> 16, n >> 8 & 255, n & 255];
4672
4940
  }
4673
- function round5(v) {
4941
+ function round6(v) {
4674
4942
  return Math.round(v * 1e3) / 1e3;
4675
4943
  }
4676
4944
 
@@ -4746,7 +5014,7 @@ function trackEmitter() {
4746
5014
  if (tt <= prev.t + 1e-6 && sameVec(prev.value, value)) return prev.t;
4747
5015
  if (tt <= prev.t + 1e-6) tt = prev.t + 1e-3;
4748
5016
  }
4749
- keyframes.push({ t: round6(tt), value: value.map(round6), ease });
5017
+ keyframes.push({ t: round7(tt), value: value.map(round7), ease });
4750
5018
  return tt;
4751
5019
  };
4752
5020
  return { keyframes, push: push2 };
@@ -4923,7 +5191,7 @@ function camBubbleRectAt(doc, t) {
4923
5191
  H
4924
5192
  );
4925
5193
  if (!track.keyframes.length) return camBubbleRect(doc.cam, W, H);
4926
- return camRectFromPose(sample(track, t, lerpArray), doc.cam, W, H);
5194
+ return camRectFromPose(sample2(track, t, lerpArray2), doc.cam, W, H);
4927
5195
  }
4928
5196
  function motionTrack(base, keys, dur) {
4929
5197
  const sorted = keys.filter((k) => Number.isFinite(k.at)).sort((a, b) => a.at - b.at);
@@ -4971,7 +5239,7 @@ function overlayMotionPoseAt(o, t) {
4971
5239
  Math.max(OVERLAY_MIN_DURATION, o.duration)
4972
5240
  );
4973
5241
  if (!track.keyframes.length) return null;
4974
- return [...sample(track, t, lerpArray)];
5242
+ return [...sample2(track, t, lerpArray2)];
4975
5243
  }
4976
5244
  function objectMotionBase(o) {
4977
5245
  const t = o.transform3d;
@@ -5001,7 +5269,7 @@ function objectMotionPoseAt(o, t, clipDur) {
5001
5269
  o.span?.duration ?? clipDur
5002
5270
  );
5003
5271
  if (!track.keyframes.length) return null;
5004
- return [...sample(track, t, lerpArray)];
5272
+ return [...sample2(track, t, lerpArray2)];
5005
5273
  }
5006
5274
  var FRAME_STEP_SRC = `
5007
5275
  function stepPaused(vid, srcT) {
@@ -6383,6 +6651,50 @@ var ON_FRAME = `(ctx, content, dt) => {
6383
6651
  }
6384
6652
  }
6385
6653
 
6654
+ // pinned layers' marks \u2014 a standing ring or underline on a layer's
6655
+ // REFERENT for the layer's life (d.pins: OUTPUT-anchored records baked at
6656
+ // lowering, the referent in normalised video fractions). Painted inside the
6657
+ // zoom transform so the mark sits on the element under any level or pose,
6658
+ // at a constant on-screen weight (\xF7 ceZs), with the layer's own entrance
6659
+ // and exit. Locals are pn-prefixed (one var scope). Skipped on the ghost
6660
+ // pass like the effects.
6661
+ var pns = d.pins || []
6662
+ if (pns.length && !tpG) {
6663
+ for (var pi = 0; pi < pns.length; pi++) {
6664
+ var pn = pns[pi]
6665
+ if (!pn.mark || pn.mark === 'none') continue
6666
+ var pnT = t - pn.start
6667
+ if (pnT < 0 || pnT > pn.dur) continue
6668
+ var pnA = 1
6669
+ if (pnT < 0.35) { var pnU = pnT / 0.35; pnA = 1 - Math.pow(1 - pnU, 3) }
6670
+ if (pn.dur - pnT < 0.35) { var pnV = (pn.dur - pnT) / 0.35; pnA = Math.min(pnA, 1 - Math.pow(1 - pnV, 3)) }
6671
+ if (pnA <= 0.004) continue
6672
+ var pnX = dx + pn.x * dw, pnY = dy + pn.y * dh, pnW = pn.w * dw, pnH = pn.h * dh
6673
+ var pnLw = 2 * s2 / ceZs, pnPad = 6 * s2 / ceZs
6674
+ var pnCol = pn.color || 'rgba(255,255,255,0.95)'
6675
+ c.save()
6676
+ c.globalAlpha = pnA
6677
+ if (pn.mark === 'underline') {
6678
+ // a bar under the referent: dark rim, then the ink
6679
+ var pnBy = pnY + pnH + pnPad * 0.5
6680
+ c.fillStyle = 'rgba(0,0,0,0.35)'
6681
+ c.fillRect(pnX - pnLw, pnBy - pnLw, pnW + pnLw * 2, pnLw * 1.5 + pnLw * 2)
6682
+ c.fillStyle = pnCol
6683
+ c.fillRect(pnX, pnBy, pnW, pnLw * 1.5)
6684
+ } else {
6685
+ // a ring around the referent: the cursor's dual stroke, a dark rim
6686
+ // under the ink, legible on any content
6687
+ c.lineWidth = pnLw + 2 * s2 / ceZs
6688
+ c.strokeStyle = 'rgba(0,0,0,0.35)'
6689
+ rr(pnX - pnPad, pnY - pnPad, pnW + pnPad * 2, pnH + pnPad * 2, pnPad, c); c.stroke()
6690
+ c.lineWidth = pnLw
6691
+ c.strokeStyle = pnCol
6692
+ rr(pnX - pnPad, pnY - pnPad, pnW + pnPad * 2, pnH + pnPad * 2, pnPad, c); c.stroke()
6693
+ }
6694
+ c.restore()
6695
+ }
6696
+ }
6697
+
6386
6698
  // cursor (SOURCE-anchored samples, read at the on-screen source moment).
6387
6699
  // The dot is the only thing cursorStyle.visible hides \u2014 the track still drives
6388
6700
  // cursor-follow zoom, and click effects draw above on their own switch.
@@ -6445,8 +6757,15 @@ var ON_FRAME = `(ctx, content, dt) => {
6445
6757
  // Wait only for the FIRST decoded frame, then keep drawing through seeks.
6446
6758
  if (camV && camV.readyState >= 2) r.camHasFrame = true
6447
6759
  var camActiveNow = !!(camV && camOn && camS.visible !== false && r.camHasFrame)
6760
+ // A pinned layer's leader is live for the layer's life: the overlay layer
6761
+ // repaints while one is up (the referent moves with the camera).
6762
+ var pqs = d.pins || [], pqLive = false
6763
+ for (var pl = 0; pl < pqs.length; pl++) {
6764
+ var pq0 = pqs[pl]
6765
+ if (pq0.leader && t >= pq0.start && t <= pq0.start + pq0.dur) { pqLive = true; break }
6766
+ }
6448
6767
  var ovSig = W + 'x' + H
6449
- var ovDirty = ov.sig !== ovSig || camActiveNow || ov.active || !!trOut
6768
+ var ovDirty = ov.sig !== ovSig || camActiveNow || ov.active || !!trOut || pqLive
6450
6769
  if (ovDirty) {
6451
6770
  ovC.clearRect(0, 0, W, H)
6452
6771
  // webcam bubble \u2014 pinned to the frame corner regardless of card tilt/zoom.
@@ -6496,6 +6815,57 @@ var ON_FRAME = `(ctx, content, dt) => {
6496
6815
  ovC.restore()
6497
6816
  }
6498
6817
  }
6818
+ // pinned layers' leaders \u2014 a hairline from the layer's near edge (the
6819
+ // baked tip track, clip-local frame fractions) to the referent's near
6820
+ // edge, the referent mapped through the camera the way the boundary dot
6821
+ // is, with a dot at the referent. Fades with the layer; rides the rise
6822
+ // entrance so the line meets the card as it lands.
6823
+ if (pqLive) {
6824
+ for (var pj = 0; pj < pqs.length; pj++) {
6825
+ var pq = pqs[pj]
6826
+ if (!pq.leader) continue
6827
+ var pqT = t - pq.start
6828
+ if (pqT < 0 || pqT > pq.dur) continue
6829
+ var pqA = 1, pqRise = 0
6830
+ if (pqT < 0.35) {
6831
+ var pqU = 1 - Math.pow(1 - pqT / 0.35, 3)
6832
+ pqA = pqU
6833
+ if (pq.enter === 'rise') pqRise = (1 - pqU) * 24 * s
6834
+ }
6835
+ if (pq.dur - pqT < 0.35) { var pqV = (pq.dur - pqT) / 0.35; pqA = Math.min(pqA, 1 - Math.pow(1 - pqV, 3)) }
6836
+ if (pqA <= 0.004 || !pq.tip || !pq.tip.keyframes || !pq.tip.keyframes.length) continue
6837
+ var pqTip = TL.sample(pq.tip, pqT, TL.lerpArray)
6838
+ var pqTx = pqTip[0] * W, pqTy = pqTip[1] * H + pqRise
6839
+ // the referent's corners in frame space under the live camera
6840
+ var pqAx = dx + pq.x * dw, pqAy = dy + pq.y * dh
6841
+ var pqBx = dx + (pq.x + pq.w) * dw, pqBy = dy + (pq.y + pq.h) * dh
6842
+ if (lvl > 1.001 && !d.zoomSuppressed) {
6843
+ if (camStage) {
6844
+ pqAx = W / 2 + (pqAx - wcx) * lvl; pqAy = H / 2 + (pqAy - wcy) * lvl
6845
+ pqBx = W / 2 + (pqBx - wcx) * lvl; pqBy = H / 2 + (pqBy - wcy) * lvl
6846
+ } else {
6847
+ pqAx = fx + (pqAx - fx) * lvl; pqAy = fy + (pqAy - fy) * lvl
6848
+ pqBx = fx + (pqBx - fx) * lvl; pqBy = fy + (pqBy - fy) * lvl
6849
+ }
6850
+ }
6851
+ var pqRx = pq.side === 'right' ? pqBx : pq.side === 'left' ? pqAx : (pqAx + pqBx) / 2
6852
+ var pqRy = pq.side === 'below' ? pqBy : pq.side === 'above' ? pqAy : (pqAy + pqBy) / 2
6853
+ var pqCol = pq.color || 'rgba(255,255,255,0.95)'
6854
+ var pqLw = 2 * s
6855
+ ovC.save()
6856
+ ovC.globalAlpha = pqA
6857
+ ovC.lineCap = 'round'
6858
+ ovC.strokeStyle = 'rgba(0,0,0,0.35)'; ovC.lineWidth = pqLw + 2 * s
6859
+ ovC.beginPath(); ovC.moveTo(pqTx, pqTy); ovC.lineTo(pqRx, pqRy); ovC.stroke()
6860
+ ovC.strokeStyle = pqCol; ovC.lineWidth = pqLw
6861
+ ovC.beginPath(); ovC.moveTo(pqTx, pqTy); ovC.lineTo(pqRx, pqRy); ovC.stroke()
6862
+ ovC.fillStyle = 'rgba(0,0,0,0.35)'
6863
+ ovC.beginPath(); ovC.arc(pqRx, pqRy, 5 * s, 0, Math.PI * 2); ovC.fill()
6864
+ ovC.fillStyle = pqCol
6865
+ ovC.beginPath(); ovC.arc(pqRx, pqRy, 4 * s, 0, Math.PI * 2); ovC.fill()
6866
+ ovC.restore()
6867
+ }
6868
+ }
6499
6869
  // The boundary dot: the outgoing card's last cursor point and the
6500
6870
  // incoming's current one, each carried by its card's move, one dot on
6501
6871
  // the eased lerp between them (the idle fade never fires across a page
@@ -6510,7 +6880,7 @@ var ON_FRAME = `(ctx, content, dt) => {
6510
6880
  drawCur(ovC, tqX2, tqY2, tqR, 1, 2 * (trQw || s))
6511
6881
  }
6512
6882
  ov.sig = ovSig
6513
- ov.active = camActiveNow || !!trOut
6883
+ ov.active = camActiveNow || !!trOut || pqLive
6514
6884
  if (ov.texture) ov.texture.needsUpdate = true
6515
6885
  }
6516
6886
 
@@ -6604,7 +6974,7 @@ var ON_FRAME = `(ctx, content, dt) => {
6604
6974
  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
6975
  }
6606
6976
  }`;
6607
- function studioLayerData(layers, duration) {
6977
+ function studioLayerData(layers, duration, pins) {
6608
6978
  const rec = "source" in layers ? layers : null;
6609
6979
  return {
6610
6980
  // Music/SFX clips with their gain envelopes baked (shared truth for the
@@ -6613,14 +6983,14 @@ function studioLayerData(layers, duration) {
6613
6983
  // needs a decoded recording, which a sync lowering can't produce.
6614
6984
  audio: (layers.audio ?? []).map((c) => ({
6615
6985
  key: c.key,
6616
- start: round6(c.start),
6617
- in: round6(c.in),
6618
- out: round6(c.out),
6619
- gain: round6(c.gain),
6986
+ start: round7(c.start),
6987
+ in: round7(c.in),
6988
+ out: round7(c.out),
6989
+ gain: round7(c.gain),
6620
6990
  loop: !!c.loop,
6621
- len: round6(clipLength(c)),
6991
+ len: round7(clipLength(c)),
6622
6992
  duck: !!c.duck,
6623
- env: clipEnvelope(c).map((p) => ({ t: round6(p.t), g: round6(p.g) }))
6993
+ env: clipEnvelope(c).map((p) => ({ t: round7(p.t), g: round7(p.g) }))
6624
6994
  })),
6625
6995
  // Object clips: numbers resolved HERE; shapes are the drafted engine spec.
6626
6996
  ...layers.objects && layers.objects.length ? {
@@ -6633,17 +7003,17 @@ function studioLayerData(layers, duration) {
6633
7003
  } : o.asset.kind === "text3d" ? resolveText3dAsset(o.asset) : { kind: "gltf", key: o.asset.key },
6634
7004
  ...o.span ? {
6635
7005
  span: {
6636
- start: round6(o.span.start),
6637
- duration: round6(o.span.duration)
7006
+ start: round7(o.span.start),
7007
+ duration: round7(o.span.duration)
6638
7008
  }
6639
7009
  } : {},
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),
7010
+ x: round7(o.transform3d.x),
7011
+ y: round7(o.transform3d.y),
7012
+ z: round7(o.transform3d.z),
7013
+ rx: round7(o.transform3d.rx),
7014
+ ry: round7(o.transform3d.ry),
7015
+ rz: round7(o.transform3d.rz),
7016
+ scale: round7(o.transform3d.scale || OBJECT_DEFAULT_SCALE),
6647
7017
  anim: idleKey(o),
6648
7018
  // Pose keyframes: clip-local [x,y,z,rx,ry,rz,scale] track,
6649
7019
  // sampled at t − span.start. Omitted when absent — parity.
@@ -6670,18 +7040,20 @@ function studioLayerData(layers, duration) {
6670
7040
  const base = {
6671
7041
  id: o.id,
6672
7042
  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),
7043
+ start: round7(o.start),
7044
+ dur: round7(Math.max(OVERLAY_MIN_DURATION, o.duration)),
7045
+ x: round7(o.transform.x),
7046
+ y: round7(o.transform.y),
7047
+ scale: round7(o.transform.scale || 1),
7048
+ rot: round7(o.transform.rotation || 0),
6679
7049
  enter: enterKey(o),
6680
7050
  exit: exitKey(o),
6681
7051
  // Pose keyframes: a CLIP-LOCAL [x, y, scale, rot, opacity]
6682
7052
  // track, sampled in ON_FRAME at t − start. Omitted when the clip
6683
7053
  // has no motion — data byte parity.
6684
7054
  ...(() => {
7055
+ const pinned = pins?.get(o.id);
7056
+ if (pinned) return { track: pinned.track };
6685
7057
  if (!o.motion || !o.motion.length) return {};
6686
7058
  const mb = overlayMotionBase(o);
6687
7059
  const track = motionTrack(
@@ -6698,10 +7070,10 @@ function studioLayerData(layers, duration) {
6698
7070
  ...base,
6699
7071
  kind: "image",
6700
7072
  key: hl.key,
6701
- w: round6(hl.width),
7073
+ w: round7(hl.width),
6702
7074
  radius: 0,
6703
7075
  shadow: "none",
6704
- opacity: round6(o.opacity ?? 1),
7076
+ opacity: round7(o.opacity ?? 1),
6705
7077
  html: hl.html
6706
7078
  };
6707
7079
  }
@@ -6718,8 +7090,8 @@ function studioLayerData(layers, duration) {
6718
7090
  ...o.frame.browserBar?.theme ? { theme: o.frame.browserBar.theme } : {}
6719
7091
  },
6720
7092
  lean: [
6721
- round6(o.frame.lean?.rx ?? 0),
6722
- round6(o.frame.lean?.ry ?? 0)
7093
+ round7(o.frame.lean?.rx ?? 0),
7094
+ round7(o.frame.lean?.ry ?? 0)
6723
7095
  ],
6724
7096
  shadow: o.frame.shadow ?? LAYER_CARD_SHADOW,
6725
7097
  shadowContact: o.frame.shadowContact ?? 0,
@@ -6735,17 +7107,17 @@ function studioLayerData(layers, duration) {
6735
7107
  for (const ev of lm.cursor) {
6736
7108
  if (ev.type === "down")
6737
7109
  clicks.push({
6738
- t: round6(ev.t / 1e3),
6739
- x: round6(ev.x),
6740
- y: round6(ev.y)
7110
+ t: round7(ev.t / 1e3),
7111
+ x: round7(ev.x),
7112
+ y: round7(ev.y)
6741
7113
  });
6742
7114
  }
6743
7115
  return {
6744
7116
  cur: {
6745
7117
  pts: sm.map((p) => ({
6746
- t: round6(p.t),
6747
- x: round6(p.x),
6748
- y: round6(p.y)
7118
+ t: round7(p.t),
7119
+ x: round7(p.x),
7120
+ y: round7(p.y)
6749
7121
  })),
6750
7122
  space: { w: lm.meta.width, h: lm.meta.height },
6751
7123
  size: rec.cursor.size,
@@ -6760,7 +7132,7 @@ function studioLayerData(layers, duration) {
6760
7132
  ...lm && lm.crop ? { crop: lm.crop } : {},
6761
7133
  ...lmCard ? { card: lmCard } : {},
6762
7134
  ...lmFacts,
6763
- w: round6(o.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH),
7135
+ w: round7(o.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH),
6764
7136
  radius: o.radius ?? OVERLAY_MEDIA_DEFAULT_RADIUS,
6765
7137
  opacity: o.opacity ?? 1,
6766
7138
  loop: !!o.loop,
@@ -6770,7 +7142,7 @@ function studioLayerData(layers, duration) {
6770
7142
  ...o.shadow ? { shadow: o.shadow } : {},
6771
7143
  ...o.border && o.border.width > 0 ? {
6772
7144
  border: {
6773
- width: round6(o.border.width),
7145
+ width: round7(o.border.width),
6774
7146
  color: o.border.color
6775
7147
  }
6776
7148
  } : {}
@@ -6790,11 +7162,11 @@ function studioLayerData(layers, duration) {
6790
7162
  // Style-v2 fields bake only when non-default (byte parity for
6791
7163
  // older docs); ON_FRAME reads them unconditionally.
6792
7164
  ...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) } : {},
7165
+ ...o.maxWidth ? { mw: round7(o.maxWidth) } : {},
7166
+ ...st.letterSpacing ? { ls: round7(st.letterSpacing) } : {},
7167
+ ...st.lineHeight !== OVERLAY_LINE_HEIGHT ? { lh: round7(st.lineHeight) } : {},
6796
7168
  ...st.align !== "center" ? { align: st.align } : {},
6797
- ...st.stroke ? { stroke: { c: st.stroke.color, w: round6(st.stroke.width) } } : {},
7169
+ ...st.stroke ? { stroke: { c: st.stroke.color, w: round7(st.stroke.width) } } : {},
6798
7170
  // Hosted face behind an override: ON_FRAME lazy-loads it so a
6799
7171
  // live family/weight edit paints without a LOAD (SETUP only
6800
7172
  // runs on cold load).
@@ -6807,10 +7179,10 @@ function studioLayerData(layers, duration) {
6807
7179
  ...bx ? {
6808
7180
  box: {
6809
7181
  c: bx.color,
6810
- o: round6(bx.opacity),
6811
- px: round6(bx.padX),
6812
- py: round6(bx.padY),
6813
- r: round6(bx.radius)
7182
+ o: round7(bx.opacity),
7183
+ px: round7(bx.padX),
7184
+ py: round7(bx.padY),
7185
+ r: round7(bx.radius)
6814
7186
  }
6815
7187
  } : {},
6816
7188
  // Entrance animation: segmentation + timing normalized
@@ -6844,36 +7216,22 @@ function lowerToComposition(input) {
6844
7216
  sourceDuration: (doc.source.meta.durationMs || 0) / 1e3
6845
7217
  }) : [];
6846
7218
  const layout = docCardLayout(doc);
6847
- const meta = doc.source.meta;
6848
7219
  const zoomStyle = resolveZoomStyle(doc.zoomStyle, doc.zoomParams);
6849
7220
  const camera = cameraModel(doc.frame);
6850
- const zoomSpans = restSpansThroughTransitions(
6851
- doc.zoom,
7221
+ const zoomSpans = loweredZoomSpans(
7222
+ doc,
6852
7223
  rated,
6853
7224
  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
- });
7225
+ layout,
7226
+ camera,
7227
+ zoomStyle
7228
+ );
7229
+ const zoomTrack = prependEntrance(
7230
+ zoomTrackFromDoc(zoomSpans, rated, zoomStyle),
7231
+ entranceZoomKeyframes(cardEnter(doc.frame))
7232
+ );
7233
+ const pins = resolvePins(doc, layout, camera, zoomTrack);
7234
+ const pinsData = pinsDataOf(doc, pins);
6877
7235
  const data = {
6878
7236
  videoSrc: doc.source.videoKey,
6879
7237
  isImage: doc.source.sourceKind === "image",
@@ -6896,17 +7254,17 @@ function lowerToComposition(input) {
6896
7254
  // Rated segments (speed spans pre-intersected): ON_FRAME's mapTime and the
6897
7255
  // export's audio splice read the rate straight off each segment.
6898
7256
  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) } : {},
7257
+ in: round7(seg.in),
7258
+ out: round7(seg.out),
7259
+ ...seg.rate !== void 0 && seg.rate !== 1 ? { rate: round7(seg.rate) } : {},
6902
7260
  ...seg.media ? { media: seg.media } : {}
6903
7261
  })),
6904
7262
  frame: doc.frame,
6905
7263
  micGain: doc.micGain ?? 1,
6906
7264
  cursor: smoothed.map((p) => ({
6907
- t: round6(p.t),
6908
- x: round6(p.x),
6909
- y: round6(p.y)
7265
+ t: round7(p.t),
7266
+ x: round7(p.x),
7267
+ y: round7(p.y)
6910
7268
  })),
6911
7269
  cursorStyle: doc.cursor,
6912
7270
  cursorSpace: { w: doc.source.meta.width, h: doc.source.meta.height },
@@ -6929,9 +7287,9 @@ function lowerToComposition(input) {
6929
7287
  crop: m.crop ?? null,
6930
7288
  hasAudio: !!m.meta.hasAudio,
6931
7289
  cursor: sm.map((p) => ({
6932
- t: round6(p.t),
6933
- x: round6(p.x),
6934
- y: round6(p.y)
7290
+ t: round7(p.t),
7291
+ x: round7(p.x),
7292
+ y: round7(p.y)
6935
7293
  })),
6936
7294
  cursorSpace: { w: m.meta.width, h: m.meta.height },
6937
7295
  // The card this media wears (its own placement, bar, corner,
@@ -6950,11 +7308,11 @@ function lowerToComposition(input) {
6950
7308
  // intensities/colors become numbers HERE — ON_FRAME reads no registry).
6951
7309
  ...clickFxData(doc, rated),
6952
7310
  // 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
- ),
7311
+ zoomTrack,
7312
+ // Pinned layers' marks and leaders: the referent (normalised video
7313
+ // fractions) plus the leader's tip track, painted by ON_FRAME beside the
7314
+ // camera. Absent when no pinned layer asks for either — byte parity.
7315
+ ...pinsData.length ? { pins: pinsData } : {},
6958
7316
  // Tilt spans: OUTPUT-time [rx, ry] degree track. The rest pose is
6959
7317
  // FLAT — there is no static card tilt any more — and the motion constants
6960
7318
  // come from the camera style's tilt personality ('drift' slows its
@@ -6987,7 +7345,7 @@ function lowerToComposition(input) {
6987
7345
  };
6988
7346
  const entryData = {
6989
7347
  lights: true,
6990
- ...studioLayerData(doc, duration)
7348
+ ...studioLayerData(doc, duration, pins)
6991
7349
  };
6992
7350
  const config = {
6993
7351
  version: 2,
@@ -7013,6 +7371,128 @@ function lowerToComposition(input) {
7013
7371
  };
7014
7372
  return { config, data, stack: { [STUDIO_ENTRY_ID]: entryData }, duration };
7015
7373
  }
7374
+ function loweredZoomSpans(doc, rated, transitions, layout, camera, zoomStyle) {
7375
+ const meta = doc.source.meta;
7376
+ return restSpansThroughTransitions(
7377
+ doc.zoom,
7378
+ rated,
7379
+ transitions,
7380
+ ZOOM_SPAN_MIN
7381
+ ).map((z) => {
7382
+ if (z.focusMode === "auto") {
7383
+ const f = followFocusEvents(
7384
+ z,
7385
+ doc.source.cursor,
7386
+ { w: meta.width, h: meta.height },
7387
+ layout,
7388
+ {
7389
+ safeRatio: zoomStyle.followSafeRatio,
7390
+ recenter: zoomStyle.followRecenter,
7391
+ lookahead: zoomStyle.followLookahead,
7392
+ camera
7393
+ }
7394
+ );
7395
+ if (f.entry)
7396
+ return { ...z, cx: f.entry.cx, cy: f.entry.cy, followEvents: f.events };
7397
+ }
7398
+ return {
7399
+ ...z,
7400
+ ...clampFocus(z.cx, z.cy, clampZoomLevel(z.level), layout, camera)
7401
+ };
7402
+ });
7403
+ }
7404
+ function docZoomTrack(input) {
7405
+ const doc = migrateMotion(input);
7406
+ const rated = ratedSegments(doc);
7407
+ const transitions = docTransitions(doc);
7408
+ const layout = docCardLayout(doc);
7409
+ const camera = cameraModel(doc.frame);
7410
+ const zoomStyle = resolveZoomStyle(doc.zoomStyle, doc.zoomParams);
7411
+ const spans = loweredZoomSpans(
7412
+ doc,
7413
+ rated,
7414
+ transitions,
7415
+ layout,
7416
+ camera,
7417
+ zoomStyle
7418
+ );
7419
+ return prependEntrance(
7420
+ zoomTrackFromDoc(spans, rated, zoomStyle),
7421
+ entranceZoomKeyframes(cardEnter(doc.frame))
7422
+ );
7423
+ }
7424
+ function resolvePins(doc, layout, camera, zoomTrack) {
7425
+ const out = /* @__PURE__ */ new Map();
7426
+ for (const o of doc.overlays ?? []) {
7427
+ if (!o.pin) continue;
7428
+ const ref = pinReferent(doc, o.pin);
7429
+ if (!ref) continue;
7430
+ const base = overlayMotionBase(o);
7431
+ const motion = o.motion && o.motion.length ? motionTrack(
7432
+ base,
7433
+ overlayMotionKeys(o, base),
7434
+ Math.max(OVERLAY_MIN_DURATION, o.duration)
7435
+ ) : null;
7436
+ out.set(
7437
+ o.id,
7438
+ pinPlacement({
7439
+ clip: o,
7440
+ referent: ref.rect,
7441
+ layout,
7442
+ camera,
7443
+ zoomTrack,
7444
+ base,
7445
+ motion
7446
+ })
7447
+ );
7448
+ }
7449
+ return out;
7450
+ }
7451
+ function overlayPinPoseAt(doc, o, t) {
7452
+ if (!o.pin) return null;
7453
+ const layout = docCardLayout(doc);
7454
+ const camera = cameraModel(doc.frame);
7455
+ const pins = resolvePins(
7456
+ { ...doc, overlays: [o] },
7457
+ layout,
7458
+ camera,
7459
+ docZoomTrack(doc)
7460
+ );
7461
+ const p = pins.get(o.id);
7462
+ if (!p) return null;
7463
+ return [...sample2(p.track, t, lerpArray2)];
7464
+ }
7465
+ function pinsDataOf(doc, pins) {
7466
+ const out = [];
7467
+ for (const o of doc.overlays ?? []) {
7468
+ const p = o.pin;
7469
+ const placed = p ? pins.get(o.id) : void 0;
7470
+ if (!p || !placed) continue;
7471
+ const mark = p.mark ?? "none";
7472
+ if (mark === "none" && !p.leader) continue;
7473
+ const ref = pinReferent(doc, p);
7474
+ if (!ref) continue;
7475
+ out.push({
7476
+ id: o.id,
7477
+ start: round7(o.start),
7478
+ dur: round7(Math.max(OVERLAY_MIN_DURATION, o.duration)),
7479
+ x: round43(ref.rect.x),
7480
+ y: round43(ref.rect.y),
7481
+ w: round43(ref.rect.w),
7482
+ h: round43(ref.rect.h),
7483
+ side: placed.side,
7484
+ mark,
7485
+ leader: !!p.leader,
7486
+ ...p.color ? { color: p.color } : {},
7487
+ enter: enterKey(o),
7488
+ tip: placed.tip
7489
+ });
7490
+ }
7491
+ return out;
7492
+ }
7493
+ function round43(v) {
7494
+ return Math.round(v * 1e5) / 1e5;
7495
+ }
7016
7496
  function clickFxData(doc, rated) {
7017
7497
  const fx = doc.cursor.clickFx;
7018
7498
  const on = fx.style !== "none" || fx.press;
@@ -7046,7 +7526,7 @@ function clickFxData(doc, rated) {
7046
7526
  }
7047
7527
  };
7048
7528
  }
7049
- function round6(v) {
7529
+ function round7(v) {
7050
7530
  return Math.round(v * 1e3) / 1e3;
7051
7531
  }
7052
7532
 
@@ -7136,8 +7616,8 @@ function planAutoSpeed(track, opts) {
7136
7616
  accepted.sort((a, b) => a.in - b.in);
7137
7617
  return accepted.map((c, i) => ({
7138
7618
  id: `s${i}`,
7139
- in: round7(c.in),
7140
- out: round7(c.out),
7619
+ in: round8(c.in),
7620
+ out: round8(c.out),
7141
7621
  rate: clampSpeedRate(c.rate),
7142
7622
  source: "auto"
7143
7623
  }));
@@ -7190,7 +7670,7 @@ function collectRuns(evs, gap, minLen, emit2) {
7190
7670
  }
7191
7671
  flush();
7192
7672
  }
7193
- function round7(v) {
7673
+ function round8(v) {
7194
7674
  return Math.round(v * 1e3) / 1e3;
7195
7675
  }
7196
7676
 
@@ -7371,16 +7851,16 @@ function momentsFromDoc(doc, plan, opts = {}) {
7371
7851
  return {
7372
7852
  id: `m${String(i + 1).padStart(2, "0")}`,
7373
7853
  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,
7854
+ source: { in: round9(d.in), out: round9(d.out) },
7855
+ output: output ? { in: round9(output.start), out: round9(output.end) } : null,
7856
+ at: d.at === null ? null : round9(d.at),
7857
+ outputAt: outAt ? round9(outAt.start) : null,
7858
+ focus: d.focus ? { cx: round9(d.focus.cx), cy: round9(d.focus.cy) } : null,
7379
7859
  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)
7860
+ x: round9(d.rect.x),
7861
+ y: round9(d.rect.y),
7862
+ w: round9(d.rect.w),
7863
+ h: round9(d.rect.h)
7384
7864
  } : null,
7385
7865
  ...d.clicks !== void 0 ? { clicks: d.clicks } : {},
7386
7866
  ...d.surface ? { surface: true } : {},
@@ -7401,7 +7881,7 @@ function activityOf(bins, d) {
7401
7881
  sum += bins[i];
7402
7882
  n++;
7403
7883
  }
7404
- return n ? round8(sum / n) : null;
7884
+ return n ? round9(sum / n) : null;
7405
7885
  }
7406
7886
  function saidOver(transcript, d) {
7407
7887
  if (!transcript?.length) return null;
@@ -7410,7 +7890,7 @@ function saidOver(transcript, d) {
7410
7890
  const text = transcript.filter((s) => s.start < hi && s.end > lo).map((s) => s.text.trim()).filter(Boolean).join(" ");
7411
7891
  return text || null;
7412
7892
  }
7413
- function round8(v) {
7893
+ function round9(v) {
7414
7894
  return Math.round(v * 1e3) / 1e3;
7415
7895
  }
7416
7896
 
@@ -7904,8 +8384,8 @@ function buildDigest(input) {
7904
8384
  return {
7905
8385
  digestVersion: DIGEST_VERSION,
7906
8386
  take: {
7907
- sourceDuration: round9(meta.durationMs / 1e3),
7908
- outputDuration: round9(input.outputDuration),
8387
+ sourceDuration: round10(meta.durationMs / 1e3),
8388
+ outputDuration: round10(input.outputDuration),
7909
8389
  width: meta.width,
7910
8390
  height: meta.height,
7911
8391
  captureWidth: meta.captureWidth ?? null,
@@ -7954,7 +8434,7 @@ function buildDigest(input) {
7954
8434
  function outputDurationOf(doc) {
7955
8435
  return docOutputDuration(doc);
7956
8436
  }
7957
- function round9(v) {
8437
+ function round10(v) {
7958
8438
  return Math.round(v * 1e3) / 1e3;
7959
8439
  }
7960
8440
 
@@ -7980,20 +8460,20 @@ function setSpeedInRange(spans, srcIn, srcOut, rate) {
7980
8460
  continue;
7981
8461
  }
7982
8462
  if (srcIn - s.in >= MIN_REMAINDER)
7983
- kept.push({ ...s, out: round10(srcIn), source: "manual" });
8463
+ kept.push({ ...s, out: round11(srcIn), source: "manual" });
7984
8464
  if (s.out - srcOut >= MIN_REMAINDER)
7985
8465
  kept.push({
7986
8466
  ...s,
7987
8467
  id: mintId(used),
7988
- in: round10(srcOut),
8468
+ in: round11(srcOut),
7989
8469
  source: "manual"
7990
8470
  });
7991
8471
  }
7992
8472
  if (rate != null)
7993
8473
  kept.push({
7994
8474
  id: mintId(used),
7995
- in: round10(srcIn),
7996
- out: round10(srcOut),
8475
+ in: round11(srcIn),
8476
+ out: round11(srcOut),
7997
8477
  rate: clampSpeedRate(rate),
7998
8478
  source: "manual"
7999
8479
  });
@@ -8009,9 +8489,9 @@ function removeSourceRange(segments, srcIn, srcOut) {
8009
8489
  next.push(seg);
8010
8490
  continue;
8011
8491
  }
8012
- if (srcIn - seg.in >= MIN_SEGMENT) next.push({ ...seg, out: round10(srcIn) });
8492
+ if (srcIn - seg.in >= MIN_SEGMENT) next.push({ ...seg, out: round11(srcIn) });
8013
8493
  if (seg.out - srcOut >= MIN_SEGMENT)
8014
- next.push({ ...seg, in: round10(srcOut) });
8494
+ next.push({ ...seg, in: round11(srcOut) });
8015
8495
  }
8016
8496
  return next.length ? next : [...segments];
8017
8497
  }
@@ -8029,8 +8509,8 @@ function zoomSpanForRange(zoom, srcIn, srcOut) {
8029
8509
  while (used.has(`u${n}`)) n++;
8030
8510
  return {
8031
8511
  id: `u${n}`,
8032
- in: round10(start),
8033
- out: round10(end),
8512
+ in: round11(start),
8513
+ out: round11(end),
8034
8514
  level: DEFAULT_ZOOM_LEVEL,
8035
8515
  cx: 0.5,
8036
8516
  cy: 0.5,
@@ -8044,7 +8524,7 @@ function mintId(used) {
8044
8524
  used.add(id);
8045
8525
  return id;
8046
8526
  }
8047
- function round10(v) {
8527
+ function round11(v) {
8048
8528
  return Math.round(v * 1e3) / 1e3;
8049
8529
  }
8050
8530
 
@@ -8748,8 +9228,8 @@ var videoLane = {
8748
9228
  return segments.map((s, i) => ({
8749
9229
  id: `seg-${i}`,
8750
9230
  kind: "clip",
8751
- t: round11(starts[i]),
8752
- duration: round11(
9231
+ t: round12(starts[i]),
9232
+ duration: round12(
8753
9233
  outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i))
8754
9234
  )
8755
9235
  }));
@@ -8832,7 +9312,7 @@ var videoLane = {
8832
9312
  if (local >= m.t - 1e-9 && local <= m.t + m.duration + 1e-9)
8833
9313
  return null;
8834
9314
  }
8835
- const sourceT = round11(
9315
+ const sourceT = round12(
8836
9316
  mapTime2(segmentPieces(s, speeds, freezes, leading), local)
8837
9317
  );
8838
9318
  if (sourceT - s.in < 0.05 || s.out - sourceT < 0.05) return null;
@@ -8873,7 +9353,7 @@ var videoLane = {
8873
9353
  n === 1 || !contiguous(segments, n - 1)
8874
9354
  )
8875
9355
  ] : []
8876
- ].map(round11);
9356
+ ].map(round12);
8877
9357
  }
8878
9358
  };
8879
9359
  function nextFreezeId(doc) {
@@ -8900,8 +9380,8 @@ var freezeLane = {
8900
9380
  {
8901
9381
  id: f.id,
8902
9382
  kind: "clip",
8903
- t: round11(m.t),
8904
- duration: round11(m.duration),
9383
+ t: round12(m.t),
9384
+ duration: round12(m.duration),
8905
9385
  label: `${trimZeros(f.seconds)}s`
8906
9386
  }
8907
9387
  ] : [];
@@ -8923,7 +9403,7 @@ var freezeLane = {
8923
9403
  if (g.t >= m.t - 1e-9 && g.t <= m.t + m.duration + 1e-9) return null;
8924
9404
  }
8925
9405
  if (g.t > totalDuration7(rated) + 1e-9) return null;
8926
- const srcT = round11(mapTime2(rated, Math.max(0, g.t)));
9406
+ const srcT = round12(mapTime2(rated, Math.max(0, g.t)));
8927
9407
  if (!kept(srcT) || !clear(srcT)) return null;
8928
9408
  const id = nextFreezeId(doc);
8929
9409
  const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
@@ -8965,7 +9445,7 @@ var freezeLane = {
8965
9445
  const marks = freezeOutputExtents(splitBySpeed4(segs, speeds), freezes);
8966
9446
  const mine = marks.get(f.id);
8967
9447
  const endOut = mine ? mine.t + mine.duration : null;
8968
- const srcT = round11(
9448
+ const srcT = round12(
8969
9449
  Math.min(sourceDuration, mapTime2(base, Math.max(0, g.t)))
8970
9450
  );
8971
9451
  if (!kept(srcT) || !clear(srcT, f.id)) return null;
@@ -9025,8 +9505,8 @@ var zoomLane = {
9025
9505
  {
9026
9506
  id: z.id,
9027
9507
  kind: "clip",
9028
- t: round11(ext.start),
9029
- duration: round11(ext.end - ext.start),
9508
+ t: round12(ext.start),
9509
+ duration: round12(ext.end - ext.start),
9030
9510
  label: `${z.level.toFixed(2)}\xD7`
9031
9511
  }
9032
9512
  ];
@@ -9053,8 +9533,8 @@ var zoomLane = {
9053
9533
  ...d.zoom,
9054
9534
  {
9055
9535
  id,
9056
- in: round11(srcT),
9057
- out: round11(srcT + len),
9536
+ in: round12(srcT),
9537
+ out: round12(srcT + len),
9058
9538
  level: DEFAULT_ZOOM_LEVEL,
9059
9539
  cx,
9060
9540
  cy,
@@ -9086,8 +9566,8 @@ var zoomLane = {
9086
9566
  return (d) => {
9087
9567
  const z = d.zoom.find((x) => x.id === g.id);
9088
9568
  if (!z) return;
9089
- z.in = round11(newIn);
9090
- z.out = round11(newIn + len);
9569
+ z.in = round12(newIn);
9570
+ z.out = round12(newIn + len);
9091
9571
  z.source = "manual";
9092
9572
  d.zoom.sort((a, b) => a.in - b.in);
9093
9573
  };
@@ -9116,8 +9596,8 @@ var zoomLane = {
9116
9596
  return (d) => {
9117
9597
  const z = d.zoom.find((x) => x.id === g.id);
9118
9598
  if (!z) return;
9119
- z.in = round11(next.in);
9120
- z.out = round11(next.out);
9599
+ z.in = round12(next.in);
9600
+ z.out = round12(next.out);
9121
9601
  z.source = "manual";
9122
9602
  };
9123
9603
  }
@@ -9139,8 +9619,8 @@ function cursorFocusAt(doc, srcT) {
9139
9619
  if (Math.abs(e.t - ms) < Math.abs(best.t - ms)) best = e;
9140
9620
  const clamp015 = (v) => Math.max(0, Math.min(1, v));
9141
9621
  return {
9142
- cx: round11(clamp015(best.x / width)),
9143
- cy: round11(clamp015(best.y / height))
9622
+ cx: round12(clamp015(best.x / width)),
9623
+ cy: round12(clamp015(best.y / height))
9144
9624
  };
9145
9625
  }
9146
9626
  var tiltLane = {
@@ -9154,8 +9634,8 @@ var tiltLane = {
9154
9634
  {
9155
9635
  id: z.id,
9156
9636
  kind: "clip",
9157
- t: round11(ext.start),
9158
- duration: round11(ext.end - ext.start),
9637
+ t: round12(ext.start),
9638
+ duration: round12(ext.end - ext.start),
9159
9639
  label: `${formatDeg(z.rx)}\xB0/${formatDeg(z.ry)}\xB0`
9160
9640
  }
9161
9641
  ];
@@ -9181,8 +9661,8 @@ var tiltLane = {
9181
9661
  ...d.tilt ?? [],
9182
9662
  {
9183
9663
  id,
9184
- in: round11(srcT),
9185
- out: round11(srcT + len),
9664
+ in: round12(srcT),
9665
+ out: round12(srcT + len),
9186
9666
  rx: DEFAULT_TILT_POSE.rx,
9187
9667
  ry: DEFAULT_TILT_POSE.ry,
9188
9668
  source: "manual",
@@ -9213,8 +9693,8 @@ var tiltLane = {
9213
9693
  return (d) => {
9214
9694
  const z = (d.tilt ?? []).find((x) => x.id === g.id);
9215
9695
  if (!z || !d.tilt) return;
9216
- z.in = round11(newIn);
9217
- z.out = round11(newIn + len);
9696
+ z.in = round12(newIn);
9697
+ z.out = round12(newIn + len);
9218
9698
  z.source = "manual";
9219
9699
  d.tilt.sort((a, b) => a.in - b.in);
9220
9700
  };
@@ -9243,8 +9723,8 @@ var tiltLane = {
9243
9723
  return (d) => {
9244
9724
  const z = (d.tilt ?? []).find((x) => x.id === g.id);
9245
9725
  if (!z) return;
9246
- z.in = round11(next.in);
9247
- z.out = round11(next.out);
9726
+ z.in = round12(next.in);
9727
+ z.out = round12(next.out);
9248
9728
  z.source = "manual";
9249
9729
  };
9250
9730
  }
@@ -9271,8 +9751,8 @@ var camMoveLane = {
9271
9751
  {
9272
9752
  id: z.id,
9273
9753
  kind: "clip",
9274
- t: round11(ext.start),
9275
- duration: round11(ext.end - ext.start),
9754
+ t: round12(ext.start),
9755
+ duration: round12(ext.end - ext.start),
9276
9756
  label: z.size != null ? `${Math.round(z.size * 100)}%` : "Move"
9277
9757
  }
9278
9758
  ];
@@ -9296,8 +9776,8 @@ var camMoveLane = {
9296
9776
  ...d.camMotion ?? [],
9297
9777
  {
9298
9778
  id,
9299
- in: round11(srcT),
9300
- out: round11(srcT + len),
9779
+ in: round12(srcT),
9780
+ out: round12(srcT + len),
9301
9781
  ...DEFAULT_CAM_POSE,
9302
9782
  source: "manual"
9303
9783
  }
@@ -9326,8 +9806,8 @@ var camMoveLane = {
9326
9806
  return (d) => {
9327
9807
  const z = (d.camMotion ?? []).find((x) => x.id === g.id);
9328
9808
  if (!z || !d.camMotion) return;
9329
- z.in = round11(newIn);
9330
- z.out = round11(newIn + len);
9809
+ z.in = round12(newIn);
9810
+ z.out = round12(newIn + len);
9331
9811
  z.source = "manual";
9332
9812
  d.camMotion.sort((a, b) => a.in - b.in);
9333
9813
  };
@@ -9356,8 +9836,8 @@ var camMoveLane = {
9356
9836
  return (d) => {
9357
9837
  const z = (d.camMotion ?? []).find((x) => x.id === g.id);
9358
9838
  if (!z) return;
9359
- z.in = round11(next.in);
9360
- z.out = round11(next.out);
9839
+ z.in = round12(next.in);
9840
+ z.out = round12(next.out);
9361
9841
  z.source = "manual";
9362
9842
  };
9363
9843
  }
@@ -9390,10 +9870,10 @@ var camLane = {
9390
9870
  items.push({
9391
9871
  id: `cam-${i}`,
9392
9872
  kind: "clip",
9393
- t: round11(
9873
+ t: round12(
9394
9874
  starts[i] + outputLen({ in: s.in, out: a }, speeds, freezes, leading)
9395
9875
  ),
9396
- duration: round11(
9876
+ duration: round12(
9397
9877
  outputLen({ in: a, out: b }, speeds, freezes, a === s.in && leading)
9398
9878
  )
9399
9879
  });
@@ -9422,7 +9902,7 @@ var camLane = {
9422
9902
  Math.max(first.in, last.out - span)
9423
9903
  );
9424
9904
  return (d) => {
9425
- d.cam.window = { in: round11(newIn), out: round11(newIn + span) };
9905
+ d.cam.window = { in: round12(newIn), out: round12(newIn + span) };
9426
9906
  };
9427
9907
  }
9428
9908
  const overlapping = eff.map((s, i) => ({
@@ -9435,8 +9915,8 @@ var camLane = {
9435
9915
  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
9916
  return (d) => {
9437
9917
  d.cam.window = {
9438
- in: round11(Math.max(0, next.in)),
9439
- out: round11(Math.min(sourceDuration, next.out))
9918
+ in: round12(Math.max(0, next.in)),
9919
+ out: round12(Math.min(sourceDuration, next.out))
9440
9920
  };
9441
9921
  };
9442
9922
  },
@@ -9456,8 +9936,8 @@ var micLane = {
9456
9936
  return segments.map((s, i) => ({
9457
9937
  id: `mic-${i}`,
9458
9938
  kind: "clip",
9459
- t: round11(starts[i]),
9460
- duration: round11(
9939
+ t: round12(starts[i]),
9940
+ duration: round12(
9461
9941
  outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i))
9462
9942
  )
9463
9943
  }));
@@ -9513,8 +9993,8 @@ var speedLane = {
9513
9993
  {
9514
9994
  id: sp.id,
9515
9995
  kind: "clip",
9516
- t: round11(start),
9517
- duration: round11(end - start),
9996
+ t: round12(start),
9997
+ duration: round12(end - start),
9518
9998
  label: `${sp.rate}\xD7`
9519
9999
  }
9520
10000
  ];
@@ -9538,8 +10018,8 @@ var speedLane = {
9538
10018
  ...d.speed ?? [],
9539
10019
  {
9540
10020
  id,
9541
- in: round11(srcT),
9542
- out: round11(srcT + len),
10021
+ in: round12(srcT),
10022
+ out: round12(srcT + len),
9543
10023
  rate: DEFAULT_SPEED_RATE,
9544
10024
  source: "manual",
9545
10025
  ...mediaHere ? { media: mediaHere } : {}
@@ -9570,8 +10050,8 @@ var speedLane = {
9570
10050
  return (d) => {
9571
10051
  const s = d.speed?.find((x) => x.id === g.id);
9572
10052
  if (!s) return;
9573
- s.in = round11(newIn);
9574
- s.out = round11(newIn + len);
10053
+ s.in = round12(newIn);
10054
+ s.out = round12(newIn + len);
9575
10055
  s.source = "manual";
9576
10056
  d.speed.sort((a, b) => a.in - b.in);
9577
10057
  };
@@ -9600,8 +10080,8 @@ var speedLane = {
9600
10080
  return (d) => {
9601
10081
  const s = d.speed?.find((x) => x.id === g.id);
9602
10082
  if (!s) return;
9603
- s.in = round11(next.in);
9604
- s.out = round11(next.out);
10083
+ s.in = round12(next.in);
10084
+ s.out = round12(next.out);
9605
10085
  s.source = "manual";
9606
10086
  };
9607
10087
  }
@@ -9620,8 +10100,8 @@ var audioLane = {
9620
10100
  return doc.audio.map((c) => ({
9621
10101
  id: c.id,
9622
10102
  kind: "clip",
9623
- t: round11(c.start),
9624
- duration: round11(clipLength(c)),
10103
+ t: round12(c.start),
10104
+ duration: round12(clipLength(c)),
9625
10105
  label: c.name
9626
10106
  }));
9627
10107
  },
@@ -9632,7 +10112,7 @@ var audioLane = {
9632
10112
  const start = Math.max(0, g.t);
9633
10113
  return (d) => {
9634
10114
  const c = d.audio.find((x) => x.id === g.id);
9635
- if (c) c.start = round11(start);
10115
+ if (c) c.start = round12(start);
9636
10116
  };
9637
10117
  }
9638
10118
  if (g.type === "resize") {
@@ -9645,8 +10125,8 @@ var audioLane = {
9645
10125
  return (d) => {
9646
10126
  const c = d.audio.find((x) => x.id === g.id);
9647
10127
  if (!c) return;
9648
- c.start = round11(newStart2);
9649
- c.loopLen = round11(end - newStart2);
10128
+ c.start = round12(newStart2);
10129
+ c.loopLen = round12(end - newStart2);
9650
10130
  };
9651
10131
  }
9652
10132
  const delta = g.t - clip.start;
@@ -9655,22 +10135,22 @@ var audioLane = {
9655
10135
  return (d) => {
9656
10136
  const c = d.audio.find((x) => x.id === g.id);
9657
10137
  if (!c) return;
9658
- c.in = round11(newIn);
9659
- c.start = round11(newStart);
10138
+ c.in = round12(newIn);
10139
+ c.start = round12(newStart);
9660
10140
  };
9661
10141
  }
9662
10142
  if (clip.loop) {
9663
10143
  const newLen = Math.max(0.1, g.t - clip.start);
9664
10144
  return (d) => {
9665
10145
  const c = d.audio.find((x) => x.id === g.id);
9666
- if (c) c.loopLen = round11(newLen);
10146
+ if (c) c.loopLen = round12(newLen);
9667
10147
  };
9668
10148
  }
9669
10149
  const span = Math.max(0.05, g.t - clip.start);
9670
10150
  const newOut = Math.min(clip.in + span, clip.duration);
9671
10151
  return (d) => {
9672
10152
  const c = d.audio.find((x) => x.id === g.id);
9673
- if (c) c.out = round11(Math.max(c.in + 0.05, newOut));
10153
+ if (c) c.out = round12(Math.max(c.in + 0.05, newOut));
9674
10154
  };
9675
10155
  }
9676
10156
  if (g.type === "remove") {
@@ -9683,8 +10163,8 @@ var audioLane = {
9683
10163
  },
9684
10164
  magnets(doc) {
9685
10165
  return doc.audio.flatMap((c) => [
9686
- round11(c.start),
9687
- round11(c.start + clipLength(c))
10166
+ round12(c.start),
10167
+ round12(c.start + clipLength(c))
9688
10168
  ]);
9689
10169
  }
9690
10170
  };
@@ -9702,7 +10182,7 @@ function poseItems(clipId, start, duration, motion) {
9702
10182
  return (motion ?? []).map((p, i) => ({
9703
10183
  id: `${clipId}::k${i}`,
9704
10184
  kind: "keyframe",
9705
- t: round11(start + Math.min(Math.max(0, p.at), duration))
10185
+ t: round12(start + Math.min(Math.max(0, p.at), duration))
9706
10186
  }));
9707
10187
  }
9708
10188
  var overlaysLane = {
@@ -9713,8 +10193,8 @@ var overlaysLane = {
9713
10193
  {
9714
10194
  id: o.id,
9715
10195
  kind: "clip",
9716
- t: round11(o.start),
9717
- duration: round11(o.duration),
10196
+ t: round12(o.start),
10197
+ duration: round12(o.duration),
9718
10198
  label: o.kind === "text" ? o.text.split("\n")[0].slice(0, 24) || "Text" : o.kind === "html" ? htmlLayerLabel(o) : o.kind === "image" ? "Image" : "Video"
9719
10199
  },
9720
10200
  // Pose diamonds render after the clips, so they sit on top.
@@ -9734,7 +10214,7 @@ var overlaysLane = {
9734
10214
  return (d) => {
9735
10215
  const o = (d.overlays ?? []).find((x) => x.id === kf.clipId);
9736
10216
  const p = o?.motion?.[kf.index];
9737
- if (p) p.at = round11(at);
10217
+ if (p) p.at = round12(at);
9738
10218
  };
9739
10219
  }
9740
10220
  if (g.type === "remove") {
@@ -9763,8 +10243,8 @@ var overlaysLane = {
9763
10243
  {
9764
10244
  id,
9765
10245
  kind: "text",
9766
- start: round11(start),
9767
- duration: round11(len),
10246
+ start: round12(start),
10247
+ duration: round12(len),
9768
10248
  text: "Title",
9769
10249
  preset: "title",
9770
10250
  // Frame fractions: centered, lower-third — at any aspect ratio.
@@ -9779,7 +10259,7 @@ var overlaysLane = {
9779
10259
  const start = Math.max(0, g.t);
9780
10260
  return (d) => {
9781
10261
  const o = (d.overlays ?? []).find((x) => x.id === g.id);
9782
- if (o) o.start = round11(start);
10262
+ if (o) o.start = round12(start);
9783
10263
  };
9784
10264
  }
9785
10265
  if (g.type === "resize") {
@@ -9789,14 +10269,14 @@ var overlaysLane = {
9789
10269
  return (d) => {
9790
10270
  const o = (d.overlays ?? []).find((x) => x.id === g.id);
9791
10271
  if (!o) return;
9792
- o.start = round11(newStart);
9793
- o.duration = round11(end - newStart);
10272
+ o.start = round12(newStart);
10273
+ o.duration = round12(end - newStart);
9794
10274
  };
9795
10275
  }
9796
10276
  const newLen = Math.max(OVERLAY_MIN_DURATION, g.t - clip.start);
9797
10277
  return (d) => {
9798
10278
  const o = (d.overlays ?? []).find((x) => x.id === g.id);
9799
- if (o) o.duration = round11(newLen);
10279
+ if (o) o.duration = round12(newLen);
9800
10280
  };
9801
10281
  }
9802
10282
  return (d) => {
@@ -9805,8 +10285,8 @@ var overlaysLane = {
9805
10285
  },
9806
10286
  magnets(doc) {
9807
10287
  return (doc.overlays ?? []).flatMap((o) => [
9808
- round11(o.start),
9809
- round11(o.start + o.duration)
10288
+ round12(o.start),
10289
+ round12(o.start + o.duration)
9810
10290
  ]);
9811
10291
  }
9812
10292
  };
@@ -9819,8 +10299,8 @@ var objectsLane = {
9819
10299
  {
9820
10300
  id: o.id,
9821
10301
  kind: "clip",
9822
- t: round11(o.span?.start ?? 0),
9823
- duration: round11(o.span?.duration ?? outDur),
10302
+ t: round12(o.span?.start ?? 0),
10303
+ duration: round12(o.span?.duration ?? outDur),
9824
10304
  label: o.asset.kind === "primitive" ? o.asset.shape : o.asset.kind === "text3d" ? o.asset.text : "model"
9825
10305
  },
9826
10306
  // Pose diamonds: clip-local `at` from the span start (0 span-less).
@@ -9848,7 +10328,7 @@ var objectsLane = {
9848
10328
  return (d) => {
9849
10329
  const o = (d.objects ?? []).find((x) => x.id === kf.clipId);
9850
10330
  const p = o?.motion?.[kf.index];
9851
- if (p) p.at = round11(at);
10331
+ if (p) p.at = round12(at);
9852
10332
  };
9853
10333
  }
9854
10334
  if (g.type === "remove") {
@@ -9869,7 +10349,7 @@ var objectsLane = {
9869
10349
  const start = Math.max(0, g.t);
9870
10350
  return (d) => {
9871
10351
  const o = (d.objects ?? []).find((x) => x.id === g.id);
9872
- if (o?.span) o.span.start = round11(start);
10352
+ if (o?.span) o.span.start = round12(start);
9873
10353
  };
9874
10354
  }
9875
10355
  if (g.type === "resize") {
@@ -9880,14 +10360,14 @@ var objectsLane = {
9880
10360
  return (d) => {
9881
10361
  const o = (d.objects ?? []).find((x) => x.id === g.id);
9882
10362
  if (!o?.span) return;
9883
- o.span.start = round11(newStart);
9884
- o.span.duration = round11(end - newStart);
10363
+ o.span.start = round12(newStart);
10364
+ o.span.duration = round12(end - newStart);
9885
10365
  };
9886
10366
  }
9887
10367
  const newLen = Math.max(OVERLAY_MIN_DURATION, g.t - clip.span.start);
9888
10368
  return (d) => {
9889
10369
  const o = (d.objects ?? []).find((x) => x.id === g.id);
9890
- if (o?.span) o.span.duration = round11(newLen);
10370
+ if (o?.span) o.span.duration = round12(newLen);
9891
10371
  };
9892
10372
  }
9893
10373
  return (d) => {
@@ -9896,7 +10376,7 @@ var objectsLane = {
9896
10376
  },
9897
10377
  magnets(doc) {
9898
10378
  return (doc.objects ?? []).flatMap(
9899
- (o) => o.span ? [round11(o.span.start), round11(o.span.start + o.span.duration)] : []
10379
+ (o) => o.span ? [round12(o.span.start), round12(o.span.start + o.span.duration)] : []
9900
10380
  );
9901
10381
  }
9902
10382
  };
@@ -9923,7 +10403,7 @@ function nextSpeedId(doc) {
9923
10403
  function segIndex(id) {
9924
10404
  return Number(id.replace("seg-", ""));
9925
10405
  }
9926
- function round11(v) {
10406
+ function round12(v) {
9927
10407
  return Math.round(v * 1e3) / 1e3;
9928
10408
  }
9929
10409
 
@@ -10164,6 +10644,8 @@ export {
10164
10644
  OVERLAY_SIZE_MIN,
10165
10645
  OVERLAY_TRANSITION_DUR,
10166
10646
  OVERLAY_Z,
10647
+ PIN_GAP,
10648
+ PIN_MARGIN,
10167
10649
  PLATE_GROUND,
10168
10650
  PLAYBACK_ACTIVITY,
10169
10651
  PRIMARY_MEDIA,
@@ -10272,6 +10754,7 @@ export {
10272
10754
  docToCropSpace,
10273
10755
  docToFullSpace,
10274
10756
  docTransitions,
10757
+ docZoomTrack,
10275
10758
  dragsFromTrack,
10276
10759
  dropTemplate,
10277
10760
  duckCurve,
@@ -10289,6 +10772,7 @@ export {
10289
10772
  expectedFrameSize,
10290
10773
  exportSizeFor,
10291
10774
  extractClicks,
10775
+ findStep,
10292
10776
  focusBounds,
10293
10777
  focusForViewportCentre,
10294
10778
  followFocusEvents,
@@ -10360,12 +10844,17 @@ export {
10360
10844
  overlayLines,
10361
10845
  overlayMotionBase,
10362
10846
  overlayMotionPoseAt,
10847
+ overlayPinPoseAt,
10363
10848
  overlayRect,
10364
10849
  overlaysLane,
10365
10850
  ownFreezes,
10366
10851
  pageDisplayUrl,
10367
10852
  parsePoseId,
10368
10853
  pickStyle,
10854
+ pinBox,
10855
+ pinPlacement,
10856
+ pinRectOnScreen,
10857
+ pinReferent,
10369
10858
  placeFreezes,
10370
10859
  planAutoSpeed,
10371
10860
  planAutoTilt,
@@ -10384,6 +10873,7 @@ export {
10384
10873
  resolveExportSize,
10385
10874
  resolveOverlayBox,
10386
10875
  resolveOverlayStyle,
10876
+ resolvePins,
10387
10877
  resolveText3dAsset,
10388
10878
  resolveZoomStyle,
10389
10879
  restLeanOf,