@real-music-packages/web-core 0.23.0 → 0.24.1
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/audio.js +5 -3
- package/dist/audio.js.map +1 -1
- package/dist/chunk-25RUSM2X.js +31 -0
- package/dist/chunk-25RUSM2X.js.map +1 -0
- package/dist/{chunk-LLMDQM4C.js → chunk-5ZK4HVY4.js} +4 -28
- package/dist/chunk-5ZK4HVY4.js.map +1 -0
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/dist/scene/index.d.ts +90 -1
- package/dist/scene/index.js +429 -1
- package/dist/scene/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-LLMDQM4C.js.map +0 -1
package/dist/scene/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
intervalBySemitones
|
|
3
3
|
} from "../chunk-N56UTMWA.js";
|
|
4
|
+
import {
|
|
5
|
+
noteNameToIndex,
|
|
6
|
+
pitchClass
|
|
7
|
+
} from "../chunk-25RUSM2X.js";
|
|
4
8
|
import {
|
|
5
9
|
ctaScene,
|
|
6
10
|
drawSafeGuides,
|
|
@@ -3657,6 +3661,419 @@ var intervalArcsFactory = {
|
|
|
3657
3661
|
}
|
|
3658
3662
|
};
|
|
3659
3663
|
|
|
3664
|
+
// src/scene/layers/kineticText.ts
|
|
3665
|
+
var DEFAULTS5 = { mode: "word-pop", startMs: 0, revealMs: 1400, fontPx: 88, centerFrac: 0.42, cursor: true };
|
|
3666
|
+
function clamp014(x) {
|
|
3667
|
+
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
3668
|
+
}
|
|
3669
|
+
function wrap2(c, words, maxW) {
|
|
3670
|
+
const lines = [];
|
|
3671
|
+
let cur = [];
|
|
3672
|
+
let curW = 0;
|
|
3673
|
+
const spaceW = c.measureText(" ").width;
|
|
3674
|
+
for (const w of words) {
|
|
3675
|
+
const ww = c.measureText(w).width;
|
|
3676
|
+
const add = cur.length ? spaceW + ww : ww;
|
|
3677
|
+
if (cur.length && curW + add > maxW) {
|
|
3678
|
+
lines.push(cur);
|
|
3679
|
+
cur = [];
|
|
3680
|
+
curW = 0;
|
|
3681
|
+
}
|
|
3682
|
+
cur.push(w);
|
|
3683
|
+
curW += cur.length === 1 ? ww : spaceW + ww;
|
|
3684
|
+
}
|
|
3685
|
+
if (cur.length) lines.push(cur);
|
|
3686
|
+
return lines;
|
|
3687
|
+
}
|
|
3688
|
+
function kineticTextLayer() {
|
|
3689
|
+
let text = "";
|
|
3690
|
+
let mode = DEFAULTS5.mode;
|
|
3691
|
+
let startMs = DEFAULTS5.startMs;
|
|
3692
|
+
let revealMs = DEFAULTS5.revealMs;
|
|
3693
|
+
let fontPx = DEFAULTS5.fontPx;
|
|
3694
|
+
let color;
|
|
3695
|
+
let centerFrac = DEFAULTS5.centerFrac;
|
|
3696
|
+
let cursor = DEFAULTS5.cursor;
|
|
3697
|
+
return {
|
|
3698
|
+
key: "kinetic-text",
|
|
3699
|
+
init(_ctx, props) {
|
|
3700
|
+
text = props.text ?? "";
|
|
3701
|
+
mode = props.mode ?? DEFAULTS5.mode;
|
|
3702
|
+
startMs = props.startMs ?? DEFAULTS5.startMs;
|
|
3703
|
+
revealMs = props.revealMs ?? DEFAULTS5.revealMs;
|
|
3704
|
+
fontPx = props.fontPx ?? DEFAULTS5.fontPx;
|
|
3705
|
+
color = props.color;
|
|
3706
|
+
centerFrac = props.centerFrac ?? DEFAULTS5.centerFrac;
|
|
3707
|
+
cursor = props.cursor ?? DEFAULTS5.cursor;
|
|
3708
|
+
},
|
|
3709
|
+
draw(ctx, tMs) {
|
|
3710
|
+
if (!text) return;
|
|
3711
|
+
const c = ctx.ctx2d;
|
|
3712
|
+
const sb = ctx.safeBox;
|
|
3713
|
+
const col = color ?? ctx.theme.ink;
|
|
3714
|
+
c.save();
|
|
3715
|
+
c.font = `700 ${fontPx}px ${ctx.theme.fontDisplay}`;
|
|
3716
|
+
c.textBaseline = "middle";
|
|
3717
|
+
const rawLines = text.split("\n");
|
|
3718
|
+
const lines = [];
|
|
3719
|
+
for (const rl of rawLines) lines.push(...wrap2(c, rl.split(/\s+/).filter(Boolean), sb.w));
|
|
3720
|
+
const lineH = fontPx * 1.25;
|
|
3721
|
+
const totalH = lines.length * lineH;
|
|
3722
|
+
const top = ctx.H * centerFrac - totalH / 2 + lineH / 2;
|
|
3723
|
+
const p = clamp014((tMs - startMs) / Math.max(1, revealMs));
|
|
3724
|
+
if (mode === "typewriter") {
|
|
3725
|
+
const full = lines.map((l) => l.join(" "));
|
|
3726
|
+
const totalChars = full.reduce((n, l) => n + l.length, 0);
|
|
3727
|
+
let shown = Math.floor(p * totalChars);
|
|
3728
|
+
c.textAlign = "center";
|
|
3729
|
+
c.fillStyle = col;
|
|
3730
|
+
let y = top;
|
|
3731
|
+
let lastDrawn = { x: 0, y: 0, end: false };
|
|
3732
|
+
for (const line of full) {
|
|
3733
|
+
const take = Math.max(0, Math.min(line.length, shown));
|
|
3734
|
+
const sub = line.slice(0, take);
|
|
3735
|
+
if (sub) c.fillText(sub, ctx.W / 2, y);
|
|
3736
|
+
shown -= line.length;
|
|
3737
|
+
if (take < line.length && take >= 0 && !lastDrawn.end) {
|
|
3738
|
+
lastDrawn = { x: ctx.W / 2 + c.measureText(sub).width / 2, y, end: true };
|
|
3739
|
+
}
|
|
3740
|
+
y += lineH;
|
|
3741
|
+
}
|
|
3742
|
+
if (cursor && p < 1 && Math.floor(tMs / 450) % 2 === 0) {
|
|
3743
|
+
c.fillRect(lastDrawn.x + 6, lastDrawn.y - fontPx * 0.42, 6, fontPx * 0.8);
|
|
3744
|
+
}
|
|
3745
|
+
} else {
|
|
3746
|
+
const allWords = lines.flat();
|
|
3747
|
+
const n = allWords.length || 1;
|
|
3748
|
+
const stagger = revealMs / n;
|
|
3749
|
+
const POP = 280;
|
|
3750
|
+
c.textAlign = "left";
|
|
3751
|
+
const spaceW = c.measureText(" ").width;
|
|
3752
|
+
let wi = 0;
|
|
3753
|
+
let y = top;
|
|
3754
|
+
for (const line of lines) {
|
|
3755
|
+
const lineW = line.reduce((w, word, i) => w + c.measureText(word).width + (i ? spaceW : 0), 0);
|
|
3756
|
+
let x = sb.left + (sb.w - lineW) / 2;
|
|
3757
|
+
for (const word of line) {
|
|
3758
|
+
const ww = c.measureText(word).width;
|
|
3759
|
+
const wp = clamp014((tMs - startMs - wi * stagger) / POP);
|
|
3760
|
+
if (wp > 0) {
|
|
3761
|
+
const scale = 0.7 + 0.3 * wp;
|
|
3762
|
+
c.save();
|
|
3763
|
+
c.globalAlpha = wp;
|
|
3764
|
+
c.translate(x + ww / 2, y);
|
|
3765
|
+
c.scale(scale, scale);
|
|
3766
|
+
c.fillStyle = col;
|
|
3767
|
+
c.fillText(word, -ww / 2, 0);
|
|
3768
|
+
c.restore();
|
|
3769
|
+
}
|
|
3770
|
+
x += ww + spaceW;
|
|
3771
|
+
wi++;
|
|
3772
|
+
}
|
|
3773
|
+
y += lineH;
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
c.restore();
|
|
3777
|
+
}
|
|
3778
|
+
};
|
|
3779
|
+
}
|
|
3780
|
+
var kineticTextFactory = {
|
|
3781
|
+
key: "kinetic-text",
|
|
3782
|
+
create: kineticTextLayer,
|
|
3783
|
+
validateProps(props) {
|
|
3784
|
+
if (props == null || typeof props !== "object") return ["kinetic-text: props must be an object"];
|
|
3785
|
+
const p = props;
|
|
3786
|
+
const errs = [];
|
|
3787
|
+
if (typeof p.text !== "string" || !p.text) errs.push("kinetic-text.text must be a non-empty string");
|
|
3788
|
+
if (p.mode != null && p.mode !== "typewriter" && p.mode !== "word-pop") errs.push('kinetic-text.mode must be "typewriter" | "word-pop"');
|
|
3789
|
+
if (p.startMs != null && typeof p.startMs !== "number") errs.push("kinetic-text.startMs must be a number");
|
|
3790
|
+
if (p.revealMs != null && (typeof p.revealMs !== "number" || p.revealMs <= 0)) errs.push("kinetic-text.revealMs must be a positive number");
|
|
3791
|
+
if (p.fontPx != null && (typeof p.fontPx !== "number" || p.fontPx <= 0)) errs.push("kinetic-text.fontPx must be a positive number");
|
|
3792
|
+
if (p.color != null && typeof p.color !== "string") errs.push("kinetic-text.color must be a string");
|
|
3793
|
+
if (p.centerFrac != null && (typeof p.centerFrac !== "number" || p.centerFrac < 0 || p.centerFrac > 1)) errs.push("kinetic-text.centerFrac must be in [0,1]");
|
|
3794
|
+
if (p.cursor != null && typeof p.cursor !== "boolean") errs.push("kinetic-text.cursor must be a boolean");
|
|
3795
|
+
return errs;
|
|
3796
|
+
}
|
|
3797
|
+
};
|
|
3798
|
+
|
|
3799
|
+
// src/scene/layers/countdown.ts
|
|
3800
|
+
var DEFAULTS6 = { from: 3, startMs: 0, durationMs: 3e3, fontPx: 280, centerFrac: 0.46, ring: true };
|
|
3801
|
+
function rgba6(hex, a) {
|
|
3802
|
+
const h = hex.replace("#", "");
|
|
3803
|
+
const n = h.length === 3 ? parseInt(h.split("").map((c) => c + c).join(""), 16) : parseInt(h, 16);
|
|
3804
|
+
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
|
|
3805
|
+
}
|
|
3806
|
+
function countdownLayer() {
|
|
3807
|
+
let from = DEFAULTS6.from;
|
|
3808
|
+
let startMs = DEFAULTS6.startMs;
|
|
3809
|
+
let durationMs = DEFAULTS6.durationMs;
|
|
3810
|
+
let color;
|
|
3811
|
+
let fontPx = DEFAULTS6.fontPx;
|
|
3812
|
+
let centerFrac = DEFAULTS6.centerFrac;
|
|
3813
|
+
let ring = DEFAULTS6.ring;
|
|
3814
|
+
return {
|
|
3815
|
+
key: "countdown",
|
|
3816
|
+
init(_ctx, props) {
|
|
3817
|
+
from = props.from ?? DEFAULTS6.from;
|
|
3818
|
+
startMs = props.startMs ?? DEFAULTS6.startMs;
|
|
3819
|
+
durationMs = props.durationMs ?? DEFAULTS6.durationMs;
|
|
3820
|
+
color = props.color;
|
|
3821
|
+
fontPx = props.fontPx ?? DEFAULTS6.fontPx;
|
|
3822
|
+
centerFrac = props.centerFrac ?? DEFAULTS6.centerFrac;
|
|
3823
|
+
ring = props.ring ?? DEFAULTS6.ring;
|
|
3824
|
+
},
|
|
3825
|
+
draw(ctx, tMs) {
|
|
3826
|
+
const elapsed = tMs - startMs;
|
|
3827
|
+
if (elapsed < 0 || elapsed >= durationMs) return;
|
|
3828
|
+
const perTick = durationMs / from;
|
|
3829
|
+
const idx = Math.floor(elapsed / perTick);
|
|
3830
|
+
const n = from - idx;
|
|
3831
|
+
if (n < 1 || n > from) return;
|
|
3832
|
+
const tickElapsed = elapsed - idx * perTick;
|
|
3833
|
+
const tickFrac = tickElapsed / perTick;
|
|
3834
|
+
const c = ctx.ctx2d;
|
|
3835
|
+
const cx = ctx.W / 2;
|
|
3836
|
+
const cy = ctx.H * centerFrac;
|
|
3837
|
+
const col = color ?? ctx.theme.accent;
|
|
3838
|
+
const pop = 1 + 0.28 * Math.exp(-tickElapsed / 120);
|
|
3839
|
+
const fade = 1 - Math.max(0, (tickFrac - 0.7) / 0.3) * 0.85;
|
|
3840
|
+
c.save();
|
|
3841
|
+
if (ring) {
|
|
3842
|
+
const r = fontPx * 0.62;
|
|
3843
|
+
c.lineWidth = 10;
|
|
3844
|
+
c.lineCap = "round";
|
|
3845
|
+
c.strokeStyle = rgba6(col, 0.18 * fade);
|
|
3846
|
+
c.beginPath();
|
|
3847
|
+
c.arc(cx, cy, r, 0, Math.PI * 2);
|
|
3848
|
+
c.stroke();
|
|
3849
|
+
c.strokeStyle = rgba6(col, 0.9 * fade);
|
|
3850
|
+
c.beginPath();
|
|
3851
|
+
c.arc(cx, cy, r, -Math.PI / 2, -Math.PI / 2 + (1 - tickFrac) * Math.PI * 2);
|
|
3852
|
+
c.stroke();
|
|
3853
|
+
}
|
|
3854
|
+
c.globalAlpha = fade;
|
|
3855
|
+
c.fillStyle = col;
|
|
3856
|
+
c.font = `800 ${Math.round(fontPx * pop)}px ${ctx.theme.fontDisplay}`;
|
|
3857
|
+
c.textAlign = "center";
|
|
3858
|
+
c.textBaseline = "middle";
|
|
3859
|
+
c.fillText(String(n), cx, cy + 2);
|
|
3860
|
+
c.restore();
|
|
3861
|
+
}
|
|
3862
|
+
};
|
|
3863
|
+
}
|
|
3864
|
+
var countdownFactory = {
|
|
3865
|
+
key: "countdown",
|
|
3866
|
+
create: countdownLayer,
|
|
3867
|
+
validateProps(props) {
|
|
3868
|
+
if (props == null || typeof props !== "object") return ["countdown: props must be an object"];
|
|
3869
|
+
const p = props;
|
|
3870
|
+
const errs = [];
|
|
3871
|
+
if (p.from != null && (typeof p.from !== "number" || p.from < 1 || !Number.isInteger(p.from))) errs.push("countdown.from must be an integer >= 1");
|
|
3872
|
+
if (p.startMs != null && typeof p.startMs !== "number") errs.push("countdown.startMs must be a number");
|
|
3873
|
+
if (p.durationMs != null && (typeof p.durationMs !== "number" || p.durationMs <= 0)) errs.push("countdown.durationMs must be a positive number");
|
|
3874
|
+
if (p.color != null && typeof p.color !== "string") errs.push("countdown.color must be a string");
|
|
3875
|
+
if (p.fontPx != null && (typeof p.fontPx !== "number" || p.fontPx <= 0)) errs.push("countdown.fontPx must be a positive number");
|
|
3876
|
+
if (p.centerFrac != null && (typeof p.centerFrac !== "number" || p.centerFrac < 0 || p.centerFrac > 1)) errs.push("countdown.centerFrac must be in [0,1]");
|
|
3877
|
+
if (p.ring != null && typeof p.ring !== "boolean") errs.push("countdown.ring must be a boolean");
|
|
3878
|
+
return errs;
|
|
3879
|
+
}
|
|
3880
|
+
};
|
|
3881
|
+
|
|
3882
|
+
// src/scene/layers/waveform.ts
|
|
3883
|
+
var SAMPLES = 128;
|
|
3884
|
+
function clampUnit(x) {
|
|
3885
|
+
return x < -1 ? -1 : x > 1 ? 1 : x;
|
|
3886
|
+
}
|
|
3887
|
+
function syntheticSample(tSec, u) {
|
|
3888
|
+
const phase = u * Math.PI * 2;
|
|
3889
|
+
return 0.55 * Math.sin(phase * 3 + tSec * 6) + 0.28 * Math.sin(phase * 5 + tSec * 9 + 1.1) + 0.14 * Math.sin(phase * 8 + tSec * 4 + 2.3);
|
|
3890
|
+
}
|
|
3891
|
+
function waveformLayer() {
|
|
3892
|
+
let n = SAMPLES;
|
|
3893
|
+
let centerFrac = 0.46;
|
|
3894
|
+
let amplitudeFrac = 0.1;
|
|
3895
|
+
let lineWidth = 5;
|
|
3896
|
+
let color;
|
|
3897
|
+
let samplesFn;
|
|
3898
|
+
function samplesAt(ctx, tMs) {
|
|
3899
|
+
const fromProp = samplesFn?.(tMs, n);
|
|
3900
|
+
if (fromProp && fromProp.length) {
|
|
3901
|
+
return Array.from({ length: n }, (_, i) => clampUnit(fromProp[Math.min(fromProp.length - 1, i)] ?? 0));
|
|
3902
|
+
}
|
|
3903
|
+
const bt = ctx.waveform?.byteTime?.(tMs);
|
|
3904
|
+
if (bt && bt.length) {
|
|
3905
|
+
return Array.from({ length: n }, (_, i) => clampUnit((bt[Math.floor(i / n * bt.length)] - 128) / 128));
|
|
3906
|
+
}
|
|
3907
|
+
const tSec = tMs / 1e3;
|
|
3908
|
+
return Array.from({ length: n }, (_, i) => clampUnit(syntheticSample(tSec, i / (n - 1))));
|
|
3909
|
+
}
|
|
3910
|
+
return {
|
|
3911
|
+
key: "waveform",
|
|
3912
|
+
init(_ctx, props) {
|
|
3913
|
+
n = props.samples ?? SAMPLES;
|
|
3914
|
+
centerFrac = props.centerFrac ?? 0.46;
|
|
3915
|
+
amplitudeFrac = props.amplitudeFrac ?? 0.1;
|
|
3916
|
+
lineWidth = props.lineWidth ?? 5;
|
|
3917
|
+
color = props.color;
|
|
3918
|
+
samplesFn = props.samplesFn;
|
|
3919
|
+
},
|
|
3920
|
+
draw(ctx, tMs) {
|
|
3921
|
+
const c = ctx.ctx2d;
|
|
3922
|
+
const sb = ctx.safeBox;
|
|
3923
|
+
const left = sb.left, right = sb.right;
|
|
3924
|
+
const cy = ctx.H * centerFrac;
|
|
3925
|
+
const amp = ctx.H * amplitudeFrac;
|
|
3926
|
+
const s = samplesAt(ctx, tMs);
|
|
3927
|
+
c.save();
|
|
3928
|
+
c.lineJoin = "round";
|
|
3929
|
+
c.lineCap = "round";
|
|
3930
|
+
c.strokeStyle = color ?? ctx.theme.accent;
|
|
3931
|
+
c.lineWidth = lineWidth;
|
|
3932
|
+
c.beginPath();
|
|
3933
|
+
for (let i = 0; i < n; i++) {
|
|
3934
|
+
const x = left + (right - left) * (i / (n - 1));
|
|
3935
|
+
const y = cy + s[i] * amp;
|
|
3936
|
+
if (i === 0) c.moveTo(x, y);
|
|
3937
|
+
else c.lineTo(x, y);
|
|
3938
|
+
}
|
|
3939
|
+
c.stroke();
|
|
3940
|
+
c.restore();
|
|
3941
|
+
}
|
|
3942
|
+
};
|
|
3943
|
+
}
|
|
3944
|
+
var waveformFactory = {
|
|
3945
|
+
key: "waveform",
|
|
3946
|
+
create: waveformLayer,
|
|
3947
|
+
validateProps(props) {
|
|
3948
|
+
if (props == null || typeof props !== "object") return ["waveform: props must be an object"];
|
|
3949
|
+
const p = props;
|
|
3950
|
+
const errs = [];
|
|
3951
|
+
if (p.samples != null && (typeof p.samples !== "number" || p.samples < 8)) errs.push("waveform.samples must be a number >= 8");
|
|
3952
|
+
if (p.centerFrac != null && (typeof p.centerFrac !== "number" || p.centerFrac < 0 || p.centerFrac > 1)) errs.push("waveform.centerFrac must be in [0,1]");
|
|
3953
|
+
if (p.amplitudeFrac != null && (typeof p.amplitudeFrac !== "number" || p.amplitudeFrac <= 0)) errs.push("waveform.amplitudeFrac must be a positive number");
|
|
3954
|
+
if (p.lineWidth != null && (typeof p.lineWidth !== "number" || p.lineWidth <= 0)) errs.push("waveform.lineWidth must be a positive number");
|
|
3955
|
+
if (p.color != null && typeof p.color !== "string") errs.push("waveform.color must be a string");
|
|
3956
|
+
if (p.samplesFn != null && typeof p.samplesFn !== "function") errs.push("waveform.samplesFn must be a function");
|
|
3957
|
+
return errs;
|
|
3958
|
+
}
|
|
3959
|
+
};
|
|
3960
|
+
|
|
3961
|
+
// src/scene/layers/scaleHighlight.ts
|
|
3962
|
+
var SCALE_INTERVALS = {
|
|
3963
|
+
major: [0, 2, 4, 5, 7, 9, 11],
|
|
3964
|
+
ionian: [0, 2, 4, 5, 7, 9, 11],
|
|
3965
|
+
minor: [0, 2, 3, 5, 7, 8, 10],
|
|
3966
|
+
aeolian: [0, 2, 3, 5, 7, 8, 10],
|
|
3967
|
+
dorian: [0, 2, 3, 5, 7, 9, 10],
|
|
3968
|
+
phrygian: [0, 1, 3, 5, 7, 8, 10],
|
|
3969
|
+
lydian: [0, 2, 4, 6, 7, 9, 11],
|
|
3970
|
+
mixolydian: [0, 2, 4, 5, 7, 9, 10],
|
|
3971
|
+
locrian: [0, 1, 3, 5, 6, 8, 10],
|
|
3972
|
+
harmonicMinor: [0, 2, 3, 5, 7, 8, 11],
|
|
3973
|
+
melodicMinor: [0, 2, 3, 5, 7, 9, 11],
|
|
3974
|
+
pentatonicMajor: [0, 2, 4, 7, 9],
|
|
3975
|
+
pentatonicMinor: [0, 3, 5, 7, 10],
|
|
3976
|
+
blues: [0, 3, 5, 6, 7, 10],
|
|
3977
|
+
chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
|
3978
|
+
};
|
|
3979
|
+
var PRETTY = {
|
|
3980
|
+
major: "Major",
|
|
3981
|
+
ionian: "Ionian",
|
|
3982
|
+
minor: "Minor",
|
|
3983
|
+
aeolian: "Aeolian",
|
|
3984
|
+
dorian: "Dorian",
|
|
3985
|
+
phrygian: "Phrygian",
|
|
3986
|
+
lydian: "Lydian",
|
|
3987
|
+
mixolydian: "Mixolydian",
|
|
3988
|
+
locrian: "Locrian",
|
|
3989
|
+
harmonicMinor: "Harmonic Minor",
|
|
3990
|
+
melodicMinor: "Melodic Minor",
|
|
3991
|
+
pentatonicMajor: "Pentatonic",
|
|
3992
|
+
pentatonicMinor: "Minor Pentatonic",
|
|
3993
|
+
blues: "Blues",
|
|
3994
|
+
chromatic: "Chromatic"
|
|
3995
|
+
};
|
|
3996
|
+
function rootPc(root) {
|
|
3997
|
+
if (typeof root === "number") return (root % 12 + 12) % 12;
|
|
3998
|
+
return noteNameToIndex(root);
|
|
3999
|
+
}
|
|
4000
|
+
function scaleHighlightLayer() {
|
|
4001
|
+
let rootProp = "C";
|
|
4002
|
+
let scaleKey = "major";
|
|
4003
|
+
let scaleColor;
|
|
4004
|
+
let tonicColor;
|
|
4005
|
+
let alpha = 0.55;
|
|
4006
|
+
let label = true;
|
|
4007
|
+
let labelTextProp;
|
|
4008
|
+
let labelCenterFrac;
|
|
4009
|
+
return {
|
|
4010
|
+
key: "scale-highlight",
|
|
4011
|
+
init(_ctx, props) {
|
|
4012
|
+
rootProp = props.root ?? "C";
|
|
4013
|
+
scaleKey = props.scale ?? "major";
|
|
4014
|
+
scaleColor = props.scaleColor;
|
|
4015
|
+
tonicColor = props.tonicColor;
|
|
4016
|
+
alpha = props.alpha ?? 0.55;
|
|
4017
|
+
label = props.label ?? true;
|
|
4018
|
+
labelTextProp = props.labelText;
|
|
4019
|
+
labelCenterFrac = props.labelCenterFrac;
|
|
4020
|
+
},
|
|
4021
|
+
draw(ctx, _tMs) {
|
|
4022
|
+
const c = ctx.ctx2d;
|
|
4023
|
+
const intervals = SCALE_INTERVALS[scaleKey] ?? SCALE_INTERVALS.major;
|
|
4024
|
+
const root = rootPc(rootProp);
|
|
4025
|
+
const inScale = new Set(intervals.map((iv) => (root + iv) % 12));
|
|
4026
|
+
const sc = scaleColor ?? ctx.theme.accent;
|
|
4027
|
+
const tc = tonicColor ?? ctx.theme.gold;
|
|
4028
|
+
const layout = getKeyboardLayout(ctx);
|
|
4029
|
+
if (layout) {
|
|
4030
|
+
c.save();
|
|
4031
|
+
c.globalAlpha = alpha;
|
|
4032
|
+
for (let m = layout.lowMidi; m <= layout.highMidi; m++) {
|
|
4033
|
+
if (!inRange(layout, m)) continue;
|
|
4034
|
+
const pc = pitchClass(m);
|
|
4035
|
+
if (!inScale.has(pc)) continue;
|
|
4036
|
+
const r = keyRect(layout, m);
|
|
4037
|
+
c.fillStyle = pc === root ? tc : sc;
|
|
4038
|
+
c.fillRect(r.x, r.y, r.w, r.h);
|
|
4039
|
+
}
|
|
4040
|
+
c.restore();
|
|
4041
|
+
}
|
|
4042
|
+
if (label) {
|
|
4043
|
+
const sb = ctx.safeBox;
|
|
4044
|
+
const name = typeof rootProp === "string" ? rootProp : ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"][root];
|
|
4045
|
+
const text = labelTextProp ?? `${name} ${PRETTY[scaleKey] ?? scaleKey}`;
|
|
4046
|
+
c.save();
|
|
4047
|
+
c.fillStyle = ctx.theme.ink;
|
|
4048
|
+
c.font = `700 ${Math.round(ctx.H * 0.03)}px ${ctx.theme.fontDisplay}`;
|
|
4049
|
+
c.textAlign = "center";
|
|
4050
|
+
c.textBaseline = labelCenterFrac != null ? "middle" : "top";
|
|
4051
|
+
const ly = labelCenterFrac != null ? ctx.H * labelCenterFrac : sb.top + sb.h * 0.04;
|
|
4052
|
+
c.fillText(text, ctx.W / 2, ly);
|
|
4053
|
+
c.restore();
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
};
|
|
4057
|
+
}
|
|
4058
|
+
var scaleHighlightFactory = {
|
|
4059
|
+
key: "scale-highlight",
|
|
4060
|
+
create: scaleHighlightLayer,
|
|
4061
|
+
validateProps(props) {
|
|
4062
|
+
if (props == null || typeof props !== "object") return ["scale-highlight: props must be an object"];
|
|
4063
|
+
const p = props;
|
|
4064
|
+
const errs = [];
|
|
4065
|
+
if (p.root != null && typeof p.root !== "string" && typeof p.root !== "number") errs.push("scale-highlight.root must be a note name or pitch-class number");
|
|
4066
|
+
if (p.scale != null && (typeof p.scale !== "string" || !(p.scale in SCALE_INTERVALS))) errs.push(`scale-highlight.scale must be one of: ${Object.keys(SCALE_INTERVALS).join(", ")}`);
|
|
4067
|
+
if (p.scaleColor != null && typeof p.scaleColor !== "string") errs.push("scale-highlight.scaleColor must be a string");
|
|
4068
|
+
if (p.tonicColor != null && typeof p.tonicColor !== "string") errs.push("scale-highlight.tonicColor must be a string");
|
|
4069
|
+
if (p.alpha != null && (typeof p.alpha !== "number" || p.alpha < 0 || p.alpha > 1)) errs.push("scale-highlight.alpha must be in [0,1]");
|
|
4070
|
+
if (p.label != null && typeof p.label !== "boolean") errs.push("scale-highlight.label must be a boolean");
|
|
4071
|
+
if (p.labelText != null && typeof p.labelText !== "string") errs.push("scale-highlight.labelText must be a string");
|
|
4072
|
+
if (p.labelCenterFrac != null && (typeof p.labelCenterFrac !== "number" || p.labelCenterFrac < 0 || p.labelCenterFrac > 1)) errs.push("scale-highlight.labelCenterFrac must be in [0,1]");
|
|
4073
|
+
return errs;
|
|
4074
|
+
}
|
|
4075
|
+
};
|
|
4076
|
+
|
|
3660
4077
|
// src/scene/registry.ts
|
|
3661
4078
|
var REGISTRY = /* @__PURE__ */ new Map();
|
|
3662
4079
|
function registerLayer(factory) {
|
|
@@ -3695,6 +4112,10 @@ registerLayer(progressRingFactory);
|
|
|
3695
4112
|
registerLayer(radialSpectrumFactory);
|
|
3696
4113
|
registerLayer(karaokeCaptionFactory);
|
|
3697
4114
|
registerLayer(intervalArcsFactory);
|
|
4115
|
+
registerLayer(kineticTextFactory);
|
|
4116
|
+
registerLayer(countdownFactory);
|
|
4117
|
+
registerLayer(waveformFactory);
|
|
4118
|
+
registerLayer(scaleHighlightFactory);
|
|
3698
4119
|
|
|
3699
4120
|
// src/scene/audioLayers.ts
|
|
3700
4121
|
function countInSchedule(opts) {
|
|
@@ -3851,7 +4272,9 @@ var SCREEN_PINNED_KEYS = /* @__PURE__ */ new Set([
|
|
|
3851
4272
|
"beat-pulse",
|
|
3852
4273
|
"chord-ribbon",
|
|
3853
4274
|
"progress-ring",
|
|
3854
|
-
"karaoke-caption"
|
|
4275
|
+
"karaoke-caption",
|
|
4276
|
+
"kinetic-text",
|
|
4277
|
+
"countdown"
|
|
3855
4278
|
]);
|
|
3856
4279
|
function defaultBufferFactory() {
|
|
3857
4280
|
const g = globalThis;
|
|
@@ -4438,6 +4861,7 @@ export {
|
|
|
4438
4861
|
FOLLOW_PAD,
|
|
4439
4862
|
PIANO_HIGH,
|
|
4440
4863
|
PIANO_LOW,
|
|
4864
|
+
SCALE_INTERVALS,
|
|
4441
4865
|
activeChord,
|
|
4442
4866
|
activeCue,
|
|
4443
4867
|
activeSection,
|
|
@@ -4467,6 +4891,7 @@ export {
|
|
|
4467
4891
|
contourPolyline,
|
|
4468
4892
|
countInLeadSec,
|
|
4469
4893
|
countInSchedule,
|
|
4894
|
+
countdownFactory,
|
|
4470
4895
|
countdownRemaining,
|
|
4471
4896
|
countdownSeconds,
|
|
4472
4897
|
countingDegreeDemoSpec,
|
|
@@ -4517,6 +4942,7 @@ export {
|
|
|
4517
4942
|
keySlot,
|
|
4518
4943
|
keyboardFactory,
|
|
4519
4944
|
keyboardLayout,
|
|
4945
|
+
kineticTextFactory,
|
|
4520
4946
|
lerp,
|
|
4521
4947
|
lerpBox,
|
|
4522
4948
|
lerpCamera,
|
|
@@ -4562,6 +4988,7 @@ export {
|
|
|
4562
4988
|
revealProgress,
|
|
4563
4989
|
runGate,
|
|
4564
4990
|
safeGuidesFactory,
|
|
4991
|
+
scaleHighlightFactory,
|
|
4565
4992
|
scoreFromMusicXML,
|
|
4566
4993
|
scorePitchSpan,
|
|
4567
4994
|
scrollCursorFactory,
|
|
@@ -4584,6 +5011,7 @@ export {
|
|
|
4584
5011
|
visualTimelineMs,
|
|
4585
5012
|
vstackAudioPlayheadLine,
|
|
4586
5013
|
vstackFollowBox,
|
|
5014
|
+
waveformFactory,
|
|
4587
5015
|
whiteKeys,
|
|
4588
5016
|
worldToViewport
|
|
4589
5017
|
};
|