@vosjs/studio-core 0.13.1 → 0.14.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.d.ts +89 -4
- package/dist/index.js +259 -75
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -511,6 +511,44 @@ function lastLayerEnd(doc) {
|
|
|
511
511
|
|
|
512
512
|
// src/lower/motion.ts
|
|
513
513
|
import { splitBySpeed, totalDuration } from "@vosjs/timeline";
|
|
514
|
+
|
|
515
|
+
// src/media.ts
|
|
516
|
+
var PRIMARY_MEDIA = "";
|
|
517
|
+
var mediaKey = (id) => id ?? PRIMARY_MEDIA;
|
|
518
|
+
var sameMedia = (a, b) => mediaKey(a) === mediaKey(b);
|
|
519
|
+
function docMediaList(doc) {
|
|
520
|
+
return [
|
|
521
|
+
{ id: PRIMARY_MEDIA, source: doc.source },
|
|
522
|
+
...(doc.media ?? []).map((m) => ({ id: m.id, source: m }))
|
|
523
|
+
];
|
|
524
|
+
}
|
|
525
|
+
function mediaSource(doc, id) {
|
|
526
|
+
const key = mediaKey(id);
|
|
527
|
+
if (key === PRIMARY_MEDIA) return doc.source;
|
|
528
|
+
return (doc.media ?? []).find((m) => m.id === key) ?? null;
|
|
529
|
+
}
|
|
530
|
+
function mediaDuration(source) {
|
|
531
|
+
return (source.meta.durationMs || 0) / 1e3;
|
|
532
|
+
}
|
|
533
|
+
function mediaAtOutput(rated, t) {
|
|
534
|
+
let acc = 0;
|
|
535
|
+
let last = null;
|
|
536
|
+
for (const p of rated) {
|
|
537
|
+
const len = Math.max(0, p.out - p.in) / (p.rate && p.rate > 0 ? p.rate : 1);
|
|
538
|
+
if (t < acc + len) return mediaKey(p.media);
|
|
539
|
+
acc += len;
|
|
540
|
+
last = p;
|
|
541
|
+
}
|
|
542
|
+
return last ? mediaKey(last.media) : PRIMARY_MEDIA;
|
|
543
|
+
}
|
|
544
|
+
function nextMediaId(doc) {
|
|
545
|
+
const taken = new Set((doc.media ?? []).map((m) => m.id));
|
|
546
|
+
let n = 1;
|
|
547
|
+
while (taken.has(`m${n}`)) n++;
|
|
548
|
+
return `m${n}`;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/lower/motion.ts
|
|
514
552
|
function docRestTime(doc) {
|
|
515
553
|
if (doc.endCard) return null;
|
|
516
554
|
const freezes = docFreezes(doc);
|
|
@@ -575,17 +613,19 @@ function exitSeconds(e) {
|
|
|
575
613
|
}
|
|
576
614
|
function placeFreezes(rated, freezes) {
|
|
577
615
|
const marks = /* @__PURE__ */ new Map();
|
|
578
|
-
const
|
|
616
|
+
const placed = /* @__PURE__ */ new Set();
|
|
579
617
|
const pieces = [];
|
|
580
618
|
let acc = 0;
|
|
581
|
-
const freezePiece = (f) => {
|
|
619
|
+
const freezePiece = (f, media) => {
|
|
582
620
|
const at = Math.max(HOLD_SOURCE_SPAN, f.at);
|
|
583
621
|
marks.set(f.id, { t: acc, duration: f.seconds });
|
|
584
622
|
pieces.push({
|
|
585
623
|
in: at - HOLD_SOURCE_SPAN,
|
|
586
624
|
out: at,
|
|
587
|
-
rate: HOLD_SOURCE_SPAN / f.seconds
|
|
625
|
+
rate: HOLD_SOURCE_SPAN / f.seconds,
|
|
626
|
+
...media !== void 0 ? { media } : {}
|
|
588
627
|
});
|
|
628
|
+
placed.add(f.id);
|
|
589
629
|
acc += f.seconds;
|
|
590
630
|
};
|
|
591
631
|
const push2 = (p) => {
|
|
@@ -595,32 +635,23 @@ function placeFreezes(rated, freezes) {
|
|
|
595
635
|
for (let i = 0; i < rated.length; i++) {
|
|
596
636
|
let piece = rated[i];
|
|
597
637
|
const next = rated[i + 1];
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
continue;
|
|
604
|
-
}
|
|
638
|
+
const continues = !!next && sameMedia(next.media, piece.media) && Math.abs(next.in - piece.out) < 1e-9 && next.out > piece.out;
|
|
639
|
+
const mine = freezes.filter(
|
|
640
|
+
(f) => f.seconds > 0 && !placed.has(f.id) && sameMedia(f.media, piece.media) && f.at >= piece.in - 1e-9 && (continues ? f.at < piece.out - 1e-9 : f.at <= piece.out + 1e-9)
|
|
641
|
+
).sort((a, b) => a.at - b.at);
|
|
642
|
+
for (const f of mine) {
|
|
605
643
|
if (f.at <= piece.in + 1e-9) {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
continue;
|
|
609
|
-
}
|
|
610
|
-
if (f.at < piece.out - 1e-9) {
|
|
644
|
+
freezePiece(f, piece.media);
|
|
645
|
+
} else if (f.at < piece.out - 1e-9) {
|
|
611
646
|
push2({ ...piece, out: f.at });
|
|
612
|
-
|
|
613
|
-
freezePiece(f);
|
|
647
|
+
freezePiece(f, piece.media);
|
|
614
648
|
piece = { ...piece, in: f.at };
|
|
615
|
-
|
|
649
|
+
} else {
|
|
650
|
+
const media = piece.media;
|
|
651
|
+
push2(piece);
|
|
652
|
+
piece = null;
|
|
653
|
+
freezePiece(f, media);
|
|
616
654
|
}
|
|
617
|
-
const continues = next && Math.abs(next.in - piece.out) < 1e-9 && next.out > piece.out;
|
|
618
|
-
if (continues) break;
|
|
619
|
-
push2(piece);
|
|
620
|
-
piece = null;
|
|
621
|
-
pending.shift();
|
|
622
|
-
freezePiece(f);
|
|
623
|
-
break;
|
|
624
655
|
}
|
|
625
656
|
if (piece) push2(piece);
|
|
626
657
|
}
|
|
@@ -1570,7 +1601,7 @@ import {
|
|
|
1570
1601
|
sample,
|
|
1571
1602
|
segmentRate as segmentRate2,
|
|
1572
1603
|
sortKeyframes as sortKeyframes2,
|
|
1573
|
-
sourceToTimeline
|
|
1604
|
+
sourceToTimeline,
|
|
1574
1605
|
splitBySpeed as splitBySpeed2,
|
|
1575
1606
|
totalDuration as totalDuration2
|
|
1576
1607
|
} from "@vosjs/timeline";
|
|
@@ -3242,7 +3273,7 @@ var STUDIO_FRAME = `(ctx, content, dt) => {
|
|
|
3242
3273
|
}`;
|
|
3243
3274
|
|
|
3244
3275
|
// src/lower/extractClicks.ts
|
|
3245
|
-
import { segmentRate
|
|
3276
|
+
import { segmentRate } from "@vosjs/timeline";
|
|
3246
3277
|
var CLICK_FX_PRE = 0.06;
|
|
3247
3278
|
var CLICK_RIPPLE_DUR = 0.45;
|
|
3248
3279
|
var CLICK_PULSE_DUR = 0.35;
|
|
@@ -3251,14 +3282,27 @@ var CLICK_SYNTH_RELEASE = 0.12;
|
|
|
3251
3282
|
var CLICK_PAIR_MAX = 10;
|
|
3252
3283
|
var CLICK_RECT_MAX_FRAC = 0.35;
|
|
3253
3284
|
var RECT_CONTAIN_MARGIN = 8;
|
|
3254
|
-
function
|
|
3285
|
+
function outputAt(segments, sourceT, media) {
|
|
3286
|
+
if (!segments.length) return sourceT;
|
|
3287
|
+
let acc = 0;
|
|
3288
|
+
for (const s of segments) {
|
|
3289
|
+
const rate = segmentRate(s);
|
|
3290
|
+
if (sameMedia(s.media, media) && sourceT >= s.in && sourceT <= s.out)
|
|
3291
|
+
return acc + (sourceT - s.in) / rate;
|
|
3292
|
+
acc += Math.max(0, s.out - s.in) / rate;
|
|
3293
|
+
}
|
|
3294
|
+
return null;
|
|
3295
|
+
}
|
|
3296
|
+
function keptOutputEnd(segments, sIn, sOut, media) {
|
|
3255
3297
|
let acc = 0;
|
|
3256
3298
|
let end = null;
|
|
3257
3299
|
for (const p of segments) {
|
|
3258
3300
|
const rate = segmentRate(p);
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3301
|
+
if (sameMedia(p.media, media)) {
|
|
3302
|
+
const ovIn = Math.max(sIn, p.in);
|
|
3303
|
+
const ovOut = Math.min(sOut, p.out);
|
|
3304
|
+
if (ovOut > ovIn) end = acc + (ovOut - p.in) / rate;
|
|
3305
|
+
}
|
|
3262
3306
|
acc += Math.max(0, p.out - p.in) / rate;
|
|
3263
3307
|
}
|
|
3264
3308
|
return end;
|
|
@@ -3290,10 +3334,10 @@ function extractClicks(track, segments, opts) {
|
|
|
3290
3334
|
break;
|
|
3291
3335
|
}
|
|
3292
3336
|
}
|
|
3293
|
-
const ot =
|
|
3337
|
+
const ot = outputAt(segments, st, opts.media);
|
|
3294
3338
|
if (ot === null) continue;
|
|
3295
3339
|
const upSrc = st + Math.max(pressLen, 0.02);
|
|
3296
|
-
const up = keptOutputEnd(segments, st, upSrc) ?? ot + CLICK_SYNTH_RELEASE;
|
|
3340
|
+
const up = keptOutputEnd(segments, st, upSrc, opts.media) ?? ot + CLICK_SYNTH_RELEASE;
|
|
3297
3341
|
const click = {
|
|
3298
3342
|
ot: round5(ot),
|
|
3299
3343
|
up: round5(Math.max(up, ot + 0.02)),
|
|
@@ -3346,19 +3390,32 @@ var PROGRAM_DURATION = 1;
|
|
|
3346
3390
|
function ratedSegments(doc) {
|
|
3347
3391
|
const segs = isRecordingDoc(doc) ? doc.segments.length ? doc.segments : [{ in: 0, out: doc.source.meta.durationMs / 1e3 }] : [{ in: 0, out: programDuration(doc) }];
|
|
3348
3392
|
const freezes = isRecordingDoc(doc) ? docFreezes(doc) : [];
|
|
3349
|
-
|
|
3393
|
+
const speeds = doc.speed ?? [];
|
|
3394
|
+
const pieces = segs.flatMap(
|
|
3395
|
+
(seg) => splitBySpeed2(
|
|
3396
|
+
[seg],
|
|
3397
|
+
speeds.filter(
|
|
3398
|
+
(sp) => sameMedia(sp.media, seg.media)
|
|
3399
|
+
)
|
|
3400
|
+
)
|
|
3401
|
+
);
|
|
3402
|
+
return withFreezes(pieces, freezes);
|
|
3350
3403
|
}
|
|
3351
3404
|
function durationSec(doc, rated) {
|
|
3352
3405
|
const trimmed = totalDuration2(rated);
|
|
3353
3406
|
return trimmed > 0 ? trimmed : doc.source.meta.durationMs / 1e3;
|
|
3354
3407
|
}
|
|
3355
|
-
function spanOutputExtent(segments, sIn, sOut) {
|
|
3408
|
+
function spanOutputExtent(segments, sIn, sOut, media) {
|
|
3356
3409
|
let acc = 0;
|
|
3357
3410
|
let start = null;
|
|
3358
3411
|
let end = null;
|
|
3359
3412
|
for (const p of segments) {
|
|
3360
3413
|
const rate = segmentRate2(p);
|
|
3361
3414
|
const len = Math.max(0, p.out - p.in) / rate;
|
|
3415
|
+
if (!sameMedia(p.media, media)) {
|
|
3416
|
+
acc += len;
|
|
3417
|
+
continue;
|
|
3418
|
+
}
|
|
3362
3419
|
const ovIn = Math.max(sIn, p.in);
|
|
3363
3420
|
const ovOut = Math.min(sOut, p.out);
|
|
3364
3421
|
if (ovOut > ovIn) {
|
|
@@ -3389,7 +3446,7 @@ function trackEmitter() {
|
|
|
3389
3446
|
function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE]) {
|
|
3390
3447
|
const panEase = style.panEase;
|
|
3391
3448
|
const mapped = [...zoom].sort((a, b) => a.in - b.in).flatMap((z) => {
|
|
3392
|
-
const ext = spanOutputExtent(segments, z.in, z.out);
|
|
3449
|
+
const ext = spanOutputExtent(segments, z.in, z.out, z.media);
|
|
3393
3450
|
return ext ? [{ z, tIn: ext.start, tOut: ext.end }] : [];
|
|
3394
3451
|
});
|
|
3395
3452
|
const { keyframes, push: push2 } = trackEmitter();
|
|
@@ -3408,7 +3465,7 @@ function zoomTrackFromDoc(zoom, segments, style = ZOOM_STYLES[DEFAULT_ZOOM_STYLE
|
|
|
3408
3465
|
push2(start + style.rampIn * m, entry, spanEase(z.ease, style.ease));
|
|
3409
3466
|
}
|
|
3410
3467
|
for (const e of z.followEvents ?? []) {
|
|
3411
|
-
const eOut =
|
|
3468
|
+
const eOut = sourceToTimeline(segments, e.t);
|
|
3412
3469
|
if (eOut === null || eOut <= tIn || eOut >= tOut) continue;
|
|
3413
3470
|
const next2 = [cur[0], e.cx, e.cy];
|
|
3414
3471
|
push2(eOut, cur, "none");
|
|
@@ -3451,7 +3508,7 @@ function tiltTrackFromDoc(tilt, segments, motion = {}) {
|
|
|
3451
3508
|
const panEase = TILT_PAN_EASE;
|
|
3452
3509
|
const rest = [0, 0];
|
|
3453
3510
|
const mapped = [...tilt].sort((a, b) => a.in - b.in).flatMap((z) => {
|
|
3454
|
-
const ext = spanOutputExtent(segments, z.in, z.out);
|
|
3511
|
+
const ext = spanOutputExtent(segments, z.in, z.out, z.media);
|
|
3455
3512
|
return ext ? [{ z, tIn: ext.start, tOut: ext.end }] : [];
|
|
3456
3513
|
});
|
|
3457
3514
|
const { keyframes, push: push2 } = trackEmitter();
|
|
@@ -3494,7 +3551,7 @@ function camTrackFromDoc(cam, spans, segments, W, H = 1080) {
|
|
|
3494
3551
|
];
|
|
3495
3552
|
const panEase = CAM_PAN_EASE;
|
|
3496
3553
|
const mapped = [...spans].sort((a, b) => a.in - b.in).flatMap((z) => {
|
|
3497
|
-
const ext = spanOutputExtent(segments, z.in, z.out);
|
|
3554
|
+
const ext = spanOutputExtent(segments, z.in, z.out, z.media);
|
|
3498
3555
|
return ext ? [{ z, tIn: ext.start, tOut: ext.end }] : [];
|
|
3499
3556
|
});
|
|
3500
3557
|
const { keyframes, push: push2 } = trackEmitter();
|
|
@@ -3870,6 +3927,26 @@ var SETUP = `async (ctx) => {
|
|
|
3870
3927
|
// the same sync as the screen video (source-anchored, so element time == the
|
|
3871
3928
|
// video's). Unmuted \u2014 syncVid's autoplay net re-mutes on policy rejection.
|
|
3872
3929
|
const mic = ctx.data.micSrc ? await load(ctx.data.micSrc, false) : null
|
|
3930
|
+
// The take's OTHER media (concat): one element each, decoded the way the
|
|
3931
|
+
// primary is; ON_FRAME drives the one under the playhead and pauses the
|
|
3932
|
+
// rest. Absent = one media, byte-identical.
|
|
3933
|
+
const mediaEls = {}
|
|
3934
|
+
for (const m of ctx.data.media || []) {
|
|
3935
|
+
try {
|
|
3936
|
+
const el = m.isImage ? await loadImage(m.src) : await load(m.src, !m.hasAudio)
|
|
3937
|
+
if (!m.isImage && ctx.data.videoDecodeMode === 'webcodecs') {
|
|
3938
|
+
try {
|
|
3939
|
+
const wp = await makeWcProvider(m.src)
|
|
3940
|
+
if (wp && (!el.videoWidth || (wp.width === el.videoWidth && wp.height === el.videoHeight))) el.__voilaWc = wp
|
|
3941
|
+
} catch (e) {
|
|
3942
|
+
console.warn('[voila] webcodecs provider unavailable for a media, seeks stay html5', e)
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
mediaEls[m.id] = el
|
|
3946
|
+
} catch (e) {
|
|
3947
|
+
console.warn('[voila] a media failed to load', m.id, e)
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3873
3950
|
// Background media (frame.backgroundMedia): warm-load so the first paint is
|
|
3874
3951
|
// complete. FAIL-OPEN \u2014 a bad key degrades to the CSS fill underneath, never
|
|
3875
3952
|
// a dead LOAD (unlike the recording, which is the comp's reason to exist).
|
|
@@ -3898,9 +3975,27 @@ var SETUP = `async (ctx) => {
|
|
|
3898
3975
|
const d = ctx.data
|
|
3899
3976
|
const TL = globalThis.__vosTimeline
|
|
3900
3977
|
const srcT = TL.mapTime(d.segments || [], t)
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3978
|
+
// The media under the playhead (concat): step its element; the
|
|
3979
|
+
// primary's sidecars belong to the primary's moments only.
|
|
3980
|
+
let am = ''
|
|
3981
|
+
{
|
|
3982
|
+
let acc = 0
|
|
3983
|
+
const sg = d.segments || []
|
|
3984
|
+
for (let i = 0; i < sg.length; i++) {
|
|
3985
|
+
const s0 = sg[i]
|
|
3986
|
+
const ln = Math.max(0, s0.out - s0.in) / (s0.rate || 1)
|
|
3987
|
+
if (t < acc + ln || i === sg.length - 1) { am = s0.media || ''; break }
|
|
3988
|
+
acc += ln
|
|
3989
|
+
}
|
|
3990
|
+
}
|
|
3991
|
+
const amEl = am ? mediaEls[am] : null
|
|
3992
|
+
if (amEl) {
|
|
3993
|
+
if (amEl.play) stepPaused(amEl, srcT)
|
|
3994
|
+
} else {
|
|
3995
|
+
if (video.play) stepPaused(video, srcT)
|
|
3996
|
+
if (cam) stepPaused(cam, srcT)
|
|
3997
|
+
if (mic) stepPaused(mic, srcT)
|
|
3998
|
+
}
|
|
3904
3999
|
const bg2 = d.frame && d.frame.backgroundMedia
|
|
3905
4000
|
if (bg2 && bg2.key && bg2.kind !== 'image') {
|
|
3906
4001
|
const bgEl2 = cache.get(bg2.key)
|
|
@@ -3910,7 +4005,7 @@ var SETUP = `async (ctx) => {
|
|
|
3910
4005
|
}
|
|
3911
4006
|
}
|
|
3912
4007
|
})
|
|
3913
|
-
return { video: video, cam: cam, mic: mic }
|
|
4008
|
+
return { video: video, cam: cam, mic: mic, media: mediaEls }
|
|
3914
4009
|
}`;
|
|
3915
4010
|
var CREATE_CONTENT = `(ctx, setupData) => {
|
|
3916
4011
|
const THREE = ctx.THREE
|
|
@@ -3961,6 +4056,7 @@ var CREATE_CONTENT = `(ctx, setupData) => {
|
|
|
3961
4056
|
// { c2d, canvas, texture }) drive the card and let bg/overlay fall back.
|
|
3962
4057
|
canvas: card.canvas, c2d: card.c2d, texture: card.texture,
|
|
3963
4058
|
video: setupData.video, cam: setupData.cam, mic: setupData.mic,
|
|
4059
|
+
media: setupData.media,
|
|
3964
4060
|
},
|
|
3965
4061
|
}
|
|
3966
4062
|
}`;
|
|
@@ -4071,6 +4167,26 @@ var ON_FRAME = `(ctx, content, dt) => {
|
|
|
4071
4167
|
// Output-timeline seconds (engine-fed master clock) \u2192 source seconds on screen.
|
|
4072
4168
|
var t = ctx.time || 0
|
|
4073
4169
|
var srcT = TL.mapTime(d.segments || [], t)
|
|
4170
|
+
// The media under the playhead (concat): its element, crop, cursor and
|
|
4171
|
+
// space stand in for the primary's below, and every other media's
|
|
4172
|
+
// element rests. An absent media list is the primary alone.
|
|
4173
|
+
var amId = ''
|
|
4174
|
+
{ var amAcc = 0, amSg = d.segments || []
|
|
4175
|
+
for (var amI = 0; amI < amSg.length; amI++) {
|
|
4176
|
+
var amS = amSg[amI], amLn = Math.max(0, amS.out - amS.in) / (amS.rate || 1)
|
|
4177
|
+
if (t < amAcc + amLn || amI === amSg.length - 1) { amId = amS.media || ''; break }
|
|
4178
|
+
amAcc += amLn
|
|
4179
|
+
} }
|
|
4180
|
+
var actM = null
|
|
4181
|
+
if (amId && d.media) for (var amJ = 0; amJ < d.media.length; amJ++) if (d.media[amJ].id === amId) actM = d.media[amJ]
|
|
4182
|
+
if (actM && r.media && r.media[amId]) video = r.media[amId]
|
|
4183
|
+
else amId = ''
|
|
4184
|
+
if (r.media) for (var amK in r.media) { var amE = r.media[amK]; if (amK !== amId && amE && amE.pause && amE.paused === false) amE.pause() }
|
|
4185
|
+
if (amId) {
|
|
4186
|
+
if (r.video && r.video.pause && r.video.paused === false) r.video.pause()
|
|
4187
|
+
if (r.cam && r.cam.pause && r.cam.paused === false) r.cam.pause()
|
|
4188
|
+
if (r.mic && r.mic.pause && r.mic.paused === false) r.mic.pause()
|
|
4189
|
+
}
|
|
4074
4190
|
var s = H / 1080 // scale design-px controls to comp px
|
|
4075
4191
|
|
|
4076
4192
|
// Play natively while playing (smooth); seek precisely otherwise (paused, scrubbing,
|
|
@@ -4152,8 +4268,8 @@ var ON_FRAME = `(ctx, content, dt) => {
|
|
|
4152
4268
|
} catch (e) {}
|
|
4153
4269
|
}
|
|
4154
4270
|
if (video.play) syncVid(video) // stills (HTMLImageElement) have nothing to sync
|
|
4155
|
-
if (r.cam) syncVid(r.cam)
|
|
4156
|
-
if (r.mic) syncVid(r.mic)
|
|
4271
|
+
if (r.cam && !amId) syncVid(r.cam)
|
|
4272
|
+
if (r.mic && !amId) syncVid(r.mic)
|
|
4157
4273
|
// Gain routing (live via SET_DATA). With a mic sidecar (AT split) the
|
|
4158
4274
|
// recording <video> carries SYSTEM audio \u2014 its volume is the system fader \u2014
|
|
4159
4275
|
// and the sidecar element is the voice (micGain). Legacy takes have one
|
|
@@ -4379,7 +4495,7 @@ var ON_FRAME = `(ctx, content, dt) => {
|
|
|
4379
4495
|
// Window takes carry a viewport crop (drawImage source rect, capture px) that
|
|
4380
4496
|
// removes the real browser chrome \u2014 the card's source dims are then the CROP
|
|
4381
4497
|
// dims (meta/cursor were rewritten into crop space at doc build).
|
|
4382
|
-
var crp = d.crop || null
|
|
4498
|
+
var crp = actM ? (actM.crop || null) : (d.crop || null)
|
|
4383
4499
|
var vw = crp ? crp.w : (video.videoWidth || video.naturalWidth || 16)
|
|
4384
4500
|
var vh = crp ? crp.h : (video.videoHeight || video.naturalHeight || 9)
|
|
4385
4501
|
// Card-chrome scale: everything that belongs to the CARD (browser bar +
|
|
@@ -4598,7 +4714,7 @@ var ON_FRAME = `(ctx, content, dt) => {
|
|
|
4598
4714
|
// card. Contain never needs it (the video rect IS the card).
|
|
4599
4715
|
if (fitCover) { c.save(); rr(cardX, cardY, cardW, cardH, radius); c.clip() }
|
|
4600
4716
|
// cursor coordinate space + drawn radius (shared by click effects + the dot)
|
|
4601
|
-
var space = d.cursorSpace || { w: vw, h: vh }
|
|
4717
|
+
var space = (actM ? actM.cursorSpace : d.cursorSpace) || { w: vw, h: vh }
|
|
4602
4718
|
var curSize = ((d.cursorStyle && d.cursorStyle.size) || 24) * s2 * 0.5
|
|
4603
4719
|
|
|
4604
4720
|
// click effects \u2014 pure f(t): d.clicks are
|
|
@@ -4630,8 +4746,9 @@ var ON_FRAME = `(ctx, content, dt) => {
|
|
|
4630
4746
|
// click's source moment, or an effect near a cut would keep painting
|
|
4631
4747
|
// over the NEXT segment's unrelated footage
|
|
4632
4748
|
if (Math.abs(srcT - ck.st) > 2) continue
|
|
4633
|
-
var
|
|
4634
|
-
var
|
|
4749
|
+
var ckSp = ck.sp || space
|
|
4750
|
+
var ckAx = dx + (ck.x / (ckSp.w || vw)) * dw
|
|
4751
|
+
var ckAy = dy + (ck.y / (ckSp.h || vh)) * dh
|
|
4635
4752
|
// press dip: smoothstep down around the real mousedown (80 ms lead \u2014
|
|
4636
4753
|
// anticipation is what makes effects feel synced), hold through the real
|
|
4637
4754
|
// down\u2192up span (drags dip long), easeOutBack rebound with a slight
|
|
@@ -4744,7 +4861,7 @@ var ON_FRAME = `(ctx, content, dt) => {
|
|
|
4744
4861
|
// The dot is the only thing cursorStyle.visible hides \u2014 the track still drives
|
|
4745
4862
|
// cursor-follow zoom, and click effects draw above on their own switch.
|
|
4746
4863
|
// Undefined reads as visible so pre-toggle docs are unchanged.
|
|
4747
|
-
var cur = d.cursor || []
|
|
4864
|
+
var cur = (actM ? actM.cursor : d.cursor) || []
|
|
4748
4865
|
if (cur.length && !(d.cursorStyle && d.cursorStyle.visible === false)) {
|
|
4749
4866
|
var px = cur[0].x, py = cur[0].y
|
|
4750
4867
|
for (var j = 0; j < cur.length; j++) { if (cur[j].t <= srcT) { px = cur[j].x; py = cur[j].y } }
|
|
@@ -4753,7 +4870,7 @@ var ON_FRAME = `(ctx, content, dt) => {
|
|
|
4753
4870
|
// Idle fade: a sparse SOURCE-time opacity curve baked by cursorIdleFade.
|
|
4754
4871
|
// Linear between keys \u2014 opacity needs no easing, and the ramps are already
|
|
4755
4872
|
// shaped by where the keys sit. Absent/empty = the cursor never dwells.
|
|
4756
|
-
var cuA = 1, cuK = d.cursorFade
|
|
4873
|
+
var cuA = 1, cuK = actM ? actM.cursorFade : d.cursorFade
|
|
4757
4874
|
if (cuK && cuK.length) {
|
|
4758
4875
|
if (srcT <= cuK[0].t) cuA = cuK[0].a
|
|
4759
4876
|
else if (srcT >= cuK[cuK.length - 1].t) cuA = cuK[cuK.length - 1].a
|
|
@@ -4786,7 +4903,7 @@ var ON_FRAME = `(ctx, content, dt) => {
|
|
|
4786
4903
|
// runtime; the card c2d under stubs). Redraw + re-upload only while the
|
|
4787
4904
|
// bubble is active (a video \u2192 every frame), on resize, or once when it turns
|
|
4788
4905
|
// off (to clear) \u2014 so a cam-less take never uploads the overlay after frame 1.
|
|
4789
|
-
var camV = r.cam, camS = d.cam || {}
|
|
4906
|
+
var camV = amId ? null : r.cam, camS = d.cam || {}
|
|
4790
4907
|
var camOn = !camS.window || (srcT >= camS.window.in && srcT <= camS.window.out)
|
|
4791
4908
|
// Readiness is STICKY: readyState drops to HAVE_METADATA while a seek is in
|
|
4792
4909
|
// flight, so gating each frame on it makes the bubble vanish on every scrub
|
|
@@ -5128,7 +5245,8 @@ function lowerToComposition(input) {
|
|
|
5128
5245
|
segments: rated.map((seg) => ({
|
|
5129
5246
|
in: round6(seg.in),
|
|
5130
5247
|
out: round6(seg.out),
|
|
5131
|
-
...seg.rate !== void 0 && seg.rate !== 1 ? { rate: round6(seg.rate) } : {}
|
|
5248
|
+
...seg.rate !== void 0 && seg.rate !== 1 ? { rate: round6(seg.rate) } : {},
|
|
5249
|
+
...seg.media ? { media: seg.media } : {}
|
|
5132
5250
|
})),
|
|
5133
5251
|
frame: doc.frame,
|
|
5134
5252
|
micGain: doc.micGain ?? 1,
|
|
@@ -5139,6 +5257,34 @@ function lowerToComposition(input) {
|
|
|
5139
5257
|
})),
|
|
5140
5258
|
cursorStyle: doc.cursor,
|
|
5141
5259
|
cursorSpace: { w: doc.source.meta.width, h: doc.source.meta.height },
|
|
5260
|
+
// The other media (concat): each with its element's source, its crop
|
|
5261
|
+
// and its own cursor in its own capture space; absent = one media.
|
|
5262
|
+
...doc.media && doc.media.length ? {
|
|
5263
|
+
media: doc.media.map((m) => {
|
|
5264
|
+
const sm = smoothCursor(m.cursor, {
|
|
5265
|
+
factor: doc.cursor.smoothing,
|
|
5266
|
+
clickSnap: fx.style !== "none" || fx.press
|
|
5267
|
+
});
|
|
5268
|
+
const fade = doc.cursor.hideWhenIdle !== false && doc.cursor.visible !== false ? cursorIdleFade(m.cursor, {
|
|
5269
|
+
space: { w: m.meta.width, h: m.meta.height },
|
|
5270
|
+
sourceDuration: (m.meta.durationMs || 0) / 1e3
|
|
5271
|
+
}) : [];
|
|
5272
|
+
return {
|
|
5273
|
+
id: m.id,
|
|
5274
|
+
src: m.videoKey,
|
|
5275
|
+
isImage: m.sourceKind === "image",
|
|
5276
|
+
crop: m.crop ?? null,
|
|
5277
|
+
hasAudio: !!m.meta.hasAudio,
|
|
5278
|
+
cursor: sm.map((p) => ({
|
|
5279
|
+
t: round6(p.t),
|
|
5280
|
+
x: round6(p.x),
|
|
5281
|
+
y: round6(p.y)
|
|
5282
|
+
})),
|
|
5283
|
+
cursorSpace: { w: m.meta.width, h: m.meta.height },
|
|
5284
|
+
...fade.length ? { cursorFade: fade } : {}
|
|
5285
|
+
};
|
|
5286
|
+
})
|
|
5287
|
+
} : {},
|
|
5142
5288
|
...cursorFade.length ? { cursorFade } : {},
|
|
5143
5289
|
// Click effects: OUTPUT-anchored click records + resolved styling (named
|
|
5144
5290
|
// intensities/colors become numbers HERE — ON_FRAME reads no registry).
|
|
@@ -5207,10 +5353,24 @@ function clickFxData(doc, rated) {
|
|
|
5207
5353
|
const meta = doc.source.meta;
|
|
5208
5354
|
const level = CLICK_FX_INTENSITY[fx.intensity];
|
|
5209
5355
|
return {
|
|
5210
|
-
clicks: on ?
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5356
|
+
clicks: on ? [
|
|
5357
|
+
...extractClicks(doc.source.cursor, rated, {
|
|
5358
|
+
rects: fx.style === "highlight",
|
|
5359
|
+
space: { w: meta.width, h: meta.height }
|
|
5360
|
+
}),
|
|
5361
|
+
// The other media's clicks map through their own pieces and carry
|
|
5362
|
+
// their own capture space, so the ring lands where the press was.
|
|
5363
|
+
...(doc.media ?? []).flatMap(
|
|
5364
|
+
(m) => extractClicks(m.cursor, rated, {
|
|
5365
|
+
rects: fx.style === "highlight",
|
|
5366
|
+
space: { w: m.meta.width, h: m.meta.height },
|
|
5367
|
+
media: m.id
|
|
5368
|
+
}).map((ck) => ({
|
|
5369
|
+
...ck,
|
|
5370
|
+
sp: { w: m.meta.width, h: m.meta.height }
|
|
5371
|
+
}))
|
|
5372
|
+
)
|
|
5373
|
+
].sort((a, b) => a.ot - b.ot) : [],
|
|
5214
5374
|
clickFx: {
|
|
5215
5375
|
style: fx.style,
|
|
5216
5376
|
press: fx.press,
|
|
@@ -6835,7 +6995,7 @@ import {
|
|
|
6835
6995
|
mapTime as mapTime2,
|
|
6836
6996
|
removeSegment,
|
|
6837
6997
|
segmentRate as segmentRate3,
|
|
6838
|
-
sourceToTimeline as
|
|
6998
|
+
sourceToTimeline as sourceToTimeline2,
|
|
6839
6999
|
splitBySpeed as splitBySpeed3,
|
|
6840
7000
|
totalDuration as totalDuration6,
|
|
6841
7001
|
trimSegment
|
|
@@ -6920,7 +7080,7 @@ var videoLane = {
|
|
|
6920
7080
|
freezes.filter((f) => f.at < seg.out - 1e-9),
|
|
6921
7081
|
index === 0 || !contiguous(segments, index)
|
|
6922
7082
|
);
|
|
6923
|
-
const anchorOut =
|
|
7083
|
+
const anchorOut = sourceToTimeline2(fullMap, Math.min(edgeSrc, sourceDuration)) ?? edgeSrc;
|
|
6924
7084
|
const sourceT = mapTime2(fullMap, anchorOut + (g.t - edgeOutNow));
|
|
6925
7085
|
const next = trimSegment(
|
|
6926
7086
|
segments,
|
|
@@ -7050,10 +7210,16 @@ var freezeLane = {
|
|
|
7050
7210
|
const srcT = round11(mapTime2(rated, Math.max(0, g.t)));
|
|
7051
7211
|
if (!kept(srcT) || !clear(srcT)) return null;
|
|
7052
7212
|
const id = nextFreezeId(doc);
|
|
7213
|
+
const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
|
|
7053
7214
|
return (d) => {
|
|
7054
7215
|
d.freeze = [
|
|
7055
7216
|
...d.freeze ?? [],
|
|
7056
|
-
{
|
|
7217
|
+
{
|
|
7218
|
+
id,
|
|
7219
|
+
at: srcT,
|
|
7220
|
+
seconds: FREEZE_DEFAULT_SECONDS,
|
|
7221
|
+
...mediaHere ? { media: mediaHere } : {}
|
|
7222
|
+
}
|
|
7057
7223
|
].sort((a, b) => a.at - b.at);
|
|
7058
7224
|
};
|
|
7059
7225
|
}
|
|
@@ -7138,7 +7304,7 @@ var zoomLane = {
|
|
|
7138
7304
|
items(doc) {
|
|
7139
7305
|
const segments = ratedSegments(doc);
|
|
7140
7306
|
return doc.zoom.flatMap((z) => {
|
|
7141
|
-
const ext = spanOutputExtent(segments, z.in, z.out);
|
|
7307
|
+
const ext = spanOutputExtent(segments, z.in, z.out, z.media);
|
|
7142
7308
|
return ext === null ? [] : [
|
|
7143
7309
|
{
|
|
7144
7310
|
id: z.id,
|
|
@@ -7156,10 +7322,13 @@ var zoomLane = {
|
|
|
7156
7322
|
const sourceDuration = anchorSourceDuration(doc);
|
|
7157
7323
|
if (g.type === "create") {
|
|
7158
7324
|
const srcT = mapTime2(rated, Math.max(0, g.t));
|
|
7159
|
-
|
|
7160
|
-
const
|
|
7161
|
-
const
|
|
7162
|
-
|
|
7325
|
+
const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
|
|
7326
|
+
const here = spans.filter((z) => (z.media ?? "") === mediaHere);
|
|
7327
|
+
const mediaLen = mediaDuration(mediaSource(doc, mediaHere) ?? doc.source);
|
|
7328
|
+
if (here.some((z) => srcT >= z.in && srcT < z.out)) return null;
|
|
7329
|
+
const next = here.filter((z) => z.in > srcT).sort((a, b) => a.in - b.in).at(0);
|
|
7330
|
+
const limit = Math.min(mediaLen, next ? next.in : mediaLen);
|
|
7331
|
+
const len = Math.min(spanDefaultLen(mediaLen), limit - srcT);
|
|
7163
7332
|
if (len < ZOOM_SPAN_MIN * rateAt(rated, srcT)) return null;
|
|
7164
7333
|
const id = nextZoomId(doc);
|
|
7165
7334
|
const { cx, cy } = cursorFocusAt(doc, srcT);
|
|
@@ -7173,7 +7342,8 @@ var zoomLane = {
|
|
|
7173
7342
|
level: DEFAULT_ZOOM_LEVEL,
|
|
7174
7343
|
cx,
|
|
7175
7344
|
cy,
|
|
7176
|
-
source: "manual"
|
|
7345
|
+
source: "manual",
|
|
7346
|
+
...mediaHere ? { media: mediaHere } : {}
|
|
7177
7347
|
}
|
|
7178
7348
|
].sort((a, b) => a.in - b.in);
|
|
7179
7349
|
};
|
|
@@ -7263,7 +7433,7 @@ var tiltLane = {
|
|
|
7263
7433
|
items(doc) {
|
|
7264
7434
|
const segments = ratedSegments(doc);
|
|
7265
7435
|
return (doc.tilt ?? []).flatMap((z) => {
|
|
7266
|
-
const ext = spanOutputExtent(segments, z.in, z.out);
|
|
7436
|
+
const ext = spanOutputExtent(segments, z.in, z.out, z.media);
|
|
7267
7437
|
return ext === null ? [] : [
|
|
7268
7438
|
{
|
|
7269
7439
|
id: z.id,
|
|
@@ -7281,10 +7451,13 @@ var tiltLane = {
|
|
|
7281
7451
|
const sourceDuration = anchorSourceDuration(doc);
|
|
7282
7452
|
if (g.type === "create") {
|
|
7283
7453
|
const srcT = mapTime2(rated, Math.max(0, g.t));
|
|
7284
|
-
|
|
7285
|
-
const
|
|
7286
|
-
const
|
|
7287
|
-
|
|
7454
|
+
const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
|
|
7455
|
+
const here = spans.filter((z) => (z.media ?? "") === mediaHere);
|
|
7456
|
+
const mediaLen = mediaDuration(mediaSource(doc, mediaHere) ?? doc.source);
|
|
7457
|
+
if (here.some((z) => srcT >= z.in && srcT < z.out)) return null;
|
|
7458
|
+
const next = here.filter((z) => z.in > srcT).sort((a, b) => a.in - b.in).at(0);
|
|
7459
|
+
const limit = Math.min(mediaLen, next ? next.in : mediaLen);
|
|
7460
|
+
const len = Math.min(spanDefaultLen(mediaLen), limit - srcT);
|
|
7288
7461
|
if (len < TILT_SPAN_MIN * rateAt(rated, srcT)) return null;
|
|
7289
7462
|
const id = nextTiltId(doc);
|
|
7290
7463
|
return (d) => {
|
|
@@ -7296,7 +7469,8 @@ var tiltLane = {
|
|
|
7296
7469
|
out: round11(srcT + len),
|
|
7297
7470
|
rx: DEFAULT_TILT_POSE.rx,
|
|
7298
7471
|
ry: DEFAULT_TILT_POSE.ry,
|
|
7299
|
-
source: "manual"
|
|
7472
|
+
source: "manual",
|
|
7473
|
+
...mediaHere ? { media: mediaHere } : {}
|
|
7300
7474
|
}
|
|
7301
7475
|
].sort((a, b) => a.in - b.in);
|
|
7302
7476
|
};
|
|
@@ -7376,7 +7550,7 @@ var camMoveLane = {
|
|
|
7376
7550
|
if (!doc.source.camKey) return [];
|
|
7377
7551
|
const segments = ratedSegments(doc);
|
|
7378
7552
|
return (doc.camMotion ?? []).flatMap((z) => {
|
|
7379
|
-
const ext = spanOutputExtent(segments, z.in, z.out);
|
|
7553
|
+
const ext = spanOutputExtent(segments, z.in, z.out, z.media);
|
|
7380
7554
|
return ext === null ? [] : [
|
|
7381
7555
|
{
|
|
7382
7556
|
id: z.id,
|
|
@@ -7642,6 +7816,7 @@ var speedLane = {
|
|
|
7642
7816
|
const len = Math.min(spanDefaultLen(sourceDuration), limit - srcT);
|
|
7643
7817
|
if (len < SPEED_SPAN_MIN * DEFAULT_SPEED_RATE) return null;
|
|
7644
7818
|
const id = nextSpeedId(doc);
|
|
7819
|
+
const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
|
|
7645
7820
|
return (d) => {
|
|
7646
7821
|
d.speed = [
|
|
7647
7822
|
...d.speed ?? [],
|
|
@@ -7650,7 +7825,8 @@ var speedLane = {
|
|
|
7650
7825
|
in: round11(srcT),
|
|
7651
7826
|
out: round11(srcT + len),
|
|
7652
7827
|
rate: DEFAULT_SPEED_RATE,
|
|
7653
|
-
source: "manual"
|
|
7828
|
+
source: "manual",
|
|
7829
|
+
...mediaHere ? { media: mediaHere } : {}
|
|
7654
7830
|
}
|
|
7655
7831
|
].sort((a, b) => a.in - b.in);
|
|
7656
7832
|
};
|
|
@@ -7698,7 +7874,7 @@ var speedLane = {
|
|
|
7698
7874
|
out: sp.out
|
|
7699
7875
|
};
|
|
7700
7876
|
} else {
|
|
7701
|
-
const startOut =
|
|
7877
|
+
const startOut = sourceToTimeline2(base, sp.in) ?? sp.in;
|
|
7702
7878
|
const sourceT = sp.in + sp.rate * Math.max(0, g.t - startOut);
|
|
7703
7879
|
next = {
|
|
7704
7880
|
in: sp.in,
|
|
@@ -8261,6 +8437,7 @@ export {
|
|
|
8261
8437
|
OVERLAY_Z,
|
|
8262
8438
|
PLATE_GROUND,
|
|
8263
8439
|
PLAYBACK_ACTIVITY,
|
|
8440
|
+
PRIMARY_MEDIA,
|
|
8264
8441
|
PROGRAM_RETIME,
|
|
8265
8442
|
REJECT_OVERLAP,
|
|
8266
8443
|
REST_TILT_ID,
|
|
@@ -8348,6 +8525,7 @@ export {
|
|
|
8348
8525
|
destinationsForChannel,
|
|
8349
8526
|
docCardLayout,
|
|
8350
8527
|
docFreezes,
|
|
8528
|
+
docMediaList,
|
|
8351
8529
|
docOutputDuration,
|
|
8352
8530
|
docRestTime,
|
|
8353
8531
|
docToCropSpace,
|
|
@@ -8393,12 +8571,17 @@ export {
|
|
|
8393
8571
|
lowerProgramDoc,
|
|
8394
8572
|
lowerStudioDoc,
|
|
8395
8573
|
lowerToComposition,
|
|
8574
|
+
mediaAtOutput,
|
|
8575
|
+
mediaDuration,
|
|
8576
|
+
mediaKey,
|
|
8577
|
+
mediaSource,
|
|
8396
8578
|
micLane,
|
|
8397
8579
|
migrateHostedDoc,
|
|
8398
8580
|
migrateMotion,
|
|
8399
8581
|
momentsFromDoc,
|
|
8400
8582
|
motionTrack,
|
|
8401
8583
|
musicBedClip,
|
|
8584
|
+
nextMediaId,
|
|
8402
8585
|
normalizeCaptureSpace,
|
|
8403
8586
|
objectMotionBase,
|
|
8404
8587
|
objectMotionPoseAt,
|
|
@@ -8441,6 +8624,7 @@ export {
|
|
|
8441
8624
|
resolveText3dAsset,
|
|
8442
8625
|
resolveZoomStyle,
|
|
8443
8626
|
restLeanOf,
|
|
8627
|
+
sameMedia,
|
|
8444
8628
|
sceneChanges,
|
|
8445
8629
|
scrollRuns,
|
|
8446
8630
|
setSpeedInRange,
|