@real-music-packages/web-core 0.23.0 → 0.24.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/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 +87 -1
- package/dist/scene/index.js +425 -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,415 @@ 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
|
+
return {
|
|
4009
|
+
key: "scale-highlight",
|
|
4010
|
+
init(_ctx, props) {
|
|
4011
|
+
rootProp = props.root ?? "C";
|
|
4012
|
+
scaleKey = props.scale ?? "major";
|
|
4013
|
+
scaleColor = props.scaleColor;
|
|
4014
|
+
tonicColor = props.tonicColor;
|
|
4015
|
+
alpha = props.alpha ?? 0.55;
|
|
4016
|
+
label = props.label ?? true;
|
|
4017
|
+
labelTextProp = props.labelText;
|
|
4018
|
+
},
|
|
4019
|
+
draw(ctx, _tMs) {
|
|
4020
|
+
const c = ctx.ctx2d;
|
|
4021
|
+
const intervals = SCALE_INTERVALS[scaleKey] ?? SCALE_INTERVALS.major;
|
|
4022
|
+
const root = rootPc(rootProp);
|
|
4023
|
+
const inScale = new Set(intervals.map((iv) => (root + iv) % 12));
|
|
4024
|
+
const sc = scaleColor ?? ctx.theme.accent;
|
|
4025
|
+
const tc = tonicColor ?? ctx.theme.gold;
|
|
4026
|
+
const layout = getKeyboardLayout(ctx);
|
|
4027
|
+
if (layout) {
|
|
4028
|
+
c.save();
|
|
4029
|
+
c.globalAlpha = alpha;
|
|
4030
|
+
for (let m = layout.lowMidi; m <= layout.highMidi; m++) {
|
|
4031
|
+
if (!inRange(layout, m)) continue;
|
|
4032
|
+
const pc = pitchClass(m);
|
|
4033
|
+
if (!inScale.has(pc)) continue;
|
|
4034
|
+
const r = keyRect(layout, m);
|
|
4035
|
+
c.fillStyle = pc === root ? tc : sc;
|
|
4036
|
+
c.fillRect(r.x, r.y, r.w, r.h);
|
|
4037
|
+
}
|
|
4038
|
+
c.restore();
|
|
4039
|
+
}
|
|
4040
|
+
if (label) {
|
|
4041
|
+
const sb = ctx.safeBox;
|
|
4042
|
+
const name = typeof rootProp === "string" ? rootProp : ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"][root];
|
|
4043
|
+
const text = labelTextProp ?? `${name} ${PRETTY[scaleKey] ?? scaleKey}`;
|
|
4044
|
+
c.save();
|
|
4045
|
+
c.fillStyle = ctx.theme.ink;
|
|
4046
|
+
c.font = `700 ${Math.round(ctx.H * 0.03)}px ${ctx.theme.fontDisplay}`;
|
|
4047
|
+
c.textAlign = "center";
|
|
4048
|
+
c.textBaseline = "top";
|
|
4049
|
+
c.fillText(text, ctx.W / 2, sb.top + sb.h * 0.04);
|
|
4050
|
+
c.restore();
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
};
|
|
4054
|
+
}
|
|
4055
|
+
var scaleHighlightFactory = {
|
|
4056
|
+
key: "scale-highlight",
|
|
4057
|
+
create: scaleHighlightLayer,
|
|
4058
|
+
validateProps(props) {
|
|
4059
|
+
if (props == null || typeof props !== "object") return ["scale-highlight: props must be an object"];
|
|
4060
|
+
const p = props;
|
|
4061
|
+
const errs = [];
|
|
4062
|
+
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");
|
|
4063
|
+
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(", ")}`);
|
|
4064
|
+
if (p.scaleColor != null && typeof p.scaleColor !== "string") errs.push("scale-highlight.scaleColor must be a string");
|
|
4065
|
+
if (p.tonicColor != null && typeof p.tonicColor !== "string") errs.push("scale-highlight.tonicColor must be a string");
|
|
4066
|
+
if (p.alpha != null && (typeof p.alpha !== "number" || p.alpha < 0 || p.alpha > 1)) errs.push("scale-highlight.alpha must be in [0,1]");
|
|
4067
|
+
if (p.label != null && typeof p.label !== "boolean") errs.push("scale-highlight.label must be a boolean");
|
|
4068
|
+
if (p.labelText != null && typeof p.labelText !== "string") errs.push("scale-highlight.labelText must be a string");
|
|
4069
|
+
return errs;
|
|
4070
|
+
}
|
|
4071
|
+
};
|
|
4072
|
+
|
|
3660
4073
|
// src/scene/registry.ts
|
|
3661
4074
|
var REGISTRY = /* @__PURE__ */ new Map();
|
|
3662
4075
|
function registerLayer(factory) {
|
|
@@ -3695,6 +4108,10 @@ registerLayer(progressRingFactory);
|
|
|
3695
4108
|
registerLayer(radialSpectrumFactory);
|
|
3696
4109
|
registerLayer(karaokeCaptionFactory);
|
|
3697
4110
|
registerLayer(intervalArcsFactory);
|
|
4111
|
+
registerLayer(kineticTextFactory);
|
|
4112
|
+
registerLayer(countdownFactory);
|
|
4113
|
+
registerLayer(waveformFactory);
|
|
4114
|
+
registerLayer(scaleHighlightFactory);
|
|
3698
4115
|
|
|
3699
4116
|
// src/scene/audioLayers.ts
|
|
3700
4117
|
function countInSchedule(opts) {
|
|
@@ -3851,7 +4268,9 @@ var SCREEN_PINNED_KEYS = /* @__PURE__ */ new Set([
|
|
|
3851
4268
|
"beat-pulse",
|
|
3852
4269
|
"chord-ribbon",
|
|
3853
4270
|
"progress-ring",
|
|
3854
|
-
"karaoke-caption"
|
|
4271
|
+
"karaoke-caption",
|
|
4272
|
+
"kinetic-text",
|
|
4273
|
+
"countdown"
|
|
3855
4274
|
]);
|
|
3856
4275
|
function defaultBufferFactory() {
|
|
3857
4276
|
const g = globalThis;
|
|
@@ -4438,6 +4857,7 @@ export {
|
|
|
4438
4857
|
FOLLOW_PAD,
|
|
4439
4858
|
PIANO_HIGH,
|
|
4440
4859
|
PIANO_LOW,
|
|
4860
|
+
SCALE_INTERVALS,
|
|
4441
4861
|
activeChord,
|
|
4442
4862
|
activeCue,
|
|
4443
4863
|
activeSection,
|
|
@@ -4467,6 +4887,7 @@ export {
|
|
|
4467
4887
|
contourPolyline,
|
|
4468
4888
|
countInLeadSec,
|
|
4469
4889
|
countInSchedule,
|
|
4890
|
+
countdownFactory,
|
|
4470
4891
|
countdownRemaining,
|
|
4471
4892
|
countdownSeconds,
|
|
4472
4893
|
countingDegreeDemoSpec,
|
|
@@ -4517,6 +4938,7 @@ export {
|
|
|
4517
4938
|
keySlot,
|
|
4518
4939
|
keyboardFactory,
|
|
4519
4940
|
keyboardLayout,
|
|
4941
|
+
kineticTextFactory,
|
|
4520
4942
|
lerp,
|
|
4521
4943
|
lerpBox,
|
|
4522
4944
|
lerpCamera,
|
|
@@ -4562,6 +4984,7 @@ export {
|
|
|
4562
4984
|
revealProgress,
|
|
4563
4985
|
runGate,
|
|
4564
4986
|
safeGuidesFactory,
|
|
4987
|
+
scaleHighlightFactory,
|
|
4565
4988
|
scoreFromMusicXML,
|
|
4566
4989
|
scorePitchSpan,
|
|
4567
4990
|
scrollCursorFactory,
|
|
@@ -4584,6 +5007,7 @@ export {
|
|
|
4584
5007
|
visualTimelineMs,
|
|
4585
5008
|
vstackAudioPlayheadLine,
|
|
4586
5009
|
vstackFollowBox,
|
|
5010
|
+
waveformFactory,
|
|
4587
5011
|
whiteKeys,
|
|
4588
5012
|
worldToViewport
|
|
4589
5013
|
};
|