@vosjs/studio-core 0.23.0 → 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
@@ -1164,18 +1164,21 @@ var ZOOM_STYLES = {
1164
1164
  typingGap: 2.5,
1165
1165
  typingHold: 1.1,
1166
1166
  typingMinLevel: 1.4,
1167
- rampIn: 0.55,
1167
+ rampIn: 0.6,
1168
1168
  rampInOverlap: 0.2,
1169
1169
  rampOut: 0.5,
1170
1170
  chainGap: 3,
1171
1171
  pan: 0.55,
1172
1172
  // The reference's corner trace at 60 fps: a quarter of the move by a
1173
- // fifth of the time, two thirds by half, the rest a settle. That is
1174
- // CSS `ease` (0.25, 0.1, 0.25, 1), a small lead-in and a long tail;
1175
- // (0.16, 1, 0.3, 1) put half the move in the first sixty milliseconds
1176
- // and read as a twitch.
1177
- ease: "css-bezier(0.25, 0.1, 0.25, 1)",
1178
- panEase: "css-bezier(0.3, 0.05, 0.25, 1)",
1173
+ // fifth of the time, two thirds by half, nine tenths by three
1174
+ // quarters, over ~0.6 s. Fitted against those three points,
1175
+ // (0.36, 0, 0.4, 1) is the closest cubic (15 / 64 / 93 %; half the
1176
+ // move at ~245 ms and 90 % at ~425 ms at 0.6 s): a gentle start and a
1177
+ // long settle. CSS ease (0.25, 0.1, 0.25, 1) reached the midpoint
1178
+ // 70 ms earlier and read a touch fast; (0.16, 1, 0.3, 1) put half the
1179
+ // move in the first sixty milliseconds and read as a twitch.
1180
+ ease: "css-bezier(0.36, 0, 0.4, 1)",
1181
+ panEase: "css-bezier(0.36, 0, 0.4, 1)",
1179
1182
  followSafeRatio: 0.45,
1180
1183
  followRecenter: 0.55,
1181
1184
  followLookahead: 0.4,
@@ -1825,8 +1828,8 @@ function round(v) {
1825
1828
  // src/lower/lowerToComposition.ts
1826
1829
  import {
1827
1830
  EASINGS,
1828
- lerpArray,
1829
- sample,
1831
+ lerpArray as lerpArray2,
1832
+ sample as sample2,
1830
1833
  segmentRate as segmentRate2,
1831
1834
  sortKeyframes as sortKeyframes2,
1832
1835
  sourceToTimeline,
@@ -2658,14 +2661,14 @@ function resolveOverlayFx(clip, clipDuration) {
2658
2661
  const maxTotal = Math.max(dur, clipDuration * 0.9);
2659
2662
  st = Math.min(st, Math.max(0, (maxTotal - dur) / (upperN - 1)));
2660
2663
  }
2661
- const round12 = (v) => Math.round(v * 1e4) / 1e4;
2664
+ const round13 = (v) => Math.round(v * 1e4) / 1e4;
2662
2665
  return {
2663
2666
  k: spec.fx,
2664
2667
  u: unit,
2665
2668
  d: FX_DIR[spec.direction ?? "forward"] ?? 0,
2666
- st: round12(st),
2667
- dur: round12(dur),
2668
- tt: round12(st * (upperN - 1) + dur),
2669
+ st: round13(st),
2670
+ dur: round13(dur),
2671
+ tt: round13(st * (upperN - 1) + dur),
2669
2672
  units,
2670
2673
  n
2671
2674
  };
@@ -3054,12 +3057,280 @@ function round2(v) {
3054
3057
  return Math.round(v * 1e3) / 1e3;
3055
3058
  }
3056
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
+
3057
3328
  // src/lower/cursorIdle.ts
3058
3329
  var CURSOR_IDLE_HOLD = 1;
3059
3330
  var CURSOR_IDLE_FADE_OUT = 0.35;
3060
3331
  var CURSOR_IDLE_FADE_IN = 0.18;
3061
3332
  var CURSOR_IDLE_EPS_FRAC = 25e-4;
3062
- var round4 = (n) => Math.round(n * 1e3) / 1e3;
3333
+ var round5 = (n) => Math.round(n * 1e3) / 1e3;
3063
3334
  function cursorIdleFade(track, o) {
3064
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 }));
3065
3336
  if (moves.length === 0) return [];
@@ -3111,7 +3382,7 @@ function emit(keys, s, e) {
3111
3382
  push(keys, e + CURSOR_IDLE_FADE_IN, 1);
3112
3383
  }
3113
3384
  function push(keys, t, a) {
3114
- const rt = round4(t);
3385
+ const rt = round5(t);
3115
3386
  if (keys.length > 0 && rt <= keys[keys.length - 1].t) {
3116
3387
  keys[keys.length - 1].a = a;
3117
3388
  return;
@@ -4619,7 +4890,7 @@ function gatedRect(rect, x, y, space) {
4619
4890
  return null;
4620
4891
  const m = RECT_CONTAIN_MARGIN;
4621
4892
  const inside = x >= rect.x - m && x <= rect.x + rect.w + m && y >= rect.y - m && y <= rect.y + rect.h + m;
4622
- 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;
4623
4894
  }
4624
4895
  function extractClicks(track, segments, opts) {
4625
4896
  const out = [];
@@ -4644,11 +4915,11 @@ function extractClicks(track, segments, opts) {
4644
4915
  const upSrc = st + Math.max(pressLen, 0.02);
4645
4916
  const up = keptOutputEnd(segments, st, upSrc, opts.media) ?? ot + CLICK_SYNTH_RELEASE;
4646
4917
  const click = {
4647
- ot: round5(ot),
4648
- up: round5(Math.max(up, ot + 0.02)),
4649
- st: round5(st),
4650
- x: round5(e.x),
4651
- 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),
4652
4923
  b: button
4653
4924
  };
4654
4925
  if (opts.rects && e.rect) {
@@ -4667,7 +4938,7 @@ function hexToRgbTriplet(hex) {
4667
4938
  const n = parseInt(h, 16);
4668
4939
  return [n >> 16, n >> 8 & 255, n & 255];
4669
4940
  }
4670
- function round5(v) {
4941
+ function round6(v) {
4671
4942
  return Math.round(v * 1e3) / 1e3;
4672
4943
  }
4673
4944
 
@@ -4743,7 +5014,7 @@ function trackEmitter() {
4743
5014
  if (tt <= prev.t + 1e-6 && sameVec(prev.value, value)) return prev.t;
4744
5015
  if (tt <= prev.t + 1e-6) tt = prev.t + 1e-3;
4745
5016
  }
4746
- keyframes.push({ t: round6(tt), value: value.map(round6), ease });
5017
+ keyframes.push({ t: round7(tt), value: value.map(round7), ease });
4747
5018
  return tt;
4748
5019
  };
4749
5020
  return { keyframes, push: push2 };
@@ -4920,7 +5191,7 @@ function camBubbleRectAt(doc, t) {
4920
5191
  H
4921
5192
  );
4922
5193
  if (!track.keyframes.length) return camBubbleRect(doc.cam, W, H);
4923
- return camRectFromPose(sample(track, t, lerpArray), doc.cam, W, H);
5194
+ return camRectFromPose(sample2(track, t, lerpArray2), doc.cam, W, H);
4924
5195
  }
4925
5196
  function motionTrack(base, keys, dur) {
4926
5197
  const sorted = keys.filter((k) => Number.isFinite(k.at)).sort((a, b) => a.at - b.at);
@@ -4968,7 +5239,7 @@ function overlayMotionPoseAt(o, t) {
4968
5239
  Math.max(OVERLAY_MIN_DURATION, o.duration)
4969
5240
  );
4970
5241
  if (!track.keyframes.length) return null;
4971
- return [...sample(track, t, lerpArray)];
5242
+ return [...sample2(track, t, lerpArray2)];
4972
5243
  }
4973
5244
  function objectMotionBase(o) {
4974
5245
  const t = o.transform3d;
@@ -4998,7 +5269,7 @@ function objectMotionPoseAt(o, t, clipDur) {
4998
5269
  o.span?.duration ?? clipDur
4999
5270
  );
5000
5271
  if (!track.keyframes.length) return null;
5001
- return [...sample(track, t, lerpArray)];
5272
+ return [...sample2(track, t, lerpArray2)];
5002
5273
  }
5003
5274
  var FRAME_STEP_SRC = `
5004
5275
  function stepPaused(vid, srcT) {
@@ -6380,6 +6651,50 @@ var ON_FRAME = `(ctx, content, dt) => {
6380
6651
  }
6381
6652
  }
6382
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
+
6383
6698
  // cursor (SOURCE-anchored samples, read at the on-screen source moment).
6384
6699
  // The dot is the only thing cursorStyle.visible hides \u2014 the track still drives
6385
6700
  // cursor-follow zoom, and click effects draw above on their own switch.
@@ -6442,8 +6757,15 @@ var ON_FRAME = `(ctx, content, dt) => {
6442
6757
  // Wait only for the FIRST decoded frame, then keep drawing through seeks.
6443
6758
  if (camV && camV.readyState >= 2) r.camHasFrame = true
6444
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
+ }
6445
6767
  var ovSig = W + 'x' + H
6446
- var ovDirty = ov.sig !== ovSig || camActiveNow || ov.active || !!trOut
6768
+ var ovDirty = ov.sig !== ovSig || camActiveNow || ov.active || !!trOut || pqLive
6447
6769
  if (ovDirty) {
6448
6770
  ovC.clearRect(0, 0, W, H)
6449
6771
  // webcam bubble \u2014 pinned to the frame corner regardless of card tilt/zoom.
@@ -6493,6 +6815,57 @@ var ON_FRAME = `(ctx, content, dt) => {
6493
6815
  ovC.restore()
6494
6816
  }
6495
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
+ }
6496
6869
  // The boundary dot: the outgoing card's last cursor point and the
6497
6870
  // incoming's current one, each carried by its card's move, one dot on
6498
6871
  // the eased lerp between them (the idle fade never fires across a page
@@ -6507,7 +6880,7 @@ var ON_FRAME = `(ctx, content, dt) => {
6507
6880
  drawCur(ovC, tqX2, tqY2, tqR, 1, 2 * (trQw || s))
6508
6881
  }
6509
6882
  ov.sig = ovSig
6510
- ov.active = camActiveNow || !!trOut
6883
+ ov.active = camActiveNow || !!trOut || pqLive
6511
6884
  if (ov.texture) ov.texture.needsUpdate = true
6512
6885
  }
6513
6886
 
@@ -6601,7 +6974,7 @@ var ON_FRAME = `(ctx, content, dt) => {
6601
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 }
6602
6975
  }
6603
6976
  }`;
6604
- function studioLayerData(layers, duration) {
6977
+ function studioLayerData(layers, duration, pins) {
6605
6978
  const rec = "source" in layers ? layers : null;
6606
6979
  return {
6607
6980
  // Music/SFX clips with their gain envelopes baked (shared truth for the
@@ -6610,14 +6983,14 @@ function studioLayerData(layers, duration) {
6610
6983
  // needs a decoded recording, which a sync lowering can't produce.
6611
6984
  audio: (layers.audio ?? []).map((c) => ({
6612
6985
  key: c.key,
6613
- start: round6(c.start),
6614
- in: round6(c.in),
6615
- out: round6(c.out),
6616
- gain: round6(c.gain),
6986
+ start: round7(c.start),
6987
+ in: round7(c.in),
6988
+ out: round7(c.out),
6989
+ gain: round7(c.gain),
6617
6990
  loop: !!c.loop,
6618
- len: round6(clipLength(c)),
6991
+ len: round7(clipLength(c)),
6619
6992
  duck: !!c.duck,
6620
- 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) }))
6621
6994
  })),
6622
6995
  // Object clips: numbers resolved HERE; shapes are the drafted engine spec.
6623
6996
  ...layers.objects && layers.objects.length ? {
@@ -6630,17 +7003,17 @@ function studioLayerData(layers, duration) {
6630
7003
  } : o.asset.kind === "text3d" ? resolveText3dAsset(o.asset) : { kind: "gltf", key: o.asset.key },
6631
7004
  ...o.span ? {
6632
7005
  span: {
6633
- start: round6(o.span.start),
6634
- duration: round6(o.span.duration)
7006
+ start: round7(o.span.start),
7007
+ duration: round7(o.span.duration)
6635
7008
  }
6636
7009
  } : {},
6637
- x: round6(o.transform3d.x),
6638
- y: round6(o.transform3d.y),
6639
- z: round6(o.transform3d.z),
6640
- rx: round6(o.transform3d.rx),
6641
- ry: round6(o.transform3d.ry),
6642
- rz: round6(o.transform3d.rz),
6643
- 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),
6644
7017
  anim: idleKey(o),
6645
7018
  // Pose keyframes: clip-local [x,y,z,rx,ry,rz,scale] track,
6646
7019
  // sampled at t − span.start. Omitted when absent — parity.
@@ -6667,18 +7040,20 @@ function studioLayerData(layers, duration) {
6667
7040
  const base = {
6668
7041
  id: o.id,
6669
7042
  kind: o.kind,
6670
- start: round6(o.start),
6671
- dur: round6(Math.max(OVERLAY_MIN_DURATION, o.duration)),
6672
- x: round6(o.transform.x),
6673
- y: round6(o.transform.y),
6674
- scale: round6(o.transform.scale || 1),
6675
- 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),
6676
7049
  enter: enterKey(o),
6677
7050
  exit: exitKey(o),
6678
7051
  // Pose keyframes: a CLIP-LOCAL [x, y, scale, rot, opacity]
6679
7052
  // track, sampled in ON_FRAME at t − start. Omitted when the clip
6680
7053
  // has no motion — data byte parity.
6681
7054
  ...(() => {
7055
+ const pinned = pins?.get(o.id);
7056
+ if (pinned) return { track: pinned.track };
6682
7057
  if (!o.motion || !o.motion.length) return {};
6683
7058
  const mb = overlayMotionBase(o);
6684
7059
  const track = motionTrack(
@@ -6695,10 +7070,10 @@ function studioLayerData(layers, duration) {
6695
7070
  ...base,
6696
7071
  kind: "image",
6697
7072
  key: hl.key,
6698
- w: round6(hl.width),
7073
+ w: round7(hl.width),
6699
7074
  radius: 0,
6700
7075
  shadow: "none",
6701
- opacity: round6(o.opacity ?? 1),
7076
+ opacity: round7(o.opacity ?? 1),
6702
7077
  html: hl.html
6703
7078
  };
6704
7079
  }
@@ -6715,8 +7090,8 @@ function studioLayerData(layers, duration) {
6715
7090
  ...o.frame.browserBar?.theme ? { theme: o.frame.browserBar.theme } : {}
6716
7091
  },
6717
7092
  lean: [
6718
- round6(o.frame.lean?.rx ?? 0),
6719
- round6(o.frame.lean?.ry ?? 0)
7093
+ round7(o.frame.lean?.rx ?? 0),
7094
+ round7(o.frame.lean?.ry ?? 0)
6720
7095
  ],
6721
7096
  shadow: o.frame.shadow ?? LAYER_CARD_SHADOW,
6722
7097
  shadowContact: o.frame.shadowContact ?? 0,
@@ -6732,17 +7107,17 @@ function studioLayerData(layers, duration) {
6732
7107
  for (const ev of lm.cursor) {
6733
7108
  if (ev.type === "down")
6734
7109
  clicks.push({
6735
- t: round6(ev.t / 1e3),
6736
- x: round6(ev.x),
6737
- y: round6(ev.y)
7110
+ t: round7(ev.t / 1e3),
7111
+ x: round7(ev.x),
7112
+ y: round7(ev.y)
6738
7113
  });
6739
7114
  }
6740
7115
  return {
6741
7116
  cur: {
6742
7117
  pts: sm.map((p) => ({
6743
- t: round6(p.t),
6744
- x: round6(p.x),
6745
- y: round6(p.y)
7118
+ t: round7(p.t),
7119
+ x: round7(p.x),
7120
+ y: round7(p.y)
6746
7121
  })),
6747
7122
  space: { w: lm.meta.width, h: lm.meta.height },
6748
7123
  size: rec.cursor.size,
@@ -6757,7 +7132,7 @@ function studioLayerData(layers, duration) {
6757
7132
  ...lm && lm.crop ? { crop: lm.crop } : {},
6758
7133
  ...lmCard ? { card: lmCard } : {},
6759
7134
  ...lmFacts,
6760
- w: round6(o.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH),
7135
+ w: round7(o.width ?? OVERLAY_MEDIA_DEFAULT_WIDTH),
6761
7136
  radius: o.radius ?? OVERLAY_MEDIA_DEFAULT_RADIUS,
6762
7137
  opacity: o.opacity ?? 1,
6763
7138
  loop: !!o.loop,
@@ -6767,7 +7142,7 @@ function studioLayerData(layers, duration) {
6767
7142
  ...o.shadow ? { shadow: o.shadow } : {},
6768
7143
  ...o.border && o.border.width > 0 ? {
6769
7144
  border: {
6770
- width: round6(o.border.width),
7145
+ width: round7(o.border.width),
6771
7146
  color: o.border.color
6772
7147
  }
6773
7148
  } : {}
@@ -6787,11 +7162,11 @@ function studioLayerData(layers, duration) {
6787
7162
  // Style-v2 fields bake only when non-default (byte parity for
6788
7163
  // older docs); ON_FRAME reads them unconditionally.
6789
7164
  ...st.fontStyle === "italic" ? { sty: "italic" } : {},
6790
- ...o.maxWidth ? { mw: round6(o.maxWidth) } : {},
6791
- ...st.letterSpacing ? { ls: round6(st.letterSpacing) } : {},
6792
- ...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) } : {},
6793
7168
  ...st.align !== "center" ? { align: st.align } : {},
6794
- ...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) } } : {},
6795
7170
  // Hosted face behind an override: ON_FRAME lazy-loads it so a
6796
7171
  // live family/weight edit paints without a LOAD (SETUP only
6797
7172
  // runs on cold load).
@@ -6804,10 +7179,10 @@ function studioLayerData(layers, duration) {
6804
7179
  ...bx ? {
6805
7180
  box: {
6806
7181
  c: bx.color,
6807
- o: round6(bx.opacity),
6808
- px: round6(bx.padX),
6809
- py: round6(bx.padY),
6810
- r: round6(bx.radius)
7182
+ o: round7(bx.opacity),
7183
+ px: round7(bx.padX),
7184
+ py: round7(bx.padY),
7185
+ r: round7(bx.radius)
6811
7186
  }
6812
7187
  } : {},
6813
7188
  // Entrance animation: segmentation + timing normalized
@@ -6841,36 +7216,22 @@ function lowerToComposition(input) {
6841
7216
  sourceDuration: (doc.source.meta.durationMs || 0) / 1e3
6842
7217
  }) : [];
6843
7218
  const layout = docCardLayout(doc);
6844
- const meta = doc.source.meta;
6845
7219
  const zoomStyle = resolveZoomStyle(doc.zoomStyle, doc.zoomParams);
6846
7220
  const camera = cameraModel(doc.frame);
6847
- const zoomSpans = restSpansThroughTransitions(
6848
- doc.zoom,
7221
+ const zoomSpans = loweredZoomSpans(
7222
+ doc,
6849
7223
  rated,
6850
7224
  transitions,
6851
- ZOOM_SPAN_MIN
6852
- ).map((z) => {
6853
- if (z.focusMode === "auto") {
6854
- const f = followFocusEvents(
6855
- z,
6856
- doc.source.cursor,
6857
- { w: meta.width, h: meta.height },
6858
- layout,
6859
- {
6860
- safeRatio: zoomStyle.followSafeRatio,
6861
- recenter: zoomStyle.followRecenter,
6862
- lookahead: zoomStyle.followLookahead,
6863
- camera
6864
- }
6865
- );
6866
- if (f.entry)
6867
- return { ...z, cx: f.entry.cx, cy: f.entry.cy, followEvents: f.events };
6868
- }
6869
- return {
6870
- ...z,
6871
- ...clampFocus(z.cx, z.cy, clampZoomLevel(z.level), layout, camera)
6872
- };
6873
- });
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);
6874
7235
  const data = {
6875
7236
  videoSrc: doc.source.videoKey,
6876
7237
  isImage: doc.source.sourceKind === "image",
@@ -6893,17 +7254,17 @@ function lowerToComposition(input) {
6893
7254
  // Rated segments (speed spans pre-intersected): ON_FRAME's mapTime and the
6894
7255
  // export's audio splice read the rate straight off each segment.
6895
7256
  segments: rated.map((seg) => ({
6896
- in: round6(seg.in),
6897
- out: round6(seg.out),
6898
- ...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) } : {},
6899
7260
  ...seg.media ? { media: seg.media } : {}
6900
7261
  })),
6901
7262
  frame: doc.frame,
6902
7263
  micGain: doc.micGain ?? 1,
6903
7264
  cursor: smoothed.map((p) => ({
6904
- t: round6(p.t),
6905
- x: round6(p.x),
6906
- y: round6(p.y)
7265
+ t: round7(p.t),
7266
+ x: round7(p.x),
7267
+ y: round7(p.y)
6907
7268
  })),
6908
7269
  cursorStyle: doc.cursor,
6909
7270
  cursorSpace: { w: doc.source.meta.width, h: doc.source.meta.height },
@@ -6926,9 +7287,9 @@ function lowerToComposition(input) {
6926
7287
  crop: m.crop ?? null,
6927
7288
  hasAudio: !!m.meta.hasAudio,
6928
7289
  cursor: sm.map((p) => ({
6929
- t: round6(p.t),
6930
- x: round6(p.x),
6931
- y: round6(p.y)
7290
+ t: round7(p.t),
7291
+ x: round7(p.x),
7292
+ y: round7(p.y)
6932
7293
  })),
6933
7294
  cursorSpace: { w: m.meta.width, h: m.meta.height },
6934
7295
  // The card this media wears (its own placement, bar, corner,
@@ -6947,11 +7308,11 @@ function lowerToComposition(input) {
6947
7308
  // intensities/colors become numbers HERE — ON_FRAME reads no registry).
6948
7309
  ...clickFxData(doc, rated),
6949
7310
  // Rated segments so zoom spans land at their speed-adjusted output times.
6950
- // A pull-out entrance writes the track's head.
6951
- zoomTrack: prependEntrance(
6952
- zoomTrackFromDoc(zoomSpans, rated, zoomStyle),
6953
- entranceZoomKeyframes(cardEnter(doc.frame))
6954
- ),
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 } : {},
6955
7316
  // Tilt spans: OUTPUT-time [rx, ry] degree track. The rest pose is
6956
7317
  // FLAT — there is no static card tilt any more — and the motion constants
6957
7318
  // come from the camera style's tilt personality ('drift' slows its
@@ -6984,7 +7345,7 @@ function lowerToComposition(input) {
6984
7345
  };
6985
7346
  const entryData = {
6986
7347
  lights: true,
6987
- ...studioLayerData(doc, duration)
7348
+ ...studioLayerData(doc, duration, pins)
6988
7349
  };
6989
7350
  const config = {
6990
7351
  version: 2,
@@ -7010,6 +7371,128 @@ function lowerToComposition(input) {
7010
7371
  };
7011
7372
  return { config, data, stack: { [STUDIO_ENTRY_ID]: entryData }, duration };
7012
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
+ }
7013
7496
  function clickFxData(doc, rated) {
7014
7497
  const fx = doc.cursor.clickFx;
7015
7498
  const on = fx.style !== "none" || fx.press;
@@ -7043,7 +7526,7 @@ function clickFxData(doc, rated) {
7043
7526
  }
7044
7527
  };
7045
7528
  }
7046
- function round6(v) {
7529
+ function round7(v) {
7047
7530
  return Math.round(v * 1e3) / 1e3;
7048
7531
  }
7049
7532
 
@@ -7133,8 +7616,8 @@ function planAutoSpeed(track, opts) {
7133
7616
  accepted.sort((a, b) => a.in - b.in);
7134
7617
  return accepted.map((c, i) => ({
7135
7618
  id: `s${i}`,
7136
- in: round7(c.in),
7137
- out: round7(c.out),
7619
+ in: round8(c.in),
7620
+ out: round8(c.out),
7138
7621
  rate: clampSpeedRate(c.rate),
7139
7622
  source: "auto"
7140
7623
  }));
@@ -7187,7 +7670,7 @@ function collectRuns(evs, gap, minLen, emit2) {
7187
7670
  }
7188
7671
  flush();
7189
7672
  }
7190
- function round7(v) {
7673
+ function round8(v) {
7191
7674
  return Math.round(v * 1e3) / 1e3;
7192
7675
  }
7193
7676
 
@@ -7368,16 +7851,16 @@ function momentsFromDoc(doc, plan, opts = {}) {
7368
7851
  return {
7369
7852
  id: `m${String(i + 1).padStart(2, "0")}`,
7370
7853
  kind: d.kind,
7371
- source: { in: round8(d.in), out: round8(d.out) },
7372
- output: output ? { in: round8(output.start), out: round8(output.end) } : null,
7373
- at: d.at === null ? null : round8(d.at),
7374
- outputAt: outAt ? round8(outAt.start) : null,
7375
- 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,
7376
7859
  rect: d.rect ? {
7377
- x: round8(d.rect.x),
7378
- y: round8(d.rect.y),
7379
- w: round8(d.rect.w),
7380
- 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)
7381
7864
  } : null,
7382
7865
  ...d.clicks !== void 0 ? { clicks: d.clicks } : {},
7383
7866
  ...d.surface ? { surface: true } : {},
@@ -7398,7 +7881,7 @@ function activityOf(bins, d) {
7398
7881
  sum += bins[i];
7399
7882
  n++;
7400
7883
  }
7401
- return n ? round8(sum / n) : null;
7884
+ return n ? round9(sum / n) : null;
7402
7885
  }
7403
7886
  function saidOver(transcript, d) {
7404
7887
  if (!transcript?.length) return null;
@@ -7407,7 +7890,7 @@ function saidOver(transcript, d) {
7407
7890
  const text = transcript.filter((s) => s.start < hi && s.end > lo).map((s) => s.text.trim()).filter(Boolean).join(" ");
7408
7891
  return text || null;
7409
7892
  }
7410
- function round8(v) {
7893
+ function round9(v) {
7411
7894
  return Math.round(v * 1e3) / 1e3;
7412
7895
  }
7413
7896
 
@@ -7901,8 +8384,8 @@ function buildDigest(input) {
7901
8384
  return {
7902
8385
  digestVersion: DIGEST_VERSION,
7903
8386
  take: {
7904
- sourceDuration: round9(meta.durationMs / 1e3),
7905
- outputDuration: round9(input.outputDuration),
8387
+ sourceDuration: round10(meta.durationMs / 1e3),
8388
+ outputDuration: round10(input.outputDuration),
7906
8389
  width: meta.width,
7907
8390
  height: meta.height,
7908
8391
  captureWidth: meta.captureWidth ?? null,
@@ -7951,7 +8434,7 @@ function buildDigest(input) {
7951
8434
  function outputDurationOf(doc) {
7952
8435
  return docOutputDuration(doc);
7953
8436
  }
7954
- function round9(v) {
8437
+ function round10(v) {
7955
8438
  return Math.round(v * 1e3) / 1e3;
7956
8439
  }
7957
8440
 
@@ -7977,20 +8460,20 @@ function setSpeedInRange(spans, srcIn, srcOut, rate) {
7977
8460
  continue;
7978
8461
  }
7979
8462
  if (srcIn - s.in >= MIN_REMAINDER)
7980
- kept.push({ ...s, out: round10(srcIn), source: "manual" });
8463
+ kept.push({ ...s, out: round11(srcIn), source: "manual" });
7981
8464
  if (s.out - srcOut >= MIN_REMAINDER)
7982
8465
  kept.push({
7983
8466
  ...s,
7984
8467
  id: mintId(used),
7985
- in: round10(srcOut),
8468
+ in: round11(srcOut),
7986
8469
  source: "manual"
7987
8470
  });
7988
8471
  }
7989
8472
  if (rate != null)
7990
8473
  kept.push({
7991
8474
  id: mintId(used),
7992
- in: round10(srcIn),
7993
- out: round10(srcOut),
8475
+ in: round11(srcIn),
8476
+ out: round11(srcOut),
7994
8477
  rate: clampSpeedRate(rate),
7995
8478
  source: "manual"
7996
8479
  });
@@ -8006,9 +8489,9 @@ function removeSourceRange(segments, srcIn, srcOut) {
8006
8489
  next.push(seg);
8007
8490
  continue;
8008
8491
  }
8009
- 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) });
8010
8493
  if (seg.out - srcOut >= MIN_SEGMENT)
8011
- next.push({ ...seg, in: round10(srcOut) });
8494
+ next.push({ ...seg, in: round11(srcOut) });
8012
8495
  }
8013
8496
  return next.length ? next : [...segments];
8014
8497
  }
@@ -8026,8 +8509,8 @@ function zoomSpanForRange(zoom, srcIn, srcOut) {
8026
8509
  while (used.has(`u${n}`)) n++;
8027
8510
  return {
8028
8511
  id: `u${n}`,
8029
- in: round10(start),
8030
- out: round10(end),
8512
+ in: round11(start),
8513
+ out: round11(end),
8031
8514
  level: DEFAULT_ZOOM_LEVEL,
8032
8515
  cx: 0.5,
8033
8516
  cy: 0.5,
@@ -8041,7 +8524,7 @@ function mintId(used) {
8041
8524
  used.add(id);
8042
8525
  return id;
8043
8526
  }
8044
- function round10(v) {
8527
+ function round11(v) {
8045
8528
  return Math.round(v * 1e3) / 1e3;
8046
8529
  }
8047
8530
 
@@ -8745,8 +9228,8 @@ var videoLane = {
8745
9228
  return segments.map((s, i) => ({
8746
9229
  id: `seg-${i}`,
8747
9230
  kind: "clip",
8748
- t: round11(starts[i]),
8749
- duration: round11(
9231
+ t: round12(starts[i]),
9232
+ duration: round12(
8750
9233
  outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i))
8751
9234
  )
8752
9235
  }));
@@ -8829,7 +9312,7 @@ var videoLane = {
8829
9312
  if (local >= m.t - 1e-9 && local <= m.t + m.duration + 1e-9)
8830
9313
  return null;
8831
9314
  }
8832
- const sourceT = round11(
9315
+ const sourceT = round12(
8833
9316
  mapTime2(segmentPieces(s, speeds, freezes, leading), local)
8834
9317
  );
8835
9318
  if (sourceT - s.in < 0.05 || s.out - sourceT < 0.05) return null;
@@ -8870,7 +9353,7 @@ var videoLane = {
8870
9353
  n === 1 || !contiguous(segments, n - 1)
8871
9354
  )
8872
9355
  ] : []
8873
- ].map(round11);
9356
+ ].map(round12);
8874
9357
  }
8875
9358
  };
8876
9359
  function nextFreezeId(doc) {
@@ -8897,8 +9380,8 @@ var freezeLane = {
8897
9380
  {
8898
9381
  id: f.id,
8899
9382
  kind: "clip",
8900
- t: round11(m.t),
8901
- duration: round11(m.duration),
9383
+ t: round12(m.t),
9384
+ duration: round12(m.duration),
8902
9385
  label: `${trimZeros(f.seconds)}s`
8903
9386
  }
8904
9387
  ] : [];
@@ -8920,7 +9403,7 @@ var freezeLane = {
8920
9403
  if (g.t >= m.t - 1e-9 && g.t <= m.t + m.duration + 1e-9) return null;
8921
9404
  }
8922
9405
  if (g.t > totalDuration7(rated) + 1e-9) return null;
8923
- const srcT = round11(mapTime2(rated, Math.max(0, g.t)));
9406
+ const srcT = round12(mapTime2(rated, Math.max(0, g.t)));
8924
9407
  if (!kept(srcT) || !clear(srcT)) return null;
8925
9408
  const id = nextFreezeId(doc);
8926
9409
  const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
@@ -8962,7 +9445,7 @@ var freezeLane = {
8962
9445
  const marks = freezeOutputExtents(splitBySpeed4(segs, speeds), freezes);
8963
9446
  const mine = marks.get(f.id);
8964
9447
  const endOut = mine ? mine.t + mine.duration : null;
8965
- const srcT = round11(
9448
+ const srcT = round12(
8966
9449
  Math.min(sourceDuration, mapTime2(base, Math.max(0, g.t)))
8967
9450
  );
8968
9451
  if (!kept(srcT) || !clear(srcT, f.id)) return null;
@@ -9022,8 +9505,8 @@ var zoomLane = {
9022
9505
  {
9023
9506
  id: z.id,
9024
9507
  kind: "clip",
9025
- t: round11(ext.start),
9026
- duration: round11(ext.end - ext.start),
9508
+ t: round12(ext.start),
9509
+ duration: round12(ext.end - ext.start),
9027
9510
  label: `${z.level.toFixed(2)}\xD7`
9028
9511
  }
9029
9512
  ];
@@ -9050,8 +9533,8 @@ var zoomLane = {
9050
9533
  ...d.zoom,
9051
9534
  {
9052
9535
  id,
9053
- in: round11(srcT),
9054
- out: round11(srcT + len),
9536
+ in: round12(srcT),
9537
+ out: round12(srcT + len),
9055
9538
  level: DEFAULT_ZOOM_LEVEL,
9056
9539
  cx,
9057
9540
  cy,
@@ -9083,8 +9566,8 @@ var zoomLane = {
9083
9566
  return (d) => {
9084
9567
  const z = d.zoom.find((x) => x.id === g.id);
9085
9568
  if (!z) return;
9086
- z.in = round11(newIn);
9087
- z.out = round11(newIn + len);
9569
+ z.in = round12(newIn);
9570
+ z.out = round12(newIn + len);
9088
9571
  z.source = "manual";
9089
9572
  d.zoom.sort((a, b) => a.in - b.in);
9090
9573
  };
@@ -9113,8 +9596,8 @@ var zoomLane = {
9113
9596
  return (d) => {
9114
9597
  const z = d.zoom.find((x) => x.id === g.id);
9115
9598
  if (!z) return;
9116
- z.in = round11(next.in);
9117
- z.out = round11(next.out);
9599
+ z.in = round12(next.in);
9600
+ z.out = round12(next.out);
9118
9601
  z.source = "manual";
9119
9602
  };
9120
9603
  }
@@ -9136,8 +9619,8 @@ function cursorFocusAt(doc, srcT) {
9136
9619
  if (Math.abs(e.t - ms) < Math.abs(best.t - ms)) best = e;
9137
9620
  const clamp015 = (v) => Math.max(0, Math.min(1, v));
9138
9621
  return {
9139
- cx: round11(clamp015(best.x / width)),
9140
- cy: round11(clamp015(best.y / height))
9622
+ cx: round12(clamp015(best.x / width)),
9623
+ cy: round12(clamp015(best.y / height))
9141
9624
  };
9142
9625
  }
9143
9626
  var tiltLane = {
@@ -9151,8 +9634,8 @@ var tiltLane = {
9151
9634
  {
9152
9635
  id: z.id,
9153
9636
  kind: "clip",
9154
- t: round11(ext.start),
9155
- duration: round11(ext.end - ext.start),
9637
+ t: round12(ext.start),
9638
+ duration: round12(ext.end - ext.start),
9156
9639
  label: `${formatDeg(z.rx)}\xB0/${formatDeg(z.ry)}\xB0`
9157
9640
  }
9158
9641
  ];
@@ -9178,8 +9661,8 @@ var tiltLane = {
9178
9661
  ...d.tilt ?? [],
9179
9662
  {
9180
9663
  id,
9181
- in: round11(srcT),
9182
- out: round11(srcT + len),
9664
+ in: round12(srcT),
9665
+ out: round12(srcT + len),
9183
9666
  rx: DEFAULT_TILT_POSE.rx,
9184
9667
  ry: DEFAULT_TILT_POSE.ry,
9185
9668
  source: "manual",
@@ -9210,8 +9693,8 @@ var tiltLane = {
9210
9693
  return (d) => {
9211
9694
  const z = (d.tilt ?? []).find((x) => x.id === g.id);
9212
9695
  if (!z || !d.tilt) return;
9213
- z.in = round11(newIn);
9214
- z.out = round11(newIn + len);
9696
+ z.in = round12(newIn);
9697
+ z.out = round12(newIn + len);
9215
9698
  z.source = "manual";
9216
9699
  d.tilt.sort((a, b) => a.in - b.in);
9217
9700
  };
@@ -9240,8 +9723,8 @@ var tiltLane = {
9240
9723
  return (d) => {
9241
9724
  const z = (d.tilt ?? []).find((x) => x.id === g.id);
9242
9725
  if (!z) return;
9243
- z.in = round11(next.in);
9244
- z.out = round11(next.out);
9726
+ z.in = round12(next.in);
9727
+ z.out = round12(next.out);
9245
9728
  z.source = "manual";
9246
9729
  };
9247
9730
  }
@@ -9268,8 +9751,8 @@ var camMoveLane = {
9268
9751
  {
9269
9752
  id: z.id,
9270
9753
  kind: "clip",
9271
- t: round11(ext.start),
9272
- duration: round11(ext.end - ext.start),
9754
+ t: round12(ext.start),
9755
+ duration: round12(ext.end - ext.start),
9273
9756
  label: z.size != null ? `${Math.round(z.size * 100)}%` : "Move"
9274
9757
  }
9275
9758
  ];
@@ -9293,8 +9776,8 @@ var camMoveLane = {
9293
9776
  ...d.camMotion ?? [],
9294
9777
  {
9295
9778
  id,
9296
- in: round11(srcT),
9297
- out: round11(srcT + len),
9779
+ in: round12(srcT),
9780
+ out: round12(srcT + len),
9298
9781
  ...DEFAULT_CAM_POSE,
9299
9782
  source: "manual"
9300
9783
  }
@@ -9323,8 +9806,8 @@ var camMoveLane = {
9323
9806
  return (d) => {
9324
9807
  const z = (d.camMotion ?? []).find((x) => x.id === g.id);
9325
9808
  if (!z || !d.camMotion) return;
9326
- z.in = round11(newIn);
9327
- z.out = round11(newIn + len);
9809
+ z.in = round12(newIn);
9810
+ z.out = round12(newIn + len);
9328
9811
  z.source = "manual";
9329
9812
  d.camMotion.sort((a, b) => a.in - b.in);
9330
9813
  };
@@ -9353,8 +9836,8 @@ var camMoveLane = {
9353
9836
  return (d) => {
9354
9837
  const z = (d.camMotion ?? []).find((x) => x.id === g.id);
9355
9838
  if (!z) return;
9356
- z.in = round11(next.in);
9357
- z.out = round11(next.out);
9839
+ z.in = round12(next.in);
9840
+ z.out = round12(next.out);
9358
9841
  z.source = "manual";
9359
9842
  };
9360
9843
  }
@@ -9387,10 +9870,10 @@ var camLane = {
9387
9870
  items.push({
9388
9871
  id: `cam-${i}`,
9389
9872
  kind: "clip",
9390
- t: round11(
9873
+ t: round12(
9391
9874
  starts[i] + outputLen({ in: s.in, out: a }, speeds, freezes, leading)
9392
9875
  ),
9393
- duration: round11(
9876
+ duration: round12(
9394
9877
  outputLen({ in: a, out: b }, speeds, freezes, a === s.in && leading)
9395
9878
  )
9396
9879
  });
@@ -9419,7 +9902,7 @@ var camLane = {
9419
9902
  Math.max(first.in, last.out - span)
9420
9903
  );
9421
9904
  return (d) => {
9422
- d.cam.window = { in: round11(newIn), out: round11(newIn + span) };
9905
+ d.cam.window = { in: round12(newIn), out: round12(newIn + span) };
9423
9906
  };
9424
9907
  }
9425
9908
  const overlapping = eff.map((s, i) => ({
@@ -9432,8 +9915,8 @@ var camLane = {
9432
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) };
9433
9916
  return (d) => {
9434
9917
  d.cam.window = {
9435
- in: round11(Math.max(0, next.in)),
9436
- out: round11(Math.min(sourceDuration, next.out))
9918
+ in: round12(Math.max(0, next.in)),
9919
+ out: round12(Math.min(sourceDuration, next.out))
9437
9920
  };
9438
9921
  };
9439
9922
  },
@@ -9453,8 +9936,8 @@ var micLane = {
9453
9936
  return segments.map((s, i) => ({
9454
9937
  id: `mic-${i}`,
9455
9938
  kind: "clip",
9456
- t: round11(starts[i]),
9457
- duration: round11(
9939
+ t: round12(starts[i]),
9940
+ duration: round12(
9458
9941
  outputLen(s, speeds, freezes, i === 0 || !contiguous(segments, i))
9459
9942
  )
9460
9943
  }));
@@ -9510,8 +9993,8 @@ var speedLane = {
9510
9993
  {
9511
9994
  id: sp.id,
9512
9995
  kind: "clip",
9513
- t: round11(start),
9514
- duration: round11(end - start),
9996
+ t: round12(start),
9997
+ duration: round12(end - start),
9515
9998
  label: `${sp.rate}\xD7`
9516
9999
  }
9517
10000
  ];
@@ -9535,8 +10018,8 @@ var speedLane = {
9535
10018
  ...d.speed ?? [],
9536
10019
  {
9537
10020
  id,
9538
- in: round11(srcT),
9539
- out: round11(srcT + len),
10021
+ in: round12(srcT),
10022
+ out: round12(srcT + len),
9540
10023
  rate: DEFAULT_SPEED_RATE,
9541
10024
  source: "manual",
9542
10025
  ...mediaHere ? { media: mediaHere } : {}
@@ -9567,8 +10050,8 @@ var speedLane = {
9567
10050
  return (d) => {
9568
10051
  const s = d.speed?.find((x) => x.id === g.id);
9569
10052
  if (!s) return;
9570
- s.in = round11(newIn);
9571
- s.out = round11(newIn + len);
10053
+ s.in = round12(newIn);
10054
+ s.out = round12(newIn + len);
9572
10055
  s.source = "manual";
9573
10056
  d.speed.sort((a, b) => a.in - b.in);
9574
10057
  };
@@ -9597,8 +10080,8 @@ var speedLane = {
9597
10080
  return (d) => {
9598
10081
  const s = d.speed?.find((x) => x.id === g.id);
9599
10082
  if (!s) return;
9600
- s.in = round11(next.in);
9601
- s.out = round11(next.out);
10083
+ s.in = round12(next.in);
10084
+ s.out = round12(next.out);
9602
10085
  s.source = "manual";
9603
10086
  };
9604
10087
  }
@@ -9617,8 +10100,8 @@ var audioLane = {
9617
10100
  return doc.audio.map((c) => ({
9618
10101
  id: c.id,
9619
10102
  kind: "clip",
9620
- t: round11(c.start),
9621
- duration: round11(clipLength(c)),
10103
+ t: round12(c.start),
10104
+ duration: round12(clipLength(c)),
9622
10105
  label: c.name
9623
10106
  }));
9624
10107
  },
@@ -9629,7 +10112,7 @@ var audioLane = {
9629
10112
  const start = Math.max(0, g.t);
9630
10113
  return (d) => {
9631
10114
  const c = d.audio.find((x) => x.id === g.id);
9632
- if (c) c.start = round11(start);
10115
+ if (c) c.start = round12(start);
9633
10116
  };
9634
10117
  }
9635
10118
  if (g.type === "resize") {
@@ -9642,8 +10125,8 @@ var audioLane = {
9642
10125
  return (d) => {
9643
10126
  const c = d.audio.find((x) => x.id === g.id);
9644
10127
  if (!c) return;
9645
- c.start = round11(newStart2);
9646
- c.loopLen = round11(end - newStart2);
10128
+ c.start = round12(newStart2);
10129
+ c.loopLen = round12(end - newStart2);
9647
10130
  };
9648
10131
  }
9649
10132
  const delta = g.t - clip.start;
@@ -9652,22 +10135,22 @@ var audioLane = {
9652
10135
  return (d) => {
9653
10136
  const c = d.audio.find((x) => x.id === g.id);
9654
10137
  if (!c) return;
9655
- c.in = round11(newIn);
9656
- c.start = round11(newStart);
10138
+ c.in = round12(newIn);
10139
+ c.start = round12(newStart);
9657
10140
  };
9658
10141
  }
9659
10142
  if (clip.loop) {
9660
10143
  const newLen = Math.max(0.1, g.t - clip.start);
9661
10144
  return (d) => {
9662
10145
  const c = d.audio.find((x) => x.id === g.id);
9663
- if (c) c.loopLen = round11(newLen);
10146
+ if (c) c.loopLen = round12(newLen);
9664
10147
  };
9665
10148
  }
9666
10149
  const span = Math.max(0.05, g.t - clip.start);
9667
10150
  const newOut = Math.min(clip.in + span, clip.duration);
9668
10151
  return (d) => {
9669
10152
  const c = d.audio.find((x) => x.id === g.id);
9670
- 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));
9671
10154
  };
9672
10155
  }
9673
10156
  if (g.type === "remove") {
@@ -9680,8 +10163,8 @@ var audioLane = {
9680
10163
  },
9681
10164
  magnets(doc) {
9682
10165
  return doc.audio.flatMap((c) => [
9683
- round11(c.start),
9684
- round11(c.start + clipLength(c))
10166
+ round12(c.start),
10167
+ round12(c.start + clipLength(c))
9685
10168
  ]);
9686
10169
  }
9687
10170
  };
@@ -9699,7 +10182,7 @@ function poseItems(clipId, start, duration, motion) {
9699
10182
  return (motion ?? []).map((p, i) => ({
9700
10183
  id: `${clipId}::k${i}`,
9701
10184
  kind: "keyframe",
9702
- t: round11(start + Math.min(Math.max(0, p.at), duration))
10185
+ t: round12(start + Math.min(Math.max(0, p.at), duration))
9703
10186
  }));
9704
10187
  }
9705
10188
  var overlaysLane = {
@@ -9710,8 +10193,8 @@ var overlaysLane = {
9710
10193
  {
9711
10194
  id: o.id,
9712
10195
  kind: "clip",
9713
- t: round11(o.start),
9714
- duration: round11(o.duration),
10196
+ t: round12(o.start),
10197
+ duration: round12(o.duration),
9715
10198
  label: o.kind === "text" ? o.text.split("\n")[0].slice(0, 24) || "Text" : o.kind === "html" ? htmlLayerLabel(o) : o.kind === "image" ? "Image" : "Video"
9716
10199
  },
9717
10200
  // Pose diamonds render after the clips, so they sit on top.
@@ -9731,7 +10214,7 @@ var overlaysLane = {
9731
10214
  return (d) => {
9732
10215
  const o = (d.overlays ?? []).find((x) => x.id === kf.clipId);
9733
10216
  const p = o?.motion?.[kf.index];
9734
- if (p) p.at = round11(at);
10217
+ if (p) p.at = round12(at);
9735
10218
  };
9736
10219
  }
9737
10220
  if (g.type === "remove") {
@@ -9760,8 +10243,8 @@ var overlaysLane = {
9760
10243
  {
9761
10244
  id,
9762
10245
  kind: "text",
9763
- start: round11(start),
9764
- duration: round11(len),
10246
+ start: round12(start),
10247
+ duration: round12(len),
9765
10248
  text: "Title",
9766
10249
  preset: "title",
9767
10250
  // Frame fractions: centered, lower-third — at any aspect ratio.
@@ -9776,7 +10259,7 @@ var overlaysLane = {
9776
10259
  const start = Math.max(0, g.t);
9777
10260
  return (d) => {
9778
10261
  const o = (d.overlays ?? []).find((x) => x.id === g.id);
9779
- if (o) o.start = round11(start);
10262
+ if (o) o.start = round12(start);
9780
10263
  };
9781
10264
  }
9782
10265
  if (g.type === "resize") {
@@ -9786,14 +10269,14 @@ var overlaysLane = {
9786
10269
  return (d) => {
9787
10270
  const o = (d.overlays ?? []).find((x) => x.id === g.id);
9788
10271
  if (!o) return;
9789
- o.start = round11(newStart);
9790
- o.duration = round11(end - newStart);
10272
+ o.start = round12(newStart);
10273
+ o.duration = round12(end - newStart);
9791
10274
  };
9792
10275
  }
9793
10276
  const newLen = Math.max(OVERLAY_MIN_DURATION, g.t - clip.start);
9794
10277
  return (d) => {
9795
10278
  const o = (d.overlays ?? []).find((x) => x.id === g.id);
9796
- if (o) o.duration = round11(newLen);
10279
+ if (o) o.duration = round12(newLen);
9797
10280
  };
9798
10281
  }
9799
10282
  return (d) => {
@@ -9802,8 +10285,8 @@ var overlaysLane = {
9802
10285
  },
9803
10286
  magnets(doc) {
9804
10287
  return (doc.overlays ?? []).flatMap((o) => [
9805
- round11(o.start),
9806
- round11(o.start + o.duration)
10288
+ round12(o.start),
10289
+ round12(o.start + o.duration)
9807
10290
  ]);
9808
10291
  }
9809
10292
  };
@@ -9816,8 +10299,8 @@ var objectsLane = {
9816
10299
  {
9817
10300
  id: o.id,
9818
10301
  kind: "clip",
9819
- t: round11(o.span?.start ?? 0),
9820
- duration: round11(o.span?.duration ?? outDur),
10302
+ t: round12(o.span?.start ?? 0),
10303
+ duration: round12(o.span?.duration ?? outDur),
9821
10304
  label: o.asset.kind === "primitive" ? o.asset.shape : o.asset.kind === "text3d" ? o.asset.text : "model"
9822
10305
  },
9823
10306
  // Pose diamonds: clip-local `at` from the span start (0 span-less).
@@ -9845,7 +10328,7 @@ var objectsLane = {
9845
10328
  return (d) => {
9846
10329
  const o = (d.objects ?? []).find((x) => x.id === kf.clipId);
9847
10330
  const p = o?.motion?.[kf.index];
9848
- if (p) p.at = round11(at);
10331
+ if (p) p.at = round12(at);
9849
10332
  };
9850
10333
  }
9851
10334
  if (g.type === "remove") {
@@ -9866,7 +10349,7 @@ var objectsLane = {
9866
10349
  const start = Math.max(0, g.t);
9867
10350
  return (d) => {
9868
10351
  const o = (d.objects ?? []).find((x) => x.id === g.id);
9869
- if (o?.span) o.span.start = round11(start);
10352
+ if (o?.span) o.span.start = round12(start);
9870
10353
  };
9871
10354
  }
9872
10355
  if (g.type === "resize") {
@@ -9877,14 +10360,14 @@ var objectsLane = {
9877
10360
  return (d) => {
9878
10361
  const o = (d.objects ?? []).find((x) => x.id === g.id);
9879
10362
  if (!o?.span) return;
9880
- o.span.start = round11(newStart);
9881
- o.span.duration = round11(end - newStart);
10363
+ o.span.start = round12(newStart);
10364
+ o.span.duration = round12(end - newStart);
9882
10365
  };
9883
10366
  }
9884
10367
  const newLen = Math.max(OVERLAY_MIN_DURATION, g.t - clip.span.start);
9885
10368
  return (d) => {
9886
10369
  const o = (d.objects ?? []).find((x) => x.id === g.id);
9887
- if (o?.span) o.span.duration = round11(newLen);
10370
+ if (o?.span) o.span.duration = round12(newLen);
9888
10371
  };
9889
10372
  }
9890
10373
  return (d) => {
@@ -9893,7 +10376,7 @@ var objectsLane = {
9893
10376
  },
9894
10377
  magnets(doc) {
9895
10378
  return (doc.objects ?? []).flatMap(
9896
- (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)] : []
9897
10380
  );
9898
10381
  }
9899
10382
  };
@@ -9920,7 +10403,7 @@ function nextSpeedId(doc) {
9920
10403
  function segIndex(id) {
9921
10404
  return Number(id.replace("seg-", ""));
9922
10405
  }
9923
- function round11(v) {
10406
+ function round12(v) {
9924
10407
  return Math.round(v * 1e3) / 1e3;
9925
10408
  }
9926
10409
 
@@ -10161,6 +10644,8 @@ export {
10161
10644
  OVERLAY_SIZE_MIN,
10162
10645
  OVERLAY_TRANSITION_DUR,
10163
10646
  OVERLAY_Z,
10647
+ PIN_GAP,
10648
+ PIN_MARGIN,
10164
10649
  PLATE_GROUND,
10165
10650
  PLAYBACK_ACTIVITY,
10166
10651
  PRIMARY_MEDIA,
@@ -10269,6 +10754,7 @@ export {
10269
10754
  docToCropSpace,
10270
10755
  docToFullSpace,
10271
10756
  docTransitions,
10757
+ docZoomTrack,
10272
10758
  dragsFromTrack,
10273
10759
  dropTemplate,
10274
10760
  duckCurve,
@@ -10286,6 +10772,7 @@ export {
10286
10772
  expectedFrameSize,
10287
10773
  exportSizeFor,
10288
10774
  extractClicks,
10775
+ findStep,
10289
10776
  focusBounds,
10290
10777
  focusForViewportCentre,
10291
10778
  followFocusEvents,
@@ -10357,12 +10844,17 @@ export {
10357
10844
  overlayLines,
10358
10845
  overlayMotionBase,
10359
10846
  overlayMotionPoseAt,
10847
+ overlayPinPoseAt,
10360
10848
  overlayRect,
10361
10849
  overlaysLane,
10362
10850
  ownFreezes,
10363
10851
  pageDisplayUrl,
10364
10852
  parsePoseId,
10365
10853
  pickStyle,
10854
+ pinBox,
10855
+ pinPlacement,
10856
+ pinRectOnScreen,
10857
+ pinReferent,
10366
10858
  placeFreezes,
10367
10859
  planAutoSpeed,
10368
10860
  planAutoTilt,
@@ -10381,6 +10873,7 @@ export {
10381
10873
  resolveExportSize,
10382
10874
  resolveOverlayBox,
10383
10875
  resolveOverlayStyle,
10876
+ resolvePins,
10384
10877
  resolveText3dAsset,
10385
10878
  resolveZoomStyle,
10386
10879
  restLeanOf,