@real-music-packages/web-core 0.45.2 → 0.47.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/README.md +10 -1
- package/dist/audio.js +7 -7
- package/dist/chunk-JCDNEJBT.js +176 -0
- package/dist/chunk-JCDNEJBT.js.map +1 -0
- package/dist/notationBeams.d.ts +26 -0
- package/dist/notationBeams.js +133 -0
- package/dist/notationBeams.js.map +1 -0
- package/dist/notationPlayerVerovio.d.ts +26 -57
- package/dist/notationPlayerVerovio.js +27 -136
- package/dist/notationPlayerVerovio.js.map +1 -1
- package/dist/notationXml.d.ts +175 -0
- package/dist/notationXml.js +19 -0
- package/dist/notationXml.js.map +1 -0
- package/dist/scene/index.d.ts +410 -2
- package/dist/scene/index.js +725 -21
- package/dist/scene/index.js.map +1 -1
- package/package.json +9 -1
package/dist/scene/index.js
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
vstackFollowBox
|
|
40
40
|
} from "../chunk-DBFGX6OK.js";
|
|
41
41
|
import {
|
|
42
|
+
SAFE_ZONE,
|
|
42
43
|
ctaScene,
|
|
43
44
|
drawSafeGuides,
|
|
44
45
|
hookScene,
|
|
@@ -2528,18 +2529,18 @@ function sectionMinimapLayer() {
|
|
|
2528
2529
|
c.fillRect(x0 - 1, barY - round - 3, 2, barHeight + 6);
|
|
2529
2530
|
});
|
|
2530
2531
|
}
|
|
2531
|
-
const
|
|
2532
|
+
const px2 = timeToX(left, width, dur, tMs);
|
|
2532
2533
|
c.globalAlpha = 0.85;
|
|
2533
2534
|
c.fillStyle = accent;
|
|
2534
|
-
drawBar(c, left, barY - round, Math.max(0,
|
|
2535
|
+
drawBar(c, left, barY - round, Math.max(0, px2 - left), barHeight, round);
|
|
2535
2536
|
c.globalAlpha = 1;
|
|
2536
2537
|
c.fillStyle = accent;
|
|
2537
2538
|
c.beginPath();
|
|
2538
|
-
c.arc(
|
|
2539
|
+
c.arc(px2, barY, barHeight * 1.6, 0, Math.PI * 2);
|
|
2539
2540
|
c.fill();
|
|
2540
2541
|
c.fillStyle = ctx.theme.paper;
|
|
2541
2542
|
c.beginPath();
|
|
2542
|
-
c.arc(
|
|
2543
|
+
c.arc(px2, barY, barHeight * 0.7, 0, Math.PI * 2);
|
|
2543
2544
|
c.fill();
|
|
2544
2545
|
if (showLabel) {
|
|
2545
2546
|
const act = activeSection(sections, tMs);
|
|
@@ -2549,7 +2550,7 @@ function sectionMinimapLayer() {
|
|
|
2549
2550
|
c.textAlign = "center";
|
|
2550
2551
|
c.textBaseline = "bottom";
|
|
2551
2552
|
c.font = `700 ${Math.round(barHeight * 2.4)}px ${ctx.theme.fontBody}`;
|
|
2552
|
-
c.fillText(act.label,
|
|
2553
|
+
c.fillText(act.label, px2, barY - barHeight * 2.2);
|
|
2553
2554
|
}
|
|
2554
2555
|
}
|
|
2555
2556
|
c.restore();
|
|
@@ -3836,9 +3837,9 @@ function tensionGraphLayer() {
|
|
|
3836
3837
|
c.stroke();
|
|
3837
3838
|
const revealed = [];
|
|
3838
3839
|
for (let i = 0; i < series.length; i++) {
|
|
3839
|
-
const
|
|
3840
|
-
if (
|
|
3841
|
-
revealed.push({ x:
|
|
3840
|
+
const px2 = x(series[i].tMs);
|
|
3841
|
+
if (px2 <= playX) {
|
|
3842
|
+
revealed.push({ x: px2, y: y(series[i].value) });
|
|
3842
3843
|
} else {
|
|
3843
3844
|
if (i > 0) {
|
|
3844
3845
|
const a = series[i - 1], b = series[i];
|
|
@@ -4529,15 +4530,15 @@ function compositeRhythmLayer() {
|
|
|
4529
4530
|
c.stroke();
|
|
4530
4531
|
}
|
|
4531
4532
|
if (playhead && tMs >= w0 && tMs <= w1) {
|
|
4532
|
-
const
|
|
4533
|
+
const px2 = x(tMs);
|
|
4533
4534
|
const topY = grids.length ? lhY - rowGap * grids.length - tickH : lhY - tickH;
|
|
4534
4535
|
const botY = rhY + tickH;
|
|
4535
4536
|
c.strokeStyle = mergeColor;
|
|
4536
4537
|
c.globalAlpha = 0.5;
|
|
4537
4538
|
c.lineWidth = 2;
|
|
4538
4539
|
c.beginPath();
|
|
4539
|
-
c.moveTo(
|
|
4540
|
-
c.lineTo(
|
|
4540
|
+
c.moveTo(px2, topY);
|
|
4541
|
+
c.lineTo(px2, botY);
|
|
4541
4542
|
c.stroke();
|
|
4542
4543
|
c.globalAlpha = 1;
|
|
4543
4544
|
}
|
|
@@ -4585,6 +4586,241 @@ var compositeRhythmFactory = {
|
|
|
4585
4586
|
}
|
|
4586
4587
|
};
|
|
4587
4588
|
|
|
4589
|
+
// src/scene/layers/videoSource.ts
|
|
4590
|
+
var DEFAULTS12 = {
|
|
4591
|
+
fit: "cover",
|
|
4592
|
+
focus: { x: 0.5, y: 0.38 },
|
|
4593
|
+
startMs: 0,
|
|
4594
|
+
sourceStartMs: 0,
|
|
4595
|
+
loop: true,
|
|
4596
|
+
rate: 1,
|
|
4597
|
+
mute: true,
|
|
4598
|
+
opacity: 1,
|
|
4599
|
+
clockMode: "play",
|
|
4600
|
+
driftTolMs: 120
|
|
4601
|
+
};
|
|
4602
|
+
function clamp2(x, lo, hi) {
|
|
4603
|
+
return x < lo ? lo : x > hi ? hi : x;
|
|
4604
|
+
}
|
|
4605
|
+
function fitBoxes(iW, iH, bw, bh, mode, focus) {
|
|
4606
|
+
if (iW <= 0 || iH <= 0 || bw <= 0 || bh <= 0) {
|
|
4607
|
+
return { sx: 0, sy: 0, sw: Math.max(0, iW), sh: Math.max(0, iH), dx: 0, dy: 0, dw: 0, dh: 0 };
|
|
4608
|
+
}
|
|
4609
|
+
if (mode === "contain") {
|
|
4610
|
+
const scale2 = Math.min(bw / iW, bh / iH);
|
|
4611
|
+
const dw = iW * scale2;
|
|
4612
|
+
const dh = iH * scale2;
|
|
4613
|
+
return { sx: 0, sy: 0, sw: iW, sh: iH, dx: (bw - dw) / 2, dy: (bh - dh) / 2, dw, dh };
|
|
4614
|
+
}
|
|
4615
|
+
const scale = Math.max(bw / iW, bh / iH);
|
|
4616
|
+
const sw = Math.min(iW, bw / scale);
|
|
4617
|
+
const sh = Math.min(iH, bh / scale);
|
|
4618
|
+
const sx = clamp2(focus.x * iW - sw / 2, 0, iW - sw);
|
|
4619
|
+
const sy = clamp2(focus.y * iH - sh / 2, 0, iH - sh);
|
|
4620
|
+
return { sx, sy, sw, sh, dx: 0, dy: 0, dw: bw, dh: bh };
|
|
4621
|
+
}
|
|
4622
|
+
function sourceTimeMs(tMs, o) {
|
|
4623
|
+
const elapsed = Math.max(0, (tMs - o.startMs) * o.rate);
|
|
4624
|
+
const span = o.durationMs - o.sourceStartMs;
|
|
4625
|
+
if (!o.loop || !(span > 0)) {
|
|
4626
|
+
const end = o.durationMs > 0 ? o.durationMs : Infinity;
|
|
4627
|
+
return Math.min(o.sourceStartMs + elapsed, end);
|
|
4628
|
+
}
|
|
4629
|
+
return o.sourceStartMs + elapsed % span;
|
|
4630
|
+
}
|
|
4631
|
+
function needsSeek(currentMs, wantMs, tolMs) {
|
|
4632
|
+
return Math.abs(currentMs - wantMs) > tolMs;
|
|
4633
|
+
}
|
|
4634
|
+
function isVideoLike(x) {
|
|
4635
|
+
return !!x && typeof x === "object" && typeof x.currentTime === "number" && typeof x.duration === "number";
|
|
4636
|
+
}
|
|
4637
|
+
function sourceSize(src) {
|
|
4638
|
+
const s = src;
|
|
4639
|
+
const w = s?.videoWidth || s?.naturalWidth || s?.width || 0;
|
|
4640
|
+
const h = s?.videoHeight || s?.naturalHeight || s?.height || 0;
|
|
4641
|
+
return { w, h };
|
|
4642
|
+
}
|
|
4643
|
+
function loadVideo(url, mute) {
|
|
4644
|
+
const v = document.createElement("video");
|
|
4645
|
+
v.src = url;
|
|
4646
|
+
v.muted = mute;
|
|
4647
|
+
v.playsInline = true;
|
|
4648
|
+
v.preload = "auto";
|
|
4649
|
+
v.crossOrigin = "anonymous";
|
|
4650
|
+
return new Promise((resolve, reject) => {
|
|
4651
|
+
const ok = () => resolve(v);
|
|
4652
|
+
v.addEventListener("canplaythrough", ok, { once: true });
|
|
4653
|
+
v.addEventListener("error", () => reject(new Error(`video-source: failed to load ${url}`)), { once: true });
|
|
4654
|
+
v.load();
|
|
4655
|
+
});
|
|
4656
|
+
}
|
|
4657
|
+
function videoSourceLayer() {
|
|
4658
|
+
let src = null;
|
|
4659
|
+
let o = { ...DEFAULTS12, durationMs: 0 };
|
|
4660
|
+
let started = false;
|
|
4661
|
+
return {
|
|
4662
|
+
key: "video-source",
|
|
4663
|
+
async init(_ctx, props) {
|
|
4664
|
+
o = {
|
|
4665
|
+
fit: props.fit ?? DEFAULTS12.fit,
|
|
4666
|
+
focus: props.focus ?? DEFAULTS12.focus,
|
|
4667
|
+
startMs: props.startMs ?? DEFAULTS12.startMs,
|
|
4668
|
+
sourceStartMs: props.sourceStartMs ?? DEFAULTS12.sourceStartMs,
|
|
4669
|
+
loop: props.loop ?? DEFAULTS12.loop,
|
|
4670
|
+
rate: props.rate ?? DEFAULTS12.rate,
|
|
4671
|
+
mute: props.mute ?? DEFAULTS12.mute,
|
|
4672
|
+
opacity: props.opacity ?? DEFAULTS12.opacity,
|
|
4673
|
+
clockMode: props.clockMode ?? DEFAULTS12.clockMode,
|
|
4674
|
+
driftTolMs: props.driftTolMs ?? DEFAULTS12.driftTolMs,
|
|
4675
|
+
durationMs: props.durationMs ?? 0
|
|
4676
|
+
};
|
|
4677
|
+
if (o.clockMode === "seek") {
|
|
4678
|
+
throw new Error(
|
|
4679
|
+
'video-source: clockMode "seek" needs an async draw path the runner does not have yet; use "play"'
|
|
4680
|
+
);
|
|
4681
|
+
}
|
|
4682
|
+
src = typeof props.src === "string" ? await loadVideo(props.src, o.mute) : props.src;
|
|
4683
|
+
if (isVideoLike(src)) {
|
|
4684
|
+
src.muted = o.mute;
|
|
4685
|
+
src.playbackRate = o.rate;
|
|
4686
|
+
if (!o.durationMs && Number.isFinite(src.duration)) o.durationMs = src.duration * 1e3;
|
|
4687
|
+
}
|
|
4688
|
+
},
|
|
4689
|
+
draw(ctx, tMs) {
|
|
4690
|
+
if (!src) return;
|
|
4691
|
+
const { w: iW, h: iH } = sourceSize(src);
|
|
4692
|
+
if (iW <= 0 || iH <= 0) return;
|
|
4693
|
+
if (isVideoLike(src)) {
|
|
4694
|
+
const want = sourceTimeMs(tMs, o);
|
|
4695
|
+
if (!started && tMs >= o.startMs) {
|
|
4696
|
+
started = true;
|
|
4697
|
+
src.currentTime = want / 1e3;
|
|
4698
|
+
void src.play?.();
|
|
4699
|
+
} else if (started && needsSeek(src.currentTime * 1e3, want, o.driftTolMs)) {
|
|
4700
|
+
src.currentTime = want / 1e3;
|
|
4701
|
+
}
|
|
4702
|
+
}
|
|
4703
|
+
const b = fitBoxes(iW, iH, ctx.W, ctx.H, o.fit, o.focus);
|
|
4704
|
+
if (b.dw <= 0 || b.dh <= 0) return;
|
|
4705
|
+
const g = ctx.ctx2d;
|
|
4706
|
+
g.save();
|
|
4707
|
+
if (o.opacity !== 1) g.globalAlpha = g.globalAlpha * o.opacity;
|
|
4708
|
+
g.drawImage(src, b.sx, b.sy, b.sw, b.sh, b.dx, b.dy, b.dw, b.dh);
|
|
4709
|
+
g.restore();
|
|
4710
|
+
}
|
|
4711
|
+
};
|
|
4712
|
+
}
|
|
4713
|
+
var videoSourceFactory = {
|
|
4714
|
+
key: "video-source",
|
|
4715
|
+
create: videoSourceLayer,
|
|
4716
|
+
validateProps(props) {
|
|
4717
|
+
const errs = [];
|
|
4718
|
+
if (props == null || typeof props !== "object") return ["video-source: props must be an object"];
|
|
4719
|
+
const p = props;
|
|
4720
|
+
if (p.src == null) errs.push("video-source.src is required");
|
|
4721
|
+
else if (typeof p.src !== "string" && typeof p.src !== "object") {
|
|
4722
|
+
errs.push("video-source.src must be a URL string or a CanvasImageSource");
|
|
4723
|
+
}
|
|
4724
|
+
if (p.fit != null && p.fit !== "cover" && p.fit !== "contain") {
|
|
4725
|
+
errs.push('video-source.fit must be "cover" or "contain"');
|
|
4726
|
+
}
|
|
4727
|
+
if (p.clockMode != null && p.clockMode !== "play" && p.clockMode !== "seek") {
|
|
4728
|
+
errs.push('video-source.clockMode must be "play" or "seek"');
|
|
4729
|
+
}
|
|
4730
|
+
if (p.focus != null) {
|
|
4731
|
+
const f = p.focus;
|
|
4732
|
+
if (typeof f?.x !== "number" || typeof f?.y !== "number") {
|
|
4733
|
+
errs.push("video-source.focus must be {x, y} numbers");
|
|
4734
|
+
}
|
|
4735
|
+
}
|
|
4736
|
+
for (const k of ["startMs", "sourceStartMs", "rate", "opacity", "driftTolMs", "durationMs"]) {
|
|
4737
|
+
if (p[k] != null && (typeof p[k] !== "number" || !Number.isFinite(p[k]))) {
|
|
4738
|
+
errs.push(`video-source.${k} must be a finite number`);
|
|
4739
|
+
}
|
|
4740
|
+
}
|
|
4741
|
+
if (p.rate != null && p.rate <= 0) errs.push("video-source.rate must be > 0");
|
|
4742
|
+
const looping = p.loop == null ? DEFAULTS12.loop : p.loop === true;
|
|
4743
|
+
if (looping && typeof p.src === "object" && p.durationMs == null) {
|
|
4744
|
+
errs.push("video-source.durationMs is required when looping a non-<video> source");
|
|
4745
|
+
}
|
|
4746
|
+
return errs;
|
|
4747
|
+
}
|
|
4748
|
+
};
|
|
4749
|
+
|
|
4750
|
+
// src/scene/layers/paneSeam.ts
|
|
4751
|
+
var DEFAULTS13 = {
|
|
4752
|
+
orientation: "h",
|
|
4753
|
+
style: "line",
|
|
4754
|
+
thicknessPx: 4,
|
|
4755
|
+
shadowPx: 28,
|
|
4756
|
+
opacity: 0.9
|
|
4757
|
+
};
|
|
4758
|
+
function paneSeamLayer() {
|
|
4759
|
+
let p = { ...DEFAULTS13 };
|
|
4760
|
+
return {
|
|
4761
|
+
key: "pane-seam",
|
|
4762
|
+
init(_ctx, props) {
|
|
4763
|
+
p = {
|
|
4764
|
+
atFrac: props.atFrac,
|
|
4765
|
+
orientation: props.orientation ?? DEFAULTS13.orientation,
|
|
4766
|
+
style: props.style ?? DEFAULTS13.style,
|
|
4767
|
+
thicknessPx: props.thicknessPx ?? DEFAULTS13.thicknessPx,
|
|
4768
|
+
shadowPx: props.shadowPx ?? DEFAULTS13.shadowPx,
|
|
4769
|
+
opacity: props.opacity ?? DEFAULTS13.opacity,
|
|
4770
|
+
color: props.color
|
|
4771
|
+
};
|
|
4772
|
+
},
|
|
4773
|
+
draw(ctx) {
|
|
4774
|
+
const g = ctx.ctx2d;
|
|
4775
|
+
const horiz = p.orientation === "h";
|
|
4776
|
+
const at = horiz ? p.atFrac * ctx.H : p.atFrac * ctx.W;
|
|
4777
|
+
const color = p.color ?? ctx.theme.ink;
|
|
4778
|
+
g.save();
|
|
4779
|
+
if (p.style === "shadow" && p.shadowPx > 0) {
|
|
4780
|
+
const grad = horiz ? g.createLinearGradient(0, at - p.shadowPx, 0, at + p.shadowPx) : g.createLinearGradient(at - p.shadowPx, 0, at + p.shadowPx, 0);
|
|
4781
|
+
grad.addColorStop(0, "rgba(0,0,0,0)");
|
|
4782
|
+
grad.addColorStop(0.5, "rgba(0,0,0,0.35)");
|
|
4783
|
+
grad.addColorStop(1, "rgba(0,0,0,0)");
|
|
4784
|
+
g.fillStyle = grad;
|
|
4785
|
+
if (horiz) g.fillRect(0, at - p.shadowPx, ctx.W, 2 * p.shadowPx);
|
|
4786
|
+
else g.fillRect(at - p.shadowPx, 0, 2 * p.shadowPx, ctx.H);
|
|
4787
|
+
}
|
|
4788
|
+
g.globalAlpha = g.globalAlpha * p.opacity;
|
|
4789
|
+
g.fillStyle = color;
|
|
4790
|
+
const t = p.thicknessPx;
|
|
4791
|
+
if (horiz) g.fillRect(0, at - t / 2, ctx.W, t);
|
|
4792
|
+
else g.fillRect(at - t / 2, 0, t, ctx.H);
|
|
4793
|
+
g.restore();
|
|
4794
|
+
}
|
|
4795
|
+
};
|
|
4796
|
+
}
|
|
4797
|
+
var paneSeamFactory = {
|
|
4798
|
+
key: "pane-seam",
|
|
4799
|
+
create: paneSeamLayer,
|
|
4800
|
+
validateProps(props) {
|
|
4801
|
+
const errs = [];
|
|
4802
|
+
if (props == null || typeof props !== "object") return ["pane-seam: props must be an object"];
|
|
4803
|
+
const p = props;
|
|
4804
|
+
if (typeof p.atFrac !== "number" || !Number.isFinite(p.atFrac)) {
|
|
4805
|
+
errs.push("pane-seam.atFrac must be a finite number");
|
|
4806
|
+
} else if (p.atFrac < 0 || p.atFrac > 1) {
|
|
4807
|
+
errs.push("pane-seam.atFrac must be within 0..1");
|
|
4808
|
+
}
|
|
4809
|
+
if (p.orientation != null && p.orientation !== "h" && p.orientation !== "v") {
|
|
4810
|
+
errs.push('pane-seam.orientation must be "h" or "v"');
|
|
4811
|
+
}
|
|
4812
|
+
if (p.style != null && p.style !== "line" && p.style !== "shadow") {
|
|
4813
|
+
errs.push('pane-seam.style must be "line" or "shadow"');
|
|
4814
|
+
}
|
|
4815
|
+
for (const k of ["thicknessPx", "shadowPx", "opacity"]) {
|
|
4816
|
+
if (p[k] != null && (typeof p[k] !== "number" || !Number.isFinite(p[k]))) {
|
|
4817
|
+
errs.push(`pane-seam.${k} must be a finite number`);
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
return errs;
|
|
4821
|
+
}
|
|
4822
|
+
};
|
|
4823
|
+
|
|
4588
4824
|
// src/scene/layers/handIndicator.ts
|
|
4589
4825
|
function activeOpacity(notes, tMs, fadeMs) {
|
|
4590
4826
|
let lastEnd = -Infinity;
|
|
@@ -4759,6 +4995,8 @@ registerLayer(endCardFactory);
|
|
|
4759
4995
|
registerLayer(imageRevealFactory);
|
|
4760
4996
|
registerLayer(compositeRhythmFactory);
|
|
4761
4997
|
registerLayer(handIndicatorFactory);
|
|
4998
|
+
registerLayer(videoSourceFactory);
|
|
4999
|
+
registerLayer(paneSeamFactory);
|
|
4762
5000
|
|
|
4763
5001
|
// src/scene/audioLayers.ts
|
|
4764
5002
|
function countInSchedule(opts) {
|
|
@@ -4879,6 +5117,63 @@ function identityCamera(W, H) {
|
|
|
4879
5117
|
return { cx: W / 2, cy: H / 2, zoom: 1 };
|
|
4880
5118
|
}
|
|
4881
5119
|
|
|
5120
|
+
// src/scene/region.ts
|
|
5121
|
+
var FULL_REGION = { x: 0, y: 0, w: 1, h: 1 };
|
|
5122
|
+
function isFullRegion(r) {
|
|
5123
|
+
return r.x === 0 && r.y === 0 && r.w === 1 && r.h === 1;
|
|
5124
|
+
}
|
|
5125
|
+
function regionRect(r, W, H) {
|
|
5126
|
+
return { x: r.x * W, y: r.y * H, w: r.w * W, h: r.h * H };
|
|
5127
|
+
}
|
|
5128
|
+
function validateRegion(r) {
|
|
5129
|
+
const errs = [];
|
|
5130
|
+
if (typeof r !== "object" || r === null) return ["region must be an object"];
|
|
5131
|
+
const o = r;
|
|
5132
|
+
for (const k of ["x", "y", "w", "h"]) {
|
|
5133
|
+
const v = o[k];
|
|
5134
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
5135
|
+
errs.push(`region.${k} must be a finite number`);
|
|
5136
|
+
}
|
|
5137
|
+
}
|
|
5138
|
+
if (errs.length) return errs;
|
|
5139
|
+
const { x, y, w, h } = o;
|
|
5140
|
+
if (w <= 0 || h <= 0) errs.push("region.w and region.h must be > 0");
|
|
5141
|
+
if (x < 0 || y < 0) errs.push("region.x and region.y must be >= 0");
|
|
5142
|
+
if (x + w > 1 + 1e-9 || y + h > 1 + 1e-9) errs.push("region must fit inside the frame (x+w <= 1, y+h <= 1)");
|
|
5143
|
+
return errs;
|
|
5144
|
+
}
|
|
5145
|
+
function paneSafeBox(r, W, H) {
|
|
5146
|
+
const frame = safeBox(W, H);
|
|
5147
|
+
const px2 = r.x * W;
|
|
5148
|
+
const py = r.y * H;
|
|
5149
|
+
const pw = r.w * W;
|
|
5150
|
+
const ph = r.h * H;
|
|
5151
|
+
const left = Math.min(Math.max(frame.left, px2), px2 + pw) - px2;
|
|
5152
|
+
const right = Math.max(Math.min(frame.right, px2 + pw), px2) - px2;
|
|
5153
|
+
const top = Math.min(Math.max(frame.top, py), py + ph) - py;
|
|
5154
|
+
const bottom = Math.max(Math.min(frame.bottom, py + ph), py) - py;
|
|
5155
|
+
const w = Math.max(0, right - left);
|
|
5156
|
+
const h = Math.max(0, bottom - top);
|
|
5157
|
+
const r2 = left + w;
|
|
5158
|
+
const b2 = top + h;
|
|
5159
|
+
return {
|
|
5160
|
+
left,
|
|
5161
|
+
right: r2,
|
|
5162
|
+
top,
|
|
5163
|
+
bottom: b2,
|
|
5164
|
+
w,
|
|
5165
|
+
h,
|
|
5166
|
+
// Same expression shape as safeBox() so a full-frame region is bit-identical
|
|
5167
|
+
// to the frame box (left + w/2 differs in the last float ulp).
|
|
5168
|
+
cx: (left + r2) / 2,
|
|
5169
|
+
cy: (top + b2) / 2,
|
|
5170
|
+
// Mirrors the frame version, but centered on the PANE's midline (pw/2) —
|
|
5171
|
+
// a layer centering content inside a pane centers on the pane, not on the
|
|
5172
|
+
// frame, and the pane may be horizontally offset.
|
|
5173
|
+
centeredW: Math.max(0, 2 * Math.min(pw / 2 - left, left + w - pw / 2))
|
|
5174
|
+
};
|
|
5175
|
+
}
|
|
5176
|
+
|
|
4882
5177
|
// src/scene/runner.ts
|
|
4883
5178
|
function resolveAnchor(anchor, totalSec) {
|
|
4884
5179
|
if (typeof anchor === "number") return anchor;
|
|
@@ -4921,7 +5216,10 @@ var SCREEN_PINNED_KEYS = /* @__PURE__ */ new Set([
|
|
|
4921
5216
|
"particle-burst",
|
|
4922
5217
|
"texture",
|
|
4923
5218
|
"stat-counter",
|
|
4924
|
-
"end-card"
|
|
5219
|
+
"end-card",
|
|
5220
|
+
// S7c — the seam belongs to the FRAME, between the panes, so it must not pan
|
|
5221
|
+
// with the camera the way a pane's contents do.
|
|
5222
|
+
"pane-seam"
|
|
4925
5223
|
]);
|
|
4926
5224
|
function defaultBufferFactory() {
|
|
4927
5225
|
const g = globalThis;
|
|
@@ -4972,14 +5270,33 @@ async function buildScene(opts) {
|
|
|
4972
5270
|
if (errs.length) {
|
|
4973
5271
|
throw new Error(`buildScene: invalid props for "${sl.k}": ${errs.join("; ")}`);
|
|
4974
5272
|
}
|
|
5273
|
+
let region;
|
|
5274
|
+
let base = baseCtx;
|
|
5275
|
+
if (sl.region) {
|
|
5276
|
+
const rErrs = validateRegion(sl.region);
|
|
5277
|
+
if (rErrs.length) {
|
|
5278
|
+
throw new Error(`buildScene: invalid region for "${sl.k}": ${rErrs.join("; ")}`);
|
|
5279
|
+
}
|
|
5280
|
+
if (!isFullRegion(sl.region)) {
|
|
5281
|
+
region = sl.region;
|
|
5282
|
+
base = {
|
|
5283
|
+
...baseCtx,
|
|
5284
|
+
W: region.w * W,
|
|
5285
|
+
H: region.h * H,
|
|
5286
|
+
safeBox: paneSafeBox(region, W, H)
|
|
5287
|
+
};
|
|
5288
|
+
}
|
|
5289
|
+
}
|
|
4975
5290
|
const layer = factory.create();
|
|
4976
|
-
await layer.init({ ...
|
|
5291
|
+
await layer.init({ ...base, ctx2d: null }, sl.p ?? {});
|
|
4977
5292
|
bound.push({
|
|
4978
5293
|
layer,
|
|
4979
5294
|
startMs: seg.startMs,
|
|
4980
5295
|
endMs: seg.endMs,
|
|
4981
5296
|
segIndex,
|
|
4982
|
-
screenPinned: SCREEN_PINNED_KEYS.has(sl.k)
|
|
5297
|
+
screenPinned: SCREEN_PINNED_KEYS.has(sl.k),
|
|
5298
|
+
region,
|
|
5299
|
+
base
|
|
4983
5300
|
});
|
|
4984
5301
|
}
|
|
4985
5302
|
}
|
|
@@ -5060,6 +5377,20 @@ async function buildScene(opts) {
|
|
|
5060
5377
|
}
|
|
5061
5378
|
return plan;
|
|
5062
5379
|
}
|
|
5380
|
+
function drawBound(target, b, drawTMs, sharedCtx) {
|
|
5381
|
+
if (!b.region) {
|
|
5382
|
+
b.layer.draw(sharedCtx, drawTMs);
|
|
5383
|
+
return;
|
|
5384
|
+
}
|
|
5385
|
+
const r = regionRect(b.region, W, H);
|
|
5386
|
+
target.save();
|
|
5387
|
+
target.beginPath();
|
|
5388
|
+
target.rect(r.x, r.y, r.w, r.h);
|
|
5389
|
+
target.clip();
|
|
5390
|
+
target.translate(r.x, r.y);
|
|
5391
|
+
b.layer.draw({ ...b.base, ctx2d: target }, drawTMs);
|
|
5392
|
+
target.restore();
|
|
5393
|
+
}
|
|
5063
5394
|
function drawSegmentInto(target, segIndex, drawTMs) {
|
|
5064
5395
|
const ctx = { ...baseCtx, ctx2d: target };
|
|
5065
5396
|
const cam = camera(drawTMs);
|
|
@@ -5068,7 +5399,7 @@ async function buildScene(opts) {
|
|
|
5068
5399
|
for (const b of bound) {
|
|
5069
5400
|
if (b.segIndex !== segIndex || b.screenPinned) continue;
|
|
5070
5401
|
if (drawTMs < b.startMs || drawTMs >= b.endMs) continue;
|
|
5071
|
-
|
|
5402
|
+
drawBound(target, b, drawTMs, ctx);
|
|
5072
5403
|
}
|
|
5073
5404
|
target.restore();
|
|
5074
5405
|
target.save();
|
|
@@ -5076,7 +5407,7 @@ async function buildScene(opts) {
|
|
|
5076
5407
|
for (const b of bound) {
|
|
5077
5408
|
if (b.segIndex !== segIndex || !b.screenPinned) continue;
|
|
5078
5409
|
if (drawTMs < b.startMs || drawTMs >= b.endMs) continue;
|
|
5079
|
-
|
|
5410
|
+
drawBound(target, b, drawTMs, ctx);
|
|
5080
5411
|
}
|
|
5081
5412
|
target.restore();
|
|
5082
5413
|
}
|
|
@@ -5116,7 +5447,7 @@ async function buildScene(opts) {
|
|
|
5116
5447
|
for (const b of bound) {
|
|
5117
5448
|
if (b.screenPinned) continue;
|
|
5118
5449
|
if (tMs < b.startMs || tMs >= b.endMs) continue;
|
|
5119
|
-
|
|
5450
|
+
drawBound(ctx2d, b, tMs, ctx);
|
|
5120
5451
|
}
|
|
5121
5452
|
ctx2d.restore();
|
|
5122
5453
|
ctx2d.save();
|
|
@@ -5124,7 +5455,7 @@ async function buildScene(opts) {
|
|
|
5124
5455
|
for (const b of bound) {
|
|
5125
5456
|
if (!b.screenPinned) continue;
|
|
5126
5457
|
if (tMs < b.startMs || tMs >= b.endMs) continue;
|
|
5127
|
-
|
|
5458
|
+
drawBound(ctx2d, b, tMs, ctx);
|
|
5128
5459
|
}
|
|
5129
5460
|
ctx2d.restore();
|
|
5130
5461
|
return;
|
|
@@ -5186,6 +5517,345 @@ async function recordSceneSpec(opts) {
|
|
|
5186
5517
|
});
|
|
5187
5518
|
}
|
|
5188
5519
|
|
|
5520
|
+
// src/scene/presets.ts
|
|
5521
|
+
var DEFAULT_HOOK_CENTER_FRAC = SAFE_ZONE.top + 0.06;
|
|
5522
|
+
var DEFAULTS14 = {
|
|
5523
|
+
split: 0.42,
|
|
5524
|
+
durationSec: 8,
|
|
5525
|
+
seam: "line",
|
|
5526
|
+
size: [1080, 1920],
|
|
5527
|
+
theme: "default",
|
|
5528
|
+
fps: 30,
|
|
5529
|
+
hookFontPx: 84
|
|
5530
|
+
};
|
|
5531
|
+
function reactionSplitSpec(o) {
|
|
5532
|
+
const split = o.split ?? DEFAULTS14.split;
|
|
5533
|
+
if (!(split > 0 && split < 1)) {
|
|
5534
|
+
throw new Error(`reactionSplitSpec: split must be strictly between 0 and 1 (got ${split})`);
|
|
5535
|
+
}
|
|
5536
|
+
if (!o.app.length) {
|
|
5537
|
+
throw new Error("reactionSplitSpec: `app` must contain at least one layer");
|
|
5538
|
+
}
|
|
5539
|
+
const durationSec = o.durationSec ?? DEFAULTS14.durationSec;
|
|
5540
|
+
const seam = o.seam ?? DEFAULTS14.seam;
|
|
5541
|
+
const topPane = { x: 0, y: 0, w: 1, h: split };
|
|
5542
|
+
const bottomPane = { x: 0, y: split, w: 1, h: 1 - split };
|
|
5543
|
+
const layers = [
|
|
5544
|
+
{ k: "video-source", region: topPane, p: o.reaction },
|
|
5545
|
+
...o.app.map((l) => ({ ...l, region: l.region ?? bottomPane }))
|
|
5546
|
+
];
|
|
5547
|
+
if (seam !== "none") {
|
|
5548
|
+
layers.push({ k: "pane-seam", p: { atFrac: split, orientation: "h", style: seam } });
|
|
5549
|
+
}
|
|
5550
|
+
layers.push({
|
|
5551
|
+
k: "kinetic-text",
|
|
5552
|
+
p: {
|
|
5553
|
+
text: o.hook,
|
|
5554
|
+
mode: "word-pop",
|
|
5555
|
+
startMs: 0,
|
|
5556
|
+
revealMs: 900,
|
|
5557
|
+
fontPx: o.hookFontPx ?? DEFAULTS14.hookFontPx,
|
|
5558
|
+
centerFrac: o.hookCenterFrac ?? DEFAULT_HOOK_CENTER_FRAC,
|
|
5559
|
+
...o.hookColor ? { color: o.hookColor } : {}
|
|
5560
|
+
}
|
|
5561
|
+
});
|
|
5562
|
+
const timeline = [{ at: [0, "end"], layers }];
|
|
5563
|
+
if (o.endCard) {
|
|
5564
|
+
if (o.endCard.atSec < 0 || o.endCard.atSec >= durationSec) {
|
|
5565
|
+
throw new Error(
|
|
5566
|
+
`reactionSplitSpec: endCard.atSec ${o.endCard.atSec} must be within [0, ${durationSec})`
|
|
5567
|
+
);
|
|
5568
|
+
}
|
|
5569
|
+
timeline.push({
|
|
5570
|
+
at: [o.endCard.atSec, "end"],
|
|
5571
|
+
layers: [{
|
|
5572
|
+
k: "end-card",
|
|
5573
|
+
p: {
|
|
5574
|
+
...o.endCard.text ? { text: o.endCard.text } : {},
|
|
5575
|
+
...o.endCard.handle ? { handle: o.endCard.handle } : {}
|
|
5576
|
+
}
|
|
5577
|
+
}]
|
|
5578
|
+
});
|
|
5579
|
+
}
|
|
5580
|
+
return {
|
|
5581
|
+
size: o.size ?? DEFAULTS14.size,
|
|
5582
|
+
theme: o.theme ?? DEFAULTS14.theme,
|
|
5583
|
+
durationMode: "fixed",
|
|
5584
|
+
durationSec,
|
|
5585
|
+
fps: o.fps ?? DEFAULTS14.fps,
|
|
5586
|
+
timeline
|
|
5587
|
+
};
|
|
5588
|
+
}
|
|
5589
|
+
|
|
5590
|
+
// src/scene/reactions.ts
|
|
5591
|
+
var REACTION_EMOTIONS = [
|
|
5592
|
+
"confused",
|
|
5593
|
+
"shocked",
|
|
5594
|
+
"impressed",
|
|
5595
|
+
"delighted",
|
|
5596
|
+
"deadpan",
|
|
5597
|
+
"skeptical"
|
|
5598
|
+
];
|
|
5599
|
+
var DEFAULT_FOCUS = { x: 0.5, y: 0.38 };
|
|
5600
|
+
function isFrac(v) {
|
|
5601
|
+
return typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 1;
|
|
5602
|
+
}
|
|
5603
|
+
function nonEmptyString(v) {
|
|
5604
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
5605
|
+
}
|
|
5606
|
+
function validateReactionManifest(json) {
|
|
5607
|
+
const errs = [];
|
|
5608
|
+
if (json == null || typeof json !== "object") return ["manifest must be an object"];
|
|
5609
|
+
const m = json;
|
|
5610
|
+
if (!Array.isArray(m.clips)) return ["manifest.clips must be an array"];
|
|
5611
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5612
|
+
m.clips.forEach((raw, i) => {
|
|
5613
|
+
const at = `clips[${i}]`;
|
|
5614
|
+
if (raw == null || typeof raw !== "object") {
|
|
5615
|
+
errs.push(`${at} must be an object`);
|
|
5616
|
+
return;
|
|
5617
|
+
}
|
|
5618
|
+
const c = raw;
|
|
5619
|
+
if (!nonEmptyString(c.id)) errs.push(`${at}.id must be a non-empty string`);
|
|
5620
|
+
else if (seen.has(c.id)) errs.push(`${at}.id "${c.id}" is duplicated`);
|
|
5621
|
+
else seen.add(c.id);
|
|
5622
|
+
if (!nonEmptyString(c.file)) errs.push(`${at}.file must be a non-empty string`);
|
|
5623
|
+
if (!REACTION_EMOTIONS.includes(c.emotion)) {
|
|
5624
|
+
errs.push(`${at}.emotion must be one of ${REACTION_EMOTIONS.join(", ")}`);
|
|
5625
|
+
}
|
|
5626
|
+
if (typeof c.durationMs !== "number" || !Number.isFinite(c.durationMs) || c.durationMs <= 0) {
|
|
5627
|
+
errs.push(`${at}.durationMs must be a positive number`);
|
|
5628
|
+
}
|
|
5629
|
+
if (!nonEmptyString(c.source)) {
|
|
5630
|
+
errs.push(`${at}.source is required \u2014 record where this clip came from (no un-provenanced footage)`);
|
|
5631
|
+
}
|
|
5632
|
+
if (!nonEmptyString(c.licenseId)) {
|
|
5633
|
+
errs.push(`${at}.licenseId is required \u2014 record the licence/receipt proving the right to use it`);
|
|
5634
|
+
}
|
|
5635
|
+
if (c.acquiredAt != null && !/^\d{4}-\d{2}-\d{2}$/.test(String(c.acquiredAt))) {
|
|
5636
|
+
errs.push(`${at}.acquiredAt must be an ISO date (YYYY-MM-DD)`);
|
|
5637
|
+
}
|
|
5638
|
+
if (c.faceBox != null) {
|
|
5639
|
+
const f = c.faceBox;
|
|
5640
|
+
if (!Array.isArray(f) || f.length !== 4 || !f.every(isFrac)) {
|
|
5641
|
+
errs.push(`${at}.faceBox must be [x, y, w, h] fractions within 0..1`);
|
|
5642
|
+
} else {
|
|
5643
|
+
const [x, y, w, h] = f;
|
|
5644
|
+
if (w <= 0 || h <= 0) errs.push(`${at}.faceBox width/height must be > 0`);
|
|
5645
|
+
if (x + w > 1 + 1e-9 || y + h > 1 + 1e-9) errs.push(`${at}.faceBox must fit inside the source`);
|
|
5646
|
+
}
|
|
5647
|
+
}
|
|
5648
|
+
});
|
|
5649
|
+
return errs;
|
|
5650
|
+
}
|
|
5651
|
+
function assertReactionManifest(json) {
|
|
5652
|
+
const errs = validateReactionManifest(json);
|
|
5653
|
+
if (errs.length) throw new Error(`reaction manifest invalid:
|
|
5654
|
+
- ${errs.join("\n - ")}`);
|
|
5655
|
+
return json;
|
|
5656
|
+
}
|
|
5657
|
+
function clipFocus(clip) {
|
|
5658
|
+
if (!clip.faceBox) return { ...DEFAULT_FOCUS };
|
|
5659
|
+
const [x, y, w, h] = clip.faceBox;
|
|
5660
|
+
return { x: x + w / 2, y: y + h / 2 };
|
|
5661
|
+
}
|
|
5662
|
+
function reactionProps(clip, o = {}) {
|
|
5663
|
+
return {
|
|
5664
|
+
src: `${o.baseUrl ?? ""}${clip.file}`,
|
|
5665
|
+
fit: "cover",
|
|
5666
|
+
focus: clipFocus(clip),
|
|
5667
|
+
startMs: o.startMs ?? 0,
|
|
5668
|
+
sourceStartMs: o.sourceStartMs ?? 0,
|
|
5669
|
+
loop: o.loop ?? true,
|
|
5670
|
+
rate: o.rate ?? 1,
|
|
5671
|
+
durationMs: clip.durationMs
|
|
5672
|
+
};
|
|
5673
|
+
}
|
|
5674
|
+
function pickClips(m, emotion) {
|
|
5675
|
+
return emotion ? m.clips.filter((c) => c.emotion === emotion) : [...m.clips];
|
|
5676
|
+
}
|
|
5677
|
+
|
|
5678
|
+
// src/scene/paneChecks.ts
|
|
5679
|
+
var PAPER_LUM_FLOOR = 200;
|
|
5680
|
+
function px(f, x, y) {
|
|
5681
|
+
const i = (y * f.w + x) * 4;
|
|
5682
|
+
return [f.data[i], f.data[i + 1], f.data[i + 2]];
|
|
5683
|
+
}
|
|
5684
|
+
function lum(r, g, b) {
|
|
5685
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
5686
|
+
}
|
|
5687
|
+
function inkFractionInRow(f, y, x0, x1) {
|
|
5688
|
+
const n = x1 - x0;
|
|
5689
|
+
if (n <= 0 || y < 0 || y >= f.h) return 0;
|
|
5690
|
+
let ink = 0;
|
|
5691
|
+
const lo = Math.max(0, x0);
|
|
5692
|
+
const hi = Math.min(f.w, x1);
|
|
5693
|
+
for (let x = lo; x < hi; x++) {
|
|
5694
|
+
const [r, g, b] = px(f, x, y);
|
|
5695
|
+
const chroma = Math.max(r, g, b) - Math.min(r, g, b);
|
|
5696
|
+
if (lum(r, g, b) < PAPER_LUM_FLOOR - 30 || chroma > 40) ink++;
|
|
5697
|
+
}
|
|
5698
|
+
return ink / n;
|
|
5699
|
+
}
|
|
5700
|
+
function checkPaneSafety(frames, o) {
|
|
5701
|
+
const srcW = o.srcW ?? 1080;
|
|
5702
|
+
const srcH = o.srcH ?? 1920;
|
|
5703
|
+
const maxInkFrac = o.maxInkFrac ?? 0.08;
|
|
5704
|
+
const frame = safeBox(srcW, srcH);
|
|
5705
|
+
const pane = regionRect(o.region, srcW, srcH);
|
|
5706
|
+
const y0Src = Math.max(frame.bottom, pane.y);
|
|
5707
|
+
const y1Src = pane.y + pane.h;
|
|
5708
|
+
const name = "PANE SAFE ZONE";
|
|
5709
|
+
if (y1Src <= y0Src) {
|
|
5710
|
+
return { name, pass: true, detail: "pane ends above the frame bottom-safe line (nothing to check)" };
|
|
5711
|
+
}
|
|
5712
|
+
const sy = frames.frameH / srcH;
|
|
5713
|
+
const sx = frames.frameW / srcW;
|
|
5714
|
+
const fy0 = Math.round(y0Src * sy);
|
|
5715
|
+
const fy1 = Math.round(y1Src * sy);
|
|
5716
|
+
const x0 = Math.round(pane.x * sx);
|
|
5717
|
+
const x1 = Math.round((pane.x + pane.w) * sx);
|
|
5718
|
+
const offenders = [];
|
|
5719
|
+
for (const { tSec, f } of frames.show) {
|
|
5720
|
+
let worst = 0;
|
|
5721
|
+
for (let y = fy0; y < Math.min(fy1, f.h); y++) {
|
|
5722
|
+
worst = Math.max(worst, inkFractionInRow(f, y, x0, x1));
|
|
5723
|
+
}
|
|
5724
|
+
if (worst > maxInkFrac) offenders.push({ tSec: +tSec.toFixed(2), inkFrac: +worst.toFixed(3) });
|
|
5725
|
+
}
|
|
5726
|
+
const pass = offenders.length === 0;
|
|
5727
|
+
return {
|
|
5728
|
+
name,
|
|
5729
|
+
pass,
|
|
5730
|
+
detail: pass ? `no pane content below the frame bottom-safe line (y=${Math.round(frame.bottom)}) across ${frames.show.length} frames` : `pane content under the platform caption/action rail in ${offenders.length} samples: ${JSON.stringify(offenders.slice(0, 6))}`
|
|
5731
|
+
};
|
|
5732
|
+
}
|
|
5733
|
+
function checkSeamPresent(frames, o) {
|
|
5734
|
+
const srcH = o.srcH ?? 1920;
|
|
5735
|
+
const tolPx = o.tolPx ?? 6;
|
|
5736
|
+
const minSpan = o.minSpan ?? 0.9;
|
|
5737
|
+
const sy = frames.frameH / srcH;
|
|
5738
|
+
const centre = Math.round(o.splitFrac * srcH * sy);
|
|
5739
|
+
const band = Math.max(1, Math.round(tolPx * sy));
|
|
5740
|
+
const missing = [];
|
|
5741
|
+
for (const { tSec, f } of frames.show) {
|
|
5742
|
+
let best = 0;
|
|
5743
|
+
for (let y = centre - band; y <= centre + band; y++) {
|
|
5744
|
+
best = Math.max(best, inkFractionInRow(f, y, 0, f.w));
|
|
5745
|
+
}
|
|
5746
|
+
if (best < minSpan) missing.push(+tSec.toFixed(2));
|
|
5747
|
+
}
|
|
5748
|
+
const pass = frames.show.length > 0 && missing.length === 0;
|
|
5749
|
+
return {
|
|
5750
|
+
name: "PANE SEAM PRESENT",
|
|
5751
|
+
pass,
|
|
5752
|
+
detail: frames.show.length === 0 ? "no frames sampled (cannot verify)" : pass ? `seam rule found within ${tolPx}px of y=${o.splitFrac * srcH | 0} in all ${frames.show.length} frames` : `seam missing/broken at t=${JSON.stringify(missing.slice(0, 6))} \u2014 did the split collapse?`
|
|
5753
|
+
};
|
|
5754
|
+
}
|
|
5755
|
+
function faceLandsInPane(o) {
|
|
5756
|
+
const frameW = o.frameW ?? 1080;
|
|
5757
|
+
const frameH = o.frameH ?? 1920;
|
|
5758
|
+
const pane = regionRect(o.region, frameW, frameH);
|
|
5759
|
+
const b = fitBoxes(o.srcW, o.srcH, pane.w, pane.h, o.fit ?? "cover", o.focus);
|
|
5760
|
+
const [fx, fy, fw, fh] = o.faceBox;
|
|
5761
|
+
const faceSrc = { x: fx * o.srcW, y: fy * o.srcH, w: fw * o.srcW, h: fh * o.srcH };
|
|
5762
|
+
const ix0 = Math.max(faceSrc.x, b.sx);
|
|
5763
|
+
const iy0 = Math.max(faceSrc.y, b.sy);
|
|
5764
|
+
const ix1 = Math.min(faceSrc.x + faceSrc.w, b.sx + b.sw);
|
|
5765
|
+
const iy1 = Math.min(faceSrc.y + faceSrc.h, b.sy + b.sh);
|
|
5766
|
+
const iw = Math.max(0, ix1 - ix0);
|
|
5767
|
+
const ih = Math.max(0, iy1 - iy0);
|
|
5768
|
+
const visibleFrac = faceSrc.w * faceSrc.h > 0 ? iw * ih / (faceSrc.w * faceSrc.h) : 0;
|
|
5769
|
+
if (iw <= 0 || ih <= 0) {
|
|
5770
|
+
return {
|
|
5771
|
+
ok: false,
|
|
5772
|
+
visibleFrac: 0,
|
|
5773
|
+
frameRect: { x: 0, y: 0, w: 0, h: 0 },
|
|
5774
|
+
reason: "the declared face is entirely outside the cover crop"
|
|
5775
|
+
};
|
|
5776
|
+
}
|
|
5777
|
+
const kx = b.dw / b.sw;
|
|
5778
|
+
const ky = b.dh / b.sh;
|
|
5779
|
+
const frameRect2 = {
|
|
5780
|
+
x: pane.x + b.dx + (ix0 - b.sx) * kx,
|
|
5781
|
+
y: pane.y + b.dy + (iy0 - b.sy) * ky,
|
|
5782
|
+
w: iw * kx,
|
|
5783
|
+
h: ih * ky
|
|
5784
|
+
};
|
|
5785
|
+
const ok = visibleFrac > 0.999;
|
|
5786
|
+
return {
|
|
5787
|
+
ok,
|
|
5788
|
+
visibleFrac,
|
|
5789
|
+
frameRect: frameRect2,
|
|
5790
|
+
...ok ? {} : { reason: `only ${(visibleFrac * 100).toFixed(1)}% of the declared face survives the crop` }
|
|
5791
|
+
};
|
|
5792
|
+
}
|
|
5793
|
+
|
|
5794
|
+
// src/scene/variants.ts
|
|
5795
|
+
function gcd(a, b) {
|
|
5796
|
+
while (b) {
|
|
5797
|
+
const t = a % b;
|
|
5798
|
+
a = b;
|
|
5799
|
+
b = t;
|
|
5800
|
+
}
|
|
5801
|
+
return a;
|
|
5802
|
+
}
|
|
5803
|
+
function coprimeStride(total) {
|
|
5804
|
+
if (total <= 2) return 1;
|
|
5805
|
+
const start = Math.max(1, Math.round(total * 0.6180339887498949));
|
|
5806
|
+
for (let s = start; s < start + total; s++) {
|
|
5807
|
+
const c = (s - 1) % (total - 1) + 1;
|
|
5808
|
+
if (gcd(c, total) === 1) return c;
|
|
5809
|
+
}
|
|
5810
|
+
return 1;
|
|
5811
|
+
}
|
|
5812
|
+
function variantIndices(n, sizes, seed = 0) {
|
|
5813
|
+
const total = sizes.reduce((a, b) => a * b, 1);
|
|
5814
|
+
if (total <= 0 || n <= 0) return [];
|
|
5815
|
+
const stride = coprimeStride(total);
|
|
5816
|
+
const out = [];
|
|
5817
|
+
for (let k = 0; k < n; k++) {
|
|
5818
|
+
let flat = ((seed + k * stride) % total + total) % total;
|
|
5819
|
+
const tuple = [];
|
|
5820
|
+
for (let a = sizes.length - 1; a >= 0; a--) {
|
|
5821
|
+
tuple[a] = flat % sizes[a];
|
|
5822
|
+
flat = Math.floor(flat / sizes[a]);
|
|
5823
|
+
}
|
|
5824
|
+
out.push(tuple);
|
|
5825
|
+
}
|
|
5826
|
+
return out;
|
|
5827
|
+
}
|
|
5828
|
+
function slugify(s, max = 32) {
|
|
5829
|
+
const base = s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
5830
|
+
return (base || "hook").slice(0, max).replace(/-+$/, "");
|
|
5831
|
+
}
|
|
5832
|
+
function reactionVariants(o) {
|
|
5833
|
+
if (!o.hooks.length) throw new Error("reactionVariants: `hooks` must not be empty");
|
|
5834
|
+
if (!o.clips.length) throw new Error("reactionVariants: `clips` must not be empty");
|
|
5835
|
+
const endings = o.endings?.length ? o.endings : [null];
|
|
5836
|
+
const sizes = [o.hooks.length, o.clips.length, endings.length];
|
|
5837
|
+
const total = sizes.reduce((a, b) => a * b, 1);
|
|
5838
|
+
const n = o.n ?? total;
|
|
5839
|
+
return variantIndices(n, sizes, o.seed ?? 0).map(([hi, ci, ei]) => {
|
|
5840
|
+
const hook = o.hooks[hi];
|
|
5841
|
+
const clip = o.clips[ci];
|
|
5842
|
+
const ending = endings[ei];
|
|
5843
|
+
const spec = reactionSplitSpec({
|
|
5844
|
+
...o.base,
|
|
5845
|
+
hook,
|
|
5846
|
+
reaction: reactionProps(clip, { baseUrl: o.baseUrl }),
|
|
5847
|
+
...ending ? { endCard: ending } : {}
|
|
5848
|
+
});
|
|
5849
|
+
return {
|
|
5850
|
+
id: `${clip.id}--${slugify(hook)}--e${ei}`,
|
|
5851
|
+
hook,
|
|
5852
|
+
clipId: clip.id,
|
|
5853
|
+
endingIndex: ei,
|
|
5854
|
+
spec
|
|
5855
|
+
};
|
|
5856
|
+
});
|
|
5857
|
+
}
|
|
5858
|
+
|
|
5189
5859
|
// src/scene/gate.ts
|
|
5190
5860
|
var AV_TOLERANCE_MS = 60;
|
|
5191
5861
|
async function runGate(input) {
|
|
@@ -5200,6 +5870,11 @@ async function runGate(input) {
|
|
|
5200
5870
|
}
|
|
5201
5871
|
const errs = factory.validateProps(sl.p ?? {});
|
|
5202
5872
|
for (const e of errs) errors.push({ check: "spec", message: e });
|
|
5873
|
+
if (sl.region) {
|
|
5874
|
+
for (const e of validateRegion(sl.region)) {
|
|
5875
|
+
errors.push({ check: "spec", message: `layer "${sl.k}": ${e}` });
|
|
5876
|
+
}
|
|
5877
|
+
}
|
|
5203
5878
|
}
|
|
5204
5879
|
}
|
|
5205
5880
|
try {
|
|
@@ -5281,12 +5956,12 @@ function mapBoxThroughLayout(base, b) {
|
|
|
5281
5956
|
};
|
|
5282
5957
|
}
|
|
5283
5958
|
function followSrcBox(rn, focusBoxCanvas) {
|
|
5284
|
-
const
|
|
5959
|
+
const px2 = focusBoxCanvas.w * (FOLLOW_PAD - 1) / 2;
|
|
5285
5960
|
const py = focusBoxCanvas.h * (FOLLOW_PAD - 1) / 2;
|
|
5286
5961
|
const src = {
|
|
5287
|
-
x: Math.max(0, focusBoxCanvas.x -
|
|
5962
|
+
x: Math.max(0, focusBoxCanvas.x - px2),
|
|
5288
5963
|
y: Math.max(0, focusBoxCanvas.y - py),
|
|
5289
|
-
w: focusBoxCanvas.w + 2 *
|
|
5964
|
+
w: focusBoxCanvas.w + 2 * px2,
|
|
5290
5965
|
h: focusBoxCanvas.h + 2 * py
|
|
5291
5966
|
};
|
|
5292
5967
|
src.w = Math.min(src.w, rn.canvas.width - src.x);
|
|
@@ -5503,13 +6178,18 @@ function contourMinimapDemoSpec(opts = {}) {
|
|
|
5503
6178
|
};
|
|
5504
6179
|
}
|
|
5505
6180
|
export {
|
|
6181
|
+
DEFAULT_FOCUS,
|
|
5506
6182
|
DEFAULT_FUNCTION_COLORS,
|
|
6183
|
+
DEFAULT_HOOK_CENTER_FRAC,
|
|
5507
6184
|
FIFTHS_MAJOR,
|
|
5508
6185
|
FIFTHS_MINOR,
|
|
5509
6186
|
FOLLOW_BARS,
|
|
5510
6187
|
FOLLOW_PAD,
|
|
6188
|
+
FULL_REGION,
|
|
6189
|
+
PAPER_LUM_FLOOR,
|
|
5511
6190
|
PIANO_HIGH,
|
|
5512
6191
|
PIANO_LOW,
|
|
6192
|
+
REACTION_EMOTIONS,
|
|
5513
6193
|
SCALE_INTERVALS,
|
|
5514
6194
|
activeChord,
|
|
5515
6195
|
activeCue,
|
|
@@ -5518,6 +6198,7 @@ export {
|
|
|
5518
6198
|
applySchedule,
|
|
5519
6199
|
applyToContext,
|
|
5520
6200
|
assertGate,
|
|
6201
|
+
assertReactionManifest,
|
|
5521
6202
|
audioPlayheadLine,
|
|
5522
6203
|
ballArc,
|
|
5523
6204
|
ballX,
|
|
@@ -5530,15 +6211,19 @@ export {
|
|
|
5530
6211
|
buildScene,
|
|
5531
6212
|
cameraForFollow,
|
|
5532
6213
|
cameraTransform,
|
|
6214
|
+
checkPaneSafety,
|
|
6215
|
+
checkSeamPresent,
|
|
5533
6216
|
chordRibbonFactory,
|
|
5534
6217
|
circleOfFifthsDemoSpec,
|
|
5535
6218
|
circleOfFifthsFactory,
|
|
5536
6219
|
clamp,
|
|
5537
6220
|
clickTrackSchedule,
|
|
6221
|
+
clipFocus,
|
|
5538
6222
|
compositeRhythmFactory,
|
|
5539
6223
|
contourMinimapDemoSpec,
|
|
5540
6224
|
contourPoints,
|
|
5541
6225
|
contourPolyline,
|
|
6226
|
+
coprimeStride,
|
|
5542
6227
|
countInLeadSec,
|
|
5543
6228
|
countInSchedule,
|
|
5544
6229
|
countdownFactory,
|
|
@@ -5563,10 +6248,12 @@ export {
|
|
|
5563
6248
|
easeInOut,
|
|
5564
6249
|
easeOut,
|
|
5565
6250
|
endCardFactory,
|
|
6251
|
+
faceLandsInPane,
|
|
5566
6252
|
fallingKeyboardDemoScore,
|
|
5567
6253
|
fallingKeyboardDemoSpec,
|
|
5568
6254
|
fallingNotesFactory,
|
|
5569
6255
|
firstMeasureBox,
|
|
6256
|
+
fitBoxes,
|
|
5570
6257
|
followBoxAt,
|
|
5571
6258
|
followSrcBox,
|
|
5572
6259
|
followWindowStart,
|
|
@@ -5585,9 +6272,11 @@ export {
|
|
|
5585
6272
|
identityCamera,
|
|
5586
6273
|
imageRevealFactory,
|
|
5587
6274
|
inRange,
|
|
6275
|
+
inkFractionInRow,
|
|
5588
6276
|
intervalArcsFactory,
|
|
5589
6277
|
invLerp,
|
|
5590
6278
|
isBlackKey,
|
|
6279
|
+
isFullRegion,
|
|
5591
6280
|
karaokeCaptionFactory,
|
|
5592
6281
|
kenBurns,
|
|
5593
6282
|
keyCenterX,
|
|
@@ -5611,14 +6300,18 @@ export {
|
|
|
5611
6300
|
measureSystemMap,
|
|
5612
6301
|
timeToX as minimapTimeToX,
|
|
5613
6302
|
msPerBeat,
|
|
6303
|
+
needsSeek,
|
|
5614
6304
|
notationFactory,
|
|
5615
6305
|
notationLayout,
|
|
5616
6306
|
noteColor,
|
|
5617
6307
|
noteSetXRange,
|
|
6308
|
+
paneSafeBox,
|
|
6309
|
+
paneSeamFactory,
|
|
5618
6310
|
parseKey,
|
|
5619
6311
|
parseTimeSig,
|
|
5620
6312
|
particleBurstFactory,
|
|
5621
6313
|
pcToSlot,
|
|
6314
|
+
pickClips,
|
|
5622
6315
|
pitchAt,
|
|
5623
6316
|
pitchContourFactory,
|
|
5624
6317
|
pitchRange,
|
|
@@ -5633,7 +6326,11 @@ export {
|
|
|
5633
6326
|
radialSpectrumFactory,
|
|
5634
6327
|
rayEndpoints,
|
|
5635
6328
|
rayPointAt,
|
|
6329
|
+
reactionProps,
|
|
6330
|
+
reactionSplitSpec,
|
|
6331
|
+
reactionVariants,
|
|
5636
6332
|
recordSceneSpec,
|
|
6333
|
+
regionRect,
|
|
5637
6334
|
registerLayer,
|
|
5638
6335
|
registeredKeys,
|
|
5639
6336
|
resolveAnchor,
|
|
@@ -5654,6 +6351,9 @@ export {
|
|
|
5654
6351
|
slotAngle,
|
|
5655
6352
|
slotPc,
|
|
5656
6353
|
slotPoint,
|
|
6354
|
+
slugify,
|
|
6355
|
+
sourceSize,
|
|
6356
|
+
sourceTimeMs,
|
|
5657
6357
|
spectrumFactory,
|
|
5658
6358
|
staffAnchor,
|
|
5659
6359
|
staffKeyboardRayFactory,
|
|
@@ -5665,7 +6365,11 @@ export {
|
|
|
5665
6365
|
transitionProgress,
|
|
5666
6366
|
validateChordTrack,
|
|
5667
6367
|
validateQuiz,
|
|
6368
|
+
validateReactionManifest,
|
|
6369
|
+
validateRegion,
|
|
5668
6370
|
validateSections,
|
|
6371
|
+
variantIndices,
|
|
6372
|
+
videoSourceFactory,
|
|
5669
6373
|
visualTimelineMs,
|
|
5670
6374
|
vstackAudioPlayheadLine,
|
|
5671
6375
|
vstackFollowBox,
|