@vosjs/studio-core 0.42.5 → 0.43.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
@@ -3791,7 +3791,7 @@ import {
3791
3791
  EASINGS,
3792
3792
  lerpArray as lerpArray2,
3793
3793
  sample as sample2,
3794
- segmentRate as segmentRate2,
3794
+ segmentRate as segmentRate3,
3795
3795
  sortKeyframes as sortKeyframes2,
3796
3796
  sourceToTimeline,
3797
3797
  splitBySpeed as splitBySpeed3,
@@ -4709,6 +4709,114 @@ function round42(v) {
4709
4709
  return Math.round(v * 1e5) / 1e5;
4710
4710
  }
4711
4711
 
4712
+ // src/lower/anchor.ts
4713
+ import { segmentRate } from "@vosjs/timeline";
4714
+ var ELAPSED_PLACEHOLDER = "{elapsed}";
4715
+ var EPS = 1e-9;
4716
+ function sourceMomentToOutput(rated, t, media) {
4717
+ if (!Number.isFinite(t)) return null;
4718
+ let acc = 0;
4719
+ let edge = null;
4720
+ for (const p of rated) {
4721
+ const rate = segmentRate(p);
4722
+ const len = Math.max(0, p.out - p.in) / rate;
4723
+ if (sameMedia(p.media, media)) {
4724
+ if (t >= p.in - EPS && t < p.out - EPS)
4725
+ return acc + Math.max(0, t - p.in) / rate;
4726
+ if (edge === null && Math.abs(t - p.out) <= 1e-6) edge = acc + len;
4727
+ }
4728
+ acc += len;
4729
+ }
4730
+ return edge;
4731
+ }
4732
+ var isNum = (v) => typeof v === "number" && Number.isFinite(v);
4733
+ function stepElapsedMs(meta, step) {
4734
+ const skips = Array.isArray(meta.skips) ? meta.skips : [];
4735
+ for (const raw of skips) {
4736
+ if (!raw || typeof raw !== "object") continue;
4737
+ const s = raw;
4738
+ if (!isNum(s.ms) || s.ms < 0) continue;
4739
+ const named = step.id !== void 0 && (s.id === step.id || s.step === step.id) || s.step === step.step;
4740
+ if (named) return s.ms;
4741
+ }
4742
+ const own = step.offCameraMs;
4743
+ return isNum(own) && own >= 0 ? own : null;
4744
+ }
4745
+ function elapsedFor(meta, step) {
4746
+ const own = stepElapsedMs(meta, step);
4747
+ if (own !== null) return { ms: own };
4748
+ const earlier = (meta.steps ?? []).filter((s) => s !== step && s.step < step.step).sort((a, b) => b.step - a.step);
4749
+ for (const s of earlier) {
4750
+ const ms = stepElapsedMs(meta, s);
4751
+ if (ms !== null) return { ms, from: s };
4752
+ }
4753
+ return null;
4754
+ }
4755
+ function resolveAnchor(doc, anchor, rated) {
4756
+ const source = mediaSource(doc, anchor.media);
4757
+ if (!source) return { start: null, miss: "media" };
4758
+ const meta = source.meta;
4759
+ const step = findStep(meta.steps ?? [], anchor.step);
4760
+ if (!step) return { start: null, miss: "step" };
4761
+ const elapsed = elapsedFor(meta, step);
4762
+ const extra = elapsed ? {
4763
+ elapsedMs: elapsed.ms,
4764
+ ...elapsed.from ? { elapsedFrom: elapsed.from } : {}
4765
+ } : {};
4766
+ if (step.skipped) return { start: null, step, miss: "skipped", ...extra };
4767
+ const moment = anchor.at === "end" ? step.tEnd : step.tStart;
4768
+ const out = sourceMomentToOutput(rated, moment, anchor.media);
4769
+ if (out === null) return { start: null, step, miss: "cut", ...extra };
4770
+ const offset = isNum(anchor.offset) ? anchor.offset : 0;
4771
+ return { start: round33(Math.max(0, out + offset)), step, ...extra };
4772
+ }
4773
+ var round33 = (v) => Math.round(v * 1e3) / 1e3;
4774
+ function formatElapsed(ms) {
4775
+ const s = Math.max(0, ms) / 1e3;
4776
+ const plural = (n, unit) => `${n} ${unit}${n === 1 ? "" : "s"}`;
4777
+ if (s < 9.5) return plural(Math.max(1, Math.round(s)), "second");
4778
+ if (s < 57.5) return plural(Math.round(s / 5) * 5, "second");
4779
+ const m = s / 60;
4780
+ if (m < 59.5) return plural(Math.max(1, Math.round(m)), "minute");
4781
+ const h = m / 60;
4782
+ if (h < 47.5) return plural(Math.max(1, Math.round(h)), "hour");
4783
+ return plural(Math.round(h / 24), "day");
4784
+ }
4785
+ function hasElapsed(text) {
4786
+ if (typeof text === "string") return text.includes(ELAPSED_PLACEHOLDER);
4787
+ return Array.isArray(text) && text.some(
4788
+ (r) => typeof r?.text === "string" && r.text.includes(ELAPSED_PLACEHOLDER)
4789
+ );
4790
+ }
4791
+ function fillElapsed(text, words) {
4792
+ const fill = (s) => s.split(ELAPSED_PLACEHOLDER).join(words);
4793
+ if (typeof text === "string") return fill(text);
4794
+ return text.map(
4795
+ (r) => typeof r.text === "string" && r.text.includes(ELAPSED_PLACEHOLDER) ? { ...r, text: fill(r.text) } : r
4796
+ );
4797
+ }
4798
+ function anchoredClip(clip, doc, rated) {
4799
+ if (!clip.anchor || typeof clip.anchor !== "object") return clip;
4800
+ const r = resolveAnchor(doc, clip.anchor, rated);
4801
+ let next = clip;
4802
+ if (r.start !== null && r.start !== clip.start)
4803
+ next = { ...next, start: r.start };
4804
+ if (next.kind === "text" && r.elapsedMs !== void 0 && hasElapsed(next.text)) {
4805
+ next = { ...next, text: fillElapsed(next.text, formatElapsed(r.elapsedMs)) };
4806
+ }
4807
+ return next;
4808
+ }
4809
+ function resolveOverlayAnchors(doc, rated) {
4810
+ const overlays = Array.isArray(doc.overlays) ? doc.overlays : [];
4811
+ if (!overlays.some((o) => o && o.anchor)) return doc;
4812
+ return {
4813
+ ...doc,
4814
+ overlays: overlays.map(
4815
+ (o) => o && o.anchor ? anchoredClip(o, doc, rated) : o
4816
+ )
4817
+ };
4818
+ }
4819
+
4712
4820
  // src/lower/cursorIdle.ts
4713
4821
  var CURSOR_IDLE_HOLD = 1;
4714
4822
  var CURSOR_IDLE_FADE_OUT = 0.35;
@@ -6264,7 +6372,7 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
6264
6372
  }`;
6265
6373
 
6266
6374
  // src/lower/extractClicks.ts
6267
- import { segmentRate } from "@vosjs/timeline";
6375
+ import { segmentRate as segmentRate2 } from "@vosjs/timeline";
6268
6376
  var CLICK_FX_PRE = 0.06;
6269
6377
  var CLICK_RIPPLE_DUR = 0.45;
6270
6378
  var CLICK_PULSE_DUR = 0.35;
@@ -6277,7 +6385,7 @@ function outputAt(segments, sourceT, media) {
6277
6385
  if (!segments.length) return sourceT;
6278
6386
  let acc = 0;
6279
6387
  for (const s of segments) {
6280
- const rate = segmentRate(s);
6388
+ const rate = segmentRate2(s);
6281
6389
  if (sameMedia(s.media, media) && sourceT >= s.in && sourceT <= s.out)
6282
6390
  return acc + (sourceT - s.in) / rate;
6283
6391
  acc += Math.max(0, s.out - s.in) / rate;
@@ -6288,7 +6396,7 @@ function keptOutputEnd(segments, sIn, sOut, media) {
6288
6396
  let acc = 0;
6289
6397
  let end = null;
6290
6398
  for (const p of segments) {
6291
- const rate = segmentRate(p);
6399
+ const rate = segmentRate2(p);
6292
6400
  if (sameMedia(p.media, media)) {
6293
6401
  const ovIn = Math.max(sIn, p.in);
6294
6402
  const ovOut = Math.min(sOut, p.out);
@@ -6401,7 +6509,7 @@ function spanOutputExtent(segments, sIn, sOut, media) {
6401
6509
  let start = null;
6402
6510
  let end = null;
6403
6511
  for (const p of segments) {
6404
- const rate = segmentRate2(p);
6512
+ const rate = segmentRate3(p);
6405
6513
  const len = Math.max(0, p.out - p.in) / rate;
6406
6514
  if (!sameMedia(p.media, media)) {
6407
6515
  acc += len;
@@ -8851,8 +8959,9 @@ function studioLayerData(input, duration, pins) {
8851
8959
  };
8852
8960
  }
8853
8961
  function lowerToComposition(input) {
8854
- const doc = migrateText(migrateMotion(input));
8855
- const rated = ratedSegments(doc);
8962
+ const read = migrateText(migrateMotion(input));
8963
+ const rated = ratedSegments(read);
8964
+ const doc = resolveOverlayAnchors(read, rated);
8856
8965
  const transitions = docTransitions(doc);
8857
8966
  const footageEnd = durationSec(doc, rated);
8858
8967
  const duration = outputEnd(doc, footageEnd);
@@ -9066,8 +9175,9 @@ function loweredZoomSpans(doc, rated, transitions, layout, camera, zoomStyle) {
9066
9175
  });
9067
9176
  }
9068
9177
  function docZoomTrack(input) {
9069
- const doc = migrateText(migrateMotion(input));
9070
- const rated = ratedSegments(doc);
9178
+ const read = migrateText(migrateMotion(input));
9179
+ const rated = ratedSegments(read);
9180
+ const doc = resolveOverlayAnchors(read, rated);
9071
9181
  const transitions = docTransitions(doc);
9072
9182
  const layout = docCardLayout(doc);
9073
9183
  const camera = cameraModel(doc.frame);
@@ -9795,14 +9905,16 @@ import { totalDuration as totalDuration5 } from "@vosjs/timeline";
9795
9905
 
9796
9906
  // src/audioBeds.ts
9797
9907
  import { totalDuration as totalDuration4 } from "@vosjs/timeline";
9798
- var round33 = (v) => Math.round(v * 1e3) / 1e3;
9908
+ var round34 = (v) => Math.round(v * 1e3) / 1e3;
9799
9909
  function docOutputDuration(doc) {
9800
- const footage = totalDuration4(ratedSegments(doc));
9801
- return isRecordingDoc(doc) ? outputEnd(migrateMotion(doc), footage) : footage;
9910
+ const rated = ratedSegments(doc);
9911
+ const footage = totalDuration4(rated);
9912
+ if (!isRecordingDoc(doc)) return footage;
9913
+ return outputEnd(resolveOverlayAnchors(migrateMotion(doc), rated), footage);
9802
9914
  }
9803
9915
  function musicBedClip(input) {
9804
- const track = round33(input.trackDuration);
9805
- const output = round33(input.outputDuration);
9916
+ const track = round34(input.trackDuration);
9917
+ const output = round34(input.outputDuration);
9806
9918
  const clip = {
9807
9919
  id: input.id,
9808
9920
  key: input.key,
@@ -9817,7 +9929,7 @@ function musicBedClip(input) {
9817
9929
  fadeIn: 0,
9818
9930
  // A bed that just stops reads as a glitch; clamp so a very short cut
9819
9931
  // still spends most of its time at full level.
9820
- fadeOut: output > 0 ? Math.min(1.5, round33(output / 4)) : 1.5,
9932
+ fadeOut: output > 0 ? Math.min(1.5, round34(output / 4)) : 1.5,
9821
9933
  duck: input.hasMic || void 0
9822
9934
  };
9823
9935
  if (output <= 0) return clip;
@@ -9836,28 +9948,28 @@ function isMusicBed(clip, outputDuration) {
9836
9948
  return Math.abs(placed - outputDuration) <= 1;
9837
9949
  }
9838
9950
  function refillAudioBeds(doc, prevDuration, nextDuration) {
9839
- const EPS2 = 0.05;
9840
- if (Math.abs(nextDuration - prevDuration) <= EPS2 || nextDuration <= 0) {
9951
+ const EPS3 = 0.05;
9952
+ if (Math.abs(nextDuration - prevDuration) <= EPS3 || nextDuration <= 0) {
9841
9953
  return false;
9842
9954
  }
9843
9955
  let changed = false;
9844
9956
  for (const clip of doc.audio) {
9845
9957
  if (clip.start >= nextDuration) continue;
9846
9958
  const placed = clip.loop ? Math.max(clip.out - clip.in, clip.loopLen ?? clip.out - clip.in) : clip.out - clip.in;
9847
- if (Math.abs(clip.start + placed - prevDuration) > EPS2) continue;
9959
+ if (Math.abs(clip.start + placed - prevDuration) > EPS3) continue;
9848
9960
  const span = clip.out - clip.in;
9849
- const nextLen = round33(nextDuration - clip.start);
9961
+ const nextLen = round34(nextDuration - clip.start);
9850
9962
  if (clip.loop) {
9851
9963
  if (nextLen >= span) {
9852
9964
  clip.loopLen = nextLen;
9853
9965
  } else {
9854
9966
  clip.loop = void 0;
9855
9967
  clip.loopLen = void 0;
9856
- clip.out = round33(clip.in + nextLen);
9968
+ clip.out = round34(clip.in + nextLen);
9857
9969
  }
9858
9970
  } else {
9859
9971
  if (clip.in + nextLen <= clip.duration) {
9860
- clip.out = round33(clip.in + nextLen);
9972
+ clip.out = round34(clip.in + nextLen);
9861
9973
  } else {
9862
9974
  clip.out = clip.duration;
9863
9975
  clip.loop = true;
@@ -10102,7 +10214,7 @@ function applyTemplate(template, take, opts) {
10102
10214
  const skipped = [];
10103
10215
  for (const clip of template.overlays ?? []) {
10104
10216
  const next = structuredClone(clip);
10105
- next.start = Math.max(0, round34(clip.start + shift));
10217
+ next.start = Math.max(0, round35(clip.start + shift));
10106
10218
  next.from = from;
10107
10219
  const mine = own.get(clip.id);
10108
10220
  if (next.kind === "text") {
@@ -10131,7 +10243,7 @@ function applyTemplate(template, take, opts) {
10131
10243
  for (const clip of template.objects ?? []) {
10132
10244
  const next = structuredClone(clip);
10133
10245
  if (next.span)
10134
- next.span.start = Math.max(0, round34(next.span.start + shift));
10246
+ next.span.start = Math.max(0, round35(next.span.start + shift));
10135
10247
  next.from = from;
10136
10248
  objects.push(next);
10137
10249
  }
@@ -10141,7 +10253,7 @@ function applyTemplate(template, take, opts) {
10141
10253
  );
10142
10254
  for (const clip of template.audio ?? []) {
10143
10255
  const next = structuredClone(clip);
10144
- next.start = Math.max(0, round34(clip.start + shift));
10256
+ next.start = Math.max(0, round35(clip.start + shift));
10145
10257
  next.from = from;
10146
10258
  audio.push(next);
10147
10259
  }
@@ -10162,7 +10274,7 @@ function applyTemplate(template, take, opts) {
10162
10274
  }
10163
10275
  return { doc, notes };
10164
10276
  }
10165
- var round34 = (v) => Math.round(v * 1e3) / 1e3;
10277
+ var round35 = (v) => Math.round(v * 1e3) / 1e3;
10166
10278
 
10167
10279
  // src/digest/geometry.ts
10168
10280
  var CROP_MIN_FRAC = 0.25;
@@ -10345,15 +10457,15 @@ function outputRangeToSource(doc, t0, t1) {
10345
10457
  srcOut: mapTime2(rated, Math.max(0, t1))
10346
10458
  };
10347
10459
  }
10348
- var EPS = 1e-6;
10460
+ var EPS2 = 1e-6;
10349
10461
  var MIN_REMAINDER = 0.25;
10350
10462
  var MIN_SEGMENT = 0.05;
10351
10463
  function setSpeedInRange(spans, srcIn, srcOut, rate) {
10352
- if (srcOut - srcIn < EPS) return [...spans];
10464
+ if (srcOut - srcIn < EPS2) return [...spans];
10353
10465
  const kept = [];
10354
10466
  const used = new Set(spans.map((s) => s.id));
10355
10467
  for (const s of spans) {
10356
- if (s.out <= srcIn + EPS || s.in >= srcOut - EPS) {
10468
+ if (s.out <= srcIn + EPS2 || s.in >= srcOut - EPS2) {
10357
10469
  kept.push(s);
10358
10470
  continue;
10359
10471
  }
@@ -10378,12 +10490,12 @@ function setSpeedInRange(spans, srcIn, srcOut, rate) {
10378
10490
  return kept.sort((a, b) => a.in - b.in);
10379
10491
  }
10380
10492
  function removeSpeedInRange(spans, srcIn, srcOut) {
10381
- return spans.filter((s) => s.out <= srcIn + EPS || s.in >= srcOut - EPS);
10493
+ return spans.filter((s) => s.out <= srcIn + EPS2 || s.in >= srcOut - EPS2);
10382
10494
  }
10383
10495
  function removeSourceRange(segments, srcIn, srcOut) {
10384
10496
  const next = [];
10385
10497
  for (const seg of segments) {
10386
- if (seg.out <= srcIn + EPS || seg.in >= srcOut - EPS) {
10498
+ if (seg.out <= srcIn + EPS2 || seg.in >= srcOut - EPS2) {
10387
10499
  next.push(seg);
10388
10500
  continue;
10389
10501
  }
@@ -10395,11 +10507,11 @@ function removeSourceRange(segments, srcIn, srcOut) {
10395
10507
  }
10396
10508
  function zoomSpanForRange(zoom, srcIn, srcOut) {
10397
10509
  let start = srcIn;
10398
- const covering = zoom.find((z) => z.in <= start + EPS && z.out > start + EPS);
10510
+ const covering = zoom.find((z) => z.in <= start + EPS2 && z.out > start + EPS2);
10399
10511
  if (covering) start = covering.out;
10400
10512
  let end = srcOut;
10401
10513
  for (const z of zoom) {
10402
- if (z.in >= start - EPS && z.in < end) end = z.in;
10514
+ if (z.in >= start - EPS2 && z.in < end) end = z.in;
10403
10515
  }
10404
10516
  if (end - start < ZOOM_SPAN_MIN) return null;
10405
10517
  const used = new Set(zoom.map((z) => z.id));
@@ -10808,7 +10920,7 @@ function applyLook(frame, look, size, video, placement) {
10808
10920
  import {
10809
10921
  mapTime as mapTime3,
10810
10922
  removeSegment,
10811
- segmentRate as segmentRate3,
10923
+ segmentRate as segmentRate4,
10812
10924
  sourceToTimeline as sourceToTimeline2,
10813
10925
  splitBySpeed as splitBySpeed4,
10814
10926
  totalDuration as totalDuration7,
@@ -11549,7 +11661,7 @@ var micLane = {
11549
11661
  var spanDefaultLen = (sourceDuration) => Math.max(1, sourceDuration * 0.05);
11550
11662
  function rateAt(rated, srcT) {
11551
11663
  for (const p of rated)
11552
- if (srcT >= p.in - 1e-9 && srcT < p.out + 1e-9) return segmentRate3(p);
11664
+ if (srcT >= p.in - 1e-9 && srcT < p.out + 1e-9) return segmentRate4(p);
11553
11665
  return 1;
11554
11666
  }
11555
11667
  function clampToKept(kept, newIn, len) {
@@ -11579,7 +11691,7 @@ var speedLane = {
11579
11691
  let start = null;
11580
11692
  let end = 0;
11581
11693
  for (const p of rated) {
11582
- const len = Math.max(0, p.out - p.in) / segmentRate3(p);
11694
+ const len = Math.max(0, p.out - p.in) / segmentRate4(p);
11583
11695
  if (p.rate === sp.rate && p.in >= sp.in - 1e-9 && p.out <= sp.out + 1e-9) {
11584
11696
  if (start === null) start = acc;
11585
11697
  end = acc + len;
@@ -12209,6 +12321,7 @@ export {
12209
12321
  DRAG_FIT_LEVEL,
12210
12322
  DRAG_MIN_S,
12211
12323
  DRAG_MIN_TRAVEL_FRAC,
12324
+ ELAPSED_PLACEHOLDER,
12212
12325
  END_CARD_FROM,
12213
12326
  END_CARD_RECEDE,
12214
12327
  END_CARD_SECONDS,
@@ -12319,6 +12432,7 @@ export {
12319
12432
  ambientDrift,
12320
12433
  anchorKindOf,
12321
12434
  anchorSourceDuration,
12435
+ anchoredClip,
12322
12436
  animStep,
12323
12437
  applyLook,
12324
12438
  applyTemplate,
@@ -12395,6 +12509,7 @@ export {
12395
12509
  dropTemplate,
12396
12510
  duckCurve,
12397
12511
  effectiveSegments,
12512
+ elapsedFor,
12398
12513
  endCardClips,
12399
12514
  enterKey,
12400
12515
  enterOf,
@@ -12414,9 +12529,11 @@ export {
12414
12529
  focusBounds,
12415
12530
  focusForViewportCentre,
12416
12531
  followFocusEvents,
12532
+ formatElapsed,
12417
12533
  frameGeometry,
12418
12534
  freezeLane,
12419
12535
  freezeOutputExtents,
12536
+ hasElapsed,
12420
12537
  hexToRgb,
12421
12538
  hexToRgbTriplet,
12422
12539
  houseLook,
@@ -12532,7 +12649,9 @@ export {
12532
12649
  removeSourceRange,
12533
12650
  removeSpeedInRange,
12534
12651
  replaceRange,
12652
+ resolveAnchor,
12535
12653
  resolveExportSize,
12654
+ resolveOverlayAnchors,
12536
12655
  resolveOverlayBox,
12537
12656
  resolveOverlayStyle,
12538
12657
  resolvePins,
@@ -12556,8 +12675,10 @@ export {
12556
12675
  smoothCursor,
12557
12676
  snapOffset,
12558
12677
  snapRunWeight,
12678
+ sourceMomentToOutput,
12559
12679
  spanOutputExtent,
12560
12680
  speedLane,
12681
+ stepElapsedMs,
12561
12682
  studioAudioPlan,
12562
12683
  studioEntry,
12563
12684
  studioLayerData,