@vosjs/studio-core 0.13.0 → 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 +268 -81
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 +
|
|
@@ -4472,13 +4588,16 @@ var ON_FRAME = `(ctx, content, dt) => {
|
|
|
4472
4588
|
// corners, drawn by the clip alone, hid theirs inside the arc.
|
|
4473
4589
|
var shD = ceZs * (cardW + cardH) + 2 * (W + H)
|
|
4474
4590
|
var shX = cardX - shD / ceZs
|
|
4475
|
-
// The card shadow is LAYERED:
|
|
4476
|
-
// while each stays at a low alpha (shadow is the strength they share)
|
|
4477
|
-
//
|
|
4478
|
-
//
|
|
4479
|
-
// a
|
|
4591
|
+
// The card shadow is LAYERED: four shadows whose blur and offset grow
|
|
4592
|
+
// while each stays at a low alpha (shadow is the strength they share):
|
|
4593
|
+
// a hairline that defines the edge, two that ground it, and a WIDE,
|
|
4594
|
+
// faint ambient bloom (120 design px of blur, dropped well below the
|
|
4595
|
+
// card) that is what lets a large card read as lifted off a light
|
|
4596
|
+
// ground instead of outlined on it. One wide layer at the full strength
|
|
4597
|
+
// was the old paint, and at the default 0.4 it read as a dark pool
|
|
4598
|
+
// under the card; three tight layers read as a rim.
|
|
4480
4599
|
if (shadow > 0) {
|
|
4481
|
-
var shL = [[
|
|
4600
|
+
var shL = [[2, 1, 0.1], [12, 4, 0.1], [40, 16, 0.14], [120, 44, 0.3]]
|
|
4482
4601
|
for (var shI = 0; shI < shL.length; shI++) {
|
|
4483
4602
|
c.save()
|
|
4484
4603
|
c.shadowColor = 'rgba(' + shRgb + ',' + +(shadow * shL[shI][2]).toFixed(3) + ')'
|
|
@@ -4595,7 +4714,7 @@ var ON_FRAME = `(ctx, content, dt) => {
|
|
|
4595
4714
|
// card. Contain never needs it (the video rect IS the card).
|
|
4596
4715
|
if (fitCover) { c.save(); rr(cardX, cardY, cardW, cardH, radius); c.clip() }
|
|
4597
4716
|
// cursor coordinate space + drawn radius (shared by click effects + the dot)
|
|
4598
|
-
var space = d.cursorSpace || { w: vw, h: vh }
|
|
4717
|
+
var space = (actM ? actM.cursorSpace : d.cursorSpace) || { w: vw, h: vh }
|
|
4599
4718
|
var curSize = ((d.cursorStyle && d.cursorStyle.size) || 24) * s2 * 0.5
|
|
4600
4719
|
|
|
4601
4720
|
// click effects \u2014 pure f(t): d.clicks are
|
|
@@ -4627,8 +4746,9 @@ var ON_FRAME = `(ctx, content, dt) => {
|
|
|
4627
4746
|
// click's source moment, or an effect near a cut would keep painting
|
|
4628
4747
|
// over the NEXT segment's unrelated footage
|
|
4629
4748
|
if (Math.abs(srcT - ck.st) > 2) continue
|
|
4630
|
-
var
|
|
4631
|
-
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
|
|
4632
4752
|
// press dip: smoothstep down around the real mousedown (80 ms lead \u2014
|
|
4633
4753
|
// anticipation is what makes effects feel synced), hold through the real
|
|
4634
4754
|
// down\u2192up span (drags dip long), easeOutBack rebound with a slight
|
|
@@ -4741,7 +4861,7 @@ var ON_FRAME = `(ctx, content, dt) => {
|
|
|
4741
4861
|
// The dot is the only thing cursorStyle.visible hides \u2014 the track still drives
|
|
4742
4862
|
// cursor-follow zoom, and click effects draw above on their own switch.
|
|
4743
4863
|
// Undefined reads as visible so pre-toggle docs are unchanged.
|
|
4744
|
-
var cur = d.cursor || []
|
|
4864
|
+
var cur = (actM ? actM.cursor : d.cursor) || []
|
|
4745
4865
|
if (cur.length && !(d.cursorStyle && d.cursorStyle.visible === false)) {
|
|
4746
4866
|
var px = cur[0].x, py = cur[0].y
|
|
4747
4867
|
for (var j = 0; j < cur.length; j++) { if (cur[j].t <= srcT) { px = cur[j].x; py = cur[j].y } }
|
|
@@ -4750,7 +4870,7 @@ var ON_FRAME = `(ctx, content, dt) => {
|
|
|
4750
4870
|
// Idle fade: a sparse SOURCE-time opacity curve baked by cursorIdleFade.
|
|
4751
4871
|
// Linear between keys \u2014 opacity needs no easing, and the ramps are already
|
|
4752
4872
|
// shaped by where the keys sit. Absent/empty = the cursor never dwells.
|
|
4753
|
-
var cuA = 1, cuK = d.cursorFade
|
|
4873
|
+
var cuA = 1, cuK = actM ? actM.cursorFade : d.cursorFade
|
|
4754
4874
|
if (cuK && cuK.length) {
|
|
4755
4875
|
if (srcT <= cuK[0].t) cuA = cuK[0].a
|
|
4756
4876
|
else if (srcT >= cuK[cuK.length - 1].t) cuA = cuK[cuK.length - 1].a
|
|
@@ -4783,7 +4903,7 @@ var ON_FRAME = `(ctx, content, dt) => {
|
|
|
4783
4903
|
// runtime; the card c2d under stubs). Redraw + re-upload only while the
|
|
4784
4904
|
// bubble is active (a video \u2192 every frame), on resize, or once when it turns
|
|
4785
4905
|
// off (to clear) \u2014 so a cam-less take never uploads the overlay after frame 1.
|
|
4786
|
-
var camV = r.cam, camS = d.cam || {}
|
|
4906
|
+
var camV = amId ? null : r.cam, camS = d.cam || {}
|
|
4787
4907
|
var camOn = !camS.window || (srcT >= camS.window.in && srcT <= camS.window.out)
|
|
4788
4908
|
// Readiness is STICKY: readyState drops to HAVE_METADATA while a seek is in
|
|
4789
4909
|
// flight, so gating each frame on it makes the bubble vanish on every scrub
|
|
@@ -5125,7 +5245,8 @@ function lowerToComposition(input) {
|
|
|
5125
5245
|
segments: rated.map((seg) => ({
|
|
5126
5246
|
in: round6(seg.in),
|
|
5127
5247
|
out: round6(seg.out),
|
|
5128
|
-
...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 } : {}
|
|
5129
5250
|
})),
|
|
5130
5251
|
frame: doc.frame,
|
|
5131
5252
|
micGain: doc.micGain ?? 1,
|
|
@@ -5136,6 +5257,34 @@ function lowerToComposition(input) {
|
|
|
5136
5257
|
})),
|
|
5137
5258
|
cursorStyle: doc.cursor,
|
|
5138
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
|
+
} : {},
|
|
5139
5288
|
...cursorFade.length ? { cursorFade } : {},
|
|
5140
5289
|
// Click effects: OUTPUT-anchored click records + resolved styling (named
|
|
5141
5290
|
// intensities/colors become numbers HERE — ON_FRAME reads no registry).
|
|
@@ -5204,10 +5353,24 @@ function clickFxData(doc, rated) {
|
|
|
5204
5353
|
const meta = doc.source.meta;
|
|
5205
5354
|
const level = CLICK_FX_INTENSITY[fx.intensity];
|
|
5206
5355
|
return {
|
|
5207
|
-
clicks: on ?
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
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) : [],
|
|
5211
5374
|
clickFx: {
|
|
5212
5375
|
style: fx.style,
|
|
5213
5376
|
press: fx.press,
|
|
@@ -6832,7 +6995,7 @@ import {
|
|
|
6832
6995
|
mapTime as mapTime2,
|
|
6833
6996
|
removeSegment,
|
|
6834
6997
|
segmentRate as segmentRate3,
|
|
6835
|
-
sourceToTimeline as
|
|
6998
|
+
sourceToTimeline as sourceToTimeline2,
|
|
6836
6999
|
splitBySpeed as splitBySpeed3,
|
|
6837
7000
|
totalDuration as totalDuration6,
|
|
6838
7001
|
trimSegment
|
|
@@ -6917,7 +7080,7 @@ var videoLane = {
|
|
|
6917
7080
|
freezes.filter((f) => f.at < seg.out - 1e-9),
|
|
6918
7081
|
index === 0 || !contiguous(segments, index)
|
|
6919
7082
|
);
|
|
6920
|
-
const anchorOut =
|
|
7083
|
+
const anchorOut = sourceToTimeline2(fullMap, Math.min(edgeSrc, sourceDuration)) ?? edgeSrc;
|
|
6921
7084
|
const sourceT = mapTime2(fullMap, anchorOut + (g.t - edgeOutNow));
|
|
6922
7085
|
const next = trimSegment(
|
|
6923
7086
|
segments,
|
|
@@ -7047,10 +7210,16 @@ var freezeLane = {
|
|
|
7047
7210
|
const srcT = round11(mapTime2(rated, Math.max(0, g.t)));
|
|
7048
7211
|
if (!kept(srcT) || !clear(srcT)) return null;
|
|
7049
7212
|
const id = nextFreezeId(doc);
|
|
7213
|
+
const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
|
|
7050
7214
|
return (d) => {
|
|
7051
7215
|
d.freeze = [
|
|
7052
7216
|
...d.freeze ?? [],
|
|
7053
|
-
{
|
|
7217
|
+
{
|
|
7218
|
+
id,
|
|
7219
|
+
at: srcT,
|
|
7220
|
+
seconds: FREEZE_DEFAULT_SECONDS,
|
|
7221
|
+
...mediaHere ? { media: mediaHere } : {}
|
|
7222
|
+
}
|
|
7054
7223
|
].sort((a, b) => a.at - b.at);
|
|
7055
7224
|
};
|
|
7056
7225
|
}
|
|
@@ -7135,7 +7304,7 @@ var zoomLane = {
|
|
|
7135
7304
|
items(doc) {
|
|
7136
7305
|
const segments = ratedSegments(doc);
|
|
7137
7306
|
return doc.zoom.flatMap((z) => {
|
|
7138
|
-
const ext = spanOutputExtent(segments, z.in, z.out);
|
|
7307
|
+
const ext = spanOutputExtent(segments, z.in, z.out, z.media);
|
|
7139
7308
|
return ext === null ? [] : [
|
|
7140
7309
|
{
|
|
7141
7310
|
id: z.id,
|
|
@@ -7153,10 +7322,13 @@ var zoomLane = {
|
|
|
7153
7322
|
const sourceDuration = anchorSourceDuration(doc);
|
|
7154
7323
|
if (g.type === "create") {
|
|
7155
7324
|
const srcT = mapTime2(rated, Math.max(0, g.t));
|
|
7156
|
-
|
|
7157
|
-
const
|
|
7158
|
-
const
|
|
7159
|
-
|
|
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);
|
|
7160
7332
|
if (len < ZOOM_SPAN_MIN * rateAt(rated, srcT)) return null;
|
|
7161
7333
|
const id = nextZoomId(doc);
|
|
7162
7334
|
const { cx, cy } = cursorFocusAt(doc, srcT);
|
|
@@ -7170,7 +7342,8 @@ var zoomLane = {
|
|
|
7170
7342
|
level: DEFAULT_ZOOM_LEVEL,
|
|
7171
7343
|
cx,
|
|
7172
7344
|
cy,
|
|
7173
|
-
source: "manual"
|
|
7345
|
+
source: "manual",
|
|
7346
|
+
...mediaHere ? { media: mediaHere } : {}
|
|
7174
7347
|
}
|
|
7175
7348
|
].sort((a, b) => a.in - b.in);
|
|
7176
7349
|
};
|
|
@@ -7260,7 +7433,7 @@ var tiltLane = {
|
|
|
7260
7433
|
items(doc) {
|
|
7261
7434
|
const segments = ratedSegments(doc);
|
|
7262
7435
|
return (doc.tilt ?? []).flatMap((z) => {
|
|
7263
|
-
const ext = spanOutputExtent(segments, z.in, z.out);
|
|
7436
|
+
const ext = spanOutputExtent(segments, z.in, z.out, z.media);
|
|
7264
7437
|
return ext === null ? [] : [
|
|
7265
7438
|
{
|
|
7266
7439
|
id: z.id,
|
|
@@ -7278,10 +7451,13 @@ var tiltLane = {
|
|
|
7278
7451
|
const sourceDuration = anchorSourceDuration(doc);
|
|
7279
7452
|
if (g.type === "create") {
|
|
7280
7453
|
const srcT = mapTime2(rated, Math.max(0, g.t));
|
|
7281
|
-
|
|
7282
|
-
const
|
|
7283
|
-
const
|
|
7284
|
-
|
|
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);
|
|
7285
7461
|
if (len < TILT_SPAN_MIN * rateAt(rated, srcT)) return null;
|
|
7286
7462
|
const id = nextTiltId(doc);
|
|
7287
7463
|
return (d) => {
|
|
@@ -7293,7 +7469,8 @@ var tiltLane = {
|
|
|
7293
7469
|
out: round11(srcT + len),
|
|
7294
7470
|
rx: DEFAULT_TILT_POSE.rx,
|
|
7295
7471
|
ry: DEFAULT_TILT_POSE.ry,
|
|
7296
|
-
source: "manual"
|
|
7472
|
+
source: "manual",
|
|
7473
|
+
...mediaHere ? { media: mediaHere } : {}
|
|
7297
7474
|
}
|
|
7298
7475
|
].sort((a, b) => a.in - b.in);
|
|
7299
7476
|
};
|
|
@@ -7373,7 +7550,7 @@ var camMoveLane = {
|
|
|
7373
7550
|
if (!doc.source.camKey) return [];
|
|
7374
7551
|
const segments = ratedSegments(doc);
|
|
7375
7552
|
return (doc.camMotion ?? []).flatMap((z) => {
|
|
7376
|
-
const ext = spanOutputExtent(segments, z.in, z.out);
|
|
7553
|
+
const ext = spanOutputExtent(segments, z.in, z.out, z.media);
|
|
7377
7554
|
return ext === null ? [] : [
|
|
7378
7555
|
{
|
|
7379
7556
|
id: z.id,
|
|
@@ -7639,6 +7816,7 @@ var speedLane = {
|
|
|
7639
7816
|
const len = Math.min(spanDefaultLen(sourceDuration), limit - srcT);
|
|
7640
7817
|
if (len < SPEED_SPAN_MIN * DEFAULT_SPEED_RATE) return null;
|
|
7641
7818
|
const id = nextSpeedId(doc);
|
|
7819
|
+
const mediaHere = mediaAtOutput(rated, Math.max(0, g.t));
|
|
7642
7820
|
return (d) => {
|
|
7643
7821
|
d.speed = [
|
|
7644
7822
|
...d.speed ?? [],
|
|
@@ -7647,7 +7825,8 @@ var speedLane = {
|
|
|
7647
7825
|
in: round11(srcT),
|
|
7648
7826
|
out: round11(srcT + len),
|
|
7649
7827
|
rate: DEFAULT_SPEED_RATE,
|
|
7650
|
-
source: "manual"
|
|
7828
|
+
source: "manual",
|
|
7829
|
+
...mediaHere ? { media: mediaHere } : {}
|
|
7651
7830
|
}
|
|
7652
7831
|
].sort((a, b) => a.in - b.in);
|
|
7653
7832
|
};
|
|
@@ -7695,7 +7874,7 @@ var speedLane = {
|
|
|
7695
7874
|
out: sp.out
|
|
7696
7875
|
};
|
|
7697
7876
|
} else {
|
|
7698
|
-
const startOut =
|
|
7877
|
+
const startOut = sourceToTimeline2(base, sp.in) ?? sp.in;
|
|
7699
7878
|
const sourceT = sp.in + sp.rate * Math.max(0, g.t - startOut);
|
|
7700
7879
|
next = {
|
|
7701
7880
|
in: sp.in,
|
|
@@ -8258,6 +8437,7 @@ export {
|
|
|
8258
8437
|
OVERLAY_Z,
|
|
8259
8438
|
PLATE_GROUND,
|
|
8260
8439
|
PLAYBACK_ACTIVITY,
|
|
8440
|
+
PRIMARY_MEDIA,
|
|
8261
8441
|
PROGRAM_RETIME,
|
|
8262
8442
|
REJECT_OVERLAP,
|
|
8263
8443
|
REST_TILT_ID,
|
|
@@ -8345,6 +8525,7 @@ export {
|
|
|
8345
8525
|
destinationsForChannel,
|
|
8346
8526
|
docCardLayout,
|
|
8347
8527
|
docFreezes,
|
|
8528
|
+
docMediaList,
|
|
8348
8529
|
docOutputDuration,
|
|
8349
8530
|
docRestTime,
|
|
8350
8531
|
docToCropSpace,
|
|
@@ -8390,12 +8571,17 @@ export {
|
|
|
8390
8571
|
lowerProgramDoc,
|
|
8391
8572
|
lowerStudioDoc,
|
|
8392
8573
|
lowerToComposition,
|
|
8574
|
+
mediaAtOutput,
|
|
8575
|
+
mediaDuration,
|
|
8576
|
+
mediaKey,
|
|
8577
|
+
mediaSource,
|
|
8393
8578
|
micLane,
|
|
8394
8579
|
migrateHostedDoc,
|
|
8395
8580
|
migrateMotion,
|
|
8396
8581
|
momentsFromDoc,
|
|
8397
8582
|
motionTrack,
|
|
8398
8583
|
musicBedClip,
|
|
8584
|
+
nextMediaId,
|
|
8399
8585
|
normalizeCaptureSpace,
|
|
8400
8586
|
objectMotionBase,
|
|
8401
8587
|
objectMotionPoseAt,
|
|
@@ -8438,6 +8624,7 @@ export {
|
|
|
8438
8624
|
resolveText3dAsset,
|
|
8439
8625
|
resolveZoomStyle,
|
|
8440
8626
|
restLeanOf,
|
|
8627
|
+
sameMedia,
|
|
8441
8628
|
sceneChanges,
|
|
8442
8629
|
scrollRuns,
|
|
8443
8630
|
setSpeedInRange,
|