@real-music-packages/web-core 0.14.0 → 0.16.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/scene/index.d.ts +41 -5
- package/dist/scene/index.js +275 -15
- package/dist/scene/index.js.map +1 -1
- package/package.json +1 -1
package/dist/scene/index.d.ts
CHANGED
|
@@ -670,15 +670,25 @@ interface NotationProps {
|
|
|
670
670
|
declare const notationFactory: LayerFactory<NotationProps>;
|
|
671
671
|
|
|
672
672
|
interface ScrollCursorProps {
|
|
673
|
+
/**
|
|
674
|
+
* Cursor pacing mode. Default `'audio'` (foolproof): the cursor is driven
|
|
675
|
+
* note-by-note off the audio clock against each note's `onsetMs`, so it lands
|
|
676
|
+
* on every note as it sounds and can never race/desync the full score width —
|
|
677
|
+
* correct by construction regardless of timeline. `'linear'` is the legacy
|
|
678
|
+
* measure-linear sweep paced over `musicMs` (RSR's original behaviour) — kept
|
|
679
|
+
* for back-compat; prefer leaving this unset.
|
|
680
|
+
*/
|
|
681
|
+
mode?: 'audio' | 'linear';
|
|
673
682
|
/** Bars visible in the follow window. Default 2 (RSR FOLLOW_BARS). Informational
|
|
674
683
|
* for v1 — the geometry uses the module constant unless overridden here. */
|
|
675
684
|
followBars?: number;
|
|
676
685
|
/** Opening-zoom duration in ms before the music/cursor start (RSR INTRO_MS=900).
|
|
677
|
-
* During this lead-in
|
|
686
|
+
* During this lead-in the follow window is held at the start. Default 900. */
|
|
678
687
|
openingZoomMs?: number;
|
|
679
|
-
/** Total music length in ms (RSR musicMs). Required to pace
|
|
680
|
-
*
|
|
681
|
-
|
|
688
|
+
/** Total music length in ms (RSR musicMs). Required by `'linear'` to pace
|
|
689
|
+
* camProgress; in `'audio'` mode it is optional (the onset clock drives pacing)
|
|
690
|
+
* and used only as a fallback when the score has no notes. */
|
|
691
|
+
musicMs?: number;
|
|
682
692
|
/** Cursor stroke colour. Defaults to theme.accent. */
|
|
683
693
|
color?: string;
|
|
684
694
|
}
|
|
@@ -755,6 +765,24 @@ interface PlayheadLine {
|
|
|
755
765
|
alpha: number;
|
|
756
766
|
}
|
|
757
767
|
declare function playheadLine(layout: NotationLayout, t01: number): PlayheadLine | null;
|
|
768
|
+
/** Distinct, sorted note onsets from a Score's notes. */
|
|
769
|
+
declare function distinctOnsets(notes: {
|
|
770
|
+
onsetMs: number;
|
|
771
|
+
}[]): number[];
|
|
772
|
+
/**
|
|
773
|
+
* The FOOLPROOF audio-driven playhead line for absolute audio time `tMs`.
|
|
774
|
+
*
|
|
775
|
+
* `tMs` is the audio clock; `onsetsMs` are the score's distinct note onsets
|
|
776
|
+
* (sorted). The cursor is the time-lerp of the two anchors bracketing `tMs`:
|
|
777
|
+
* before the first onset it holds on anchor 0; after the last it holds on the
|
|
778
|
+
* last; in a gap it eases between the bracketing onsets. Pure function of
|
|
779
|
+
* (layout, onsetsMs, tMs). Returns null only when there is no geometry to anchor
|
|
780
|
+
* to (falls back to the rect sweep in the layer, same as `playheadLine`).
|
|
781
|
+
*
|
|
782
|
+
* Fade in/out mirrors `playheadLine` but is keyed on position in the ONSET span
|
|
783
|
+
* (first→last onset), not on musicMs — so the fade tracks the notes too.
|
|
784
|
+
*/
|
|
785
|
+
declare function audioPlayheadLine(layout: NotationLayout, onsetsMs: number[], tMs: number): PlayheadLine | null;
|
|
758
786
|
|
|
759
787
|
/** Map a canvas-space box through a base notation layout into world/screen coords. */
|
|
760
788
|
declare function mapBoxThroughLayout(base: NotationLayout, b: Box): Box;
|
|
@@ -902,6 +930,10 @@ interface KeyboardProps {
|
|
|
902
930
|
bottomY?: number;
|
|
903
931
|
/** Right/left hand colours. Defaults: R = theme.accent, L = theme.gold. */
|
|
904
932
|
handColors?: HandColors;
|
|
933
|
+
/** Realistic 3-D piano styling: white/black key gradients, rounded key fronts,
|
|
934
|
+
* a felt rail + cast shadow at the top, and a soft glow on lit keys. Default
|
|
935
|
+
* false (the flat two-tone look — unchanged for existing consumers). */
|
|
936
|
+
realistic?: boolean;
|
|
905
937
|
}
|
|
906
938
|
declare const keyboardFactory: LayerFactory<KeyboardProps>;
|
|
907
939
|
|
|
@@ -918,6 +950,10 @@ interface FallingNotesProps {
|
|
|
918
950
|
speed?: number;
|
|
919
951
|
/** Glow flash at the hit-line while a note sounds. Default true. */
|
|
920
952
|
hitGlow?: boolean;
|
|
953
|
+
/** Glowy/translucent block styling: rounded corners, a vertical gradient that
|
|
954
|
+
* fades toward the top, a soft bloom (shadow) in the note colour, and a bright
|
|
955
|
+
* leading-edge cap. Default false (flat opaque rectangles — unchanged). */
|
|
956
|
+
glow?: boolean;
|
|
921
957
|
/** Standalone-only: keyboard range when `keyboard:false`. Default "auto". */
|
|
922
958
|
range?: '88' | 'auto';
|
|
923
959
|
/** Standalone-only: hit-line (world y). Default safeBox.bottom - 220. */
|
|
@@ -1532,4 +1568,4 @@ interface SectionMinimapProps {
|
|
|
1532
1568
|
}
|
|
1533
1569
|
declare const sectionMinimapFactory: LayerFactory<SectionMinimapProps>;
|
|
1534
1570
|
|
|
1535
|
-
export { type Affine, type AudioClock, type AudioEvent, type BackgroundProps, type BeatGridOpts, type BeatTick, type BrandingProps, type BufferFactory, type BuildSceneOpts, type BuiltScene, type CameraState, type CaptionCue, type CaptionProps, type CaptionScript, type CaptionStyle, type ChordSpan, type CircleOfFifthsProps, type CirclePoint, type ClickTrackOpts, type ColorBy, type ContourPlot, type ContourPoint, type CountInOpts, type CountingTrackProps, type CtaProps, DEFAULT_FUNCTION_COLORS, type DegreeLabel, type DegreeLabelsProps, type DroneOpts, type DuckWindow, type Easing, type ExtendedDemoOpts, FIFTHS_MAJOR, FIFTHS_MINOR, FOLLOW_BARS, FOLLOW_PAD, type FallingKeyboardDemoOpts, type FallingNotesProps, type FunctionColors, type FunctionalHarmonyProps, type GateError, type GateInput, type HandColors, type HarmonicFunction, type HarmonyMode, type HighlightRegion, type HookProps, type KeyRect, type KeyboardLayout, type KeyboardLayoutOpts, type KeyboardProps, type LabelMode, type Layer, type LayerFactory, type McqCardProps, type NotationEngraving, type NotationLayout, type NotationLayoutOpts, type NotationProps, type NotationRect, type OutputProbe, PIANO_HIGH, PIANO_LOW, type PitchContourProps, type Placement, type PlayheadLine, type PortraitProps, type PromoCardsDemoOpts, type Quiz, type QuizOption, type QuizPhase, type RayEndpoints, type RecordSceneSpecOpts, type Rect, type RenderCtx, type ResolvedSegment, type RevealProps, type SafeGuidesProps, type SceneSpec, type ScheduleTarget, type Score, type ScoreFromMusicXMLOpts, type ScoreNote, type ScrollCursorProps, type Section, type SectionMinimapProps, type SegmentAudio, type SegmentAudioCtx, type SegmentTransition, type SpecLayer, type SpectrumInput, type SpectrumProps, type StaffKeyboardRayProps, type TempoMap, type TimeAnchor, type TimelineSegment, activeChord, activeCue, activeSection, animatedSlot, applySchedule, applyToContext, assertGate, ballArc, ballX, beatGrid, beatPhase, blackKeys, bpmOf, brandingFactory, buildScene, cameraForFollow, cameraTransform, circleOfFifthsDemoSpec, circleOfFifthsFactory, clamp, clickTrackSchedule, contourMinimapDemoSpec, contourPoints, contourPolyline, countInLeadSec, countInSchedule, countdownRemaining, countdownSeconds, countingDegreeDemoSpec, countingTrackFactory, cropAroundBox, ctaFactory, cubicEaseInOut, cueOpacity, degreeLabel, degreeLabelsFactory, dotAt, drawCaption, drawHighlight, droneSchedule, duckGainAt, easeIn, easeInOut, easeOut, fallingKeyboardDemoScore, fallingKeyboardDemoSpec, fallingNotesFactory, firstMeasureBox, followBoxAt, followSrcBox, followWindowStart, fracSlotPoint, frameRect, functionColor, functionalHarmonyFactory, getKeyboardLayout, getLayerFactory, getNotationEngraving, harmonyDemoSpec, highlightIntensity, hookFactory, identityCamera, inRange, invLerp, isBlackKey, kenBurns, keyCenterX, keyColumnWidth, keyRect, keySlot, keyboardFactory, keyboardLayout, lerp, lerpBox, lerpCamera, linear, mapBoxThroughLayout, mcqCardFactory, mcqDemoSpec, measureColumnsFromLayout, measureCount, measureSpanBox, measureSpans, timeToX as minimapTimeToX, msPerBeat, notationFactory, notationLayout, noteColor, noteSetXRange, parseKey, parseTimeSig, pcToSlot, pitchAt, pitchContourFactory, pitchRange, playheadLine, portraitFactory, progress01, projectPoint, promoCardsDemoSpec, quizPhase, rayEndpoints, rayPointAt, recordSceneSpec, registerLayer, registeredKeys, resolveAnchor, resolveKeyboardLayout, resolveTimeline, revealFactory, revealProgress, runGate, safeGuidesFactory, scoreFromMusicXML, scorePitchSpan, scrollCursorFactory, sectionMinimapFactory, setFollowLayoutProvider, setKeyboardLayout, setNotationEngraving, slotAngle, slotPc, slotPoint, spectrumFactory, staffAnchor, staffKeyboardRayFactory, staffRayDemoSpec, transitionProgress, validateChordTrack, validateQuiz, validateSections, visualTimelineMs, whiteKeys, worldToViewport };
|
|
1571
|
+
export { type Affine, type AudioClock, type AudioEvent, type BackgroundProps, type BeatGridOpts, type BeatTick, type BrandingProps, type BufferFactory, type BuildSceneOpts, type BuiltScene, type CameraState, type CaptionCue, type CaptionProps, type CaptionScript, type CaptionStyle, type ChordSpan, type CircleOfFifthsProps, type CirclePoint, type ClickTrackOpts, type ColorBy, type ContourPlot, type ContourPoint, type CountInOpts, type CountingTrackProps, type CtaProps, DEFAULT_FUNCTION_COLORS, type DegreeLabel, type DegreeLabelsProps, type DroneOpts, type DuckWindow, type Easing, type ExtendedDemoOpts, FIFTHS_MAJOR, FIFTHS_MINOR, FOLLOW_BARS, FOLLOW_PAD, type FallingKeyboardDemoOpts, type FallingNotesProps, type FunctionColors, type FunctionalHarmonyProps, type GateError, type GateInput, type HandColors, type HarmonicFunction, type HarmonyMode, type HighlightRegion, type HookProps, type KeyRect, type KeyboardLayout, type KeyboardLayoutOpts, type KeyboardProps, type LabelMode, type Layer, type LayerFactory, type McqCardProps, type NotationEngraving, type NotationLayout, type NotationLayoutOpts, type NotationProps, type NotationRect, type OutputProbe, PIANO_HIGH, PIANO_LOW, type PitchContourProps, type Placement, type PlayheadLine, type PortraitProps, type PromoCardsDemoOpts, type Quiz, type QuizOption, type QuizPhase, type RayEndpoints, type RecordSceneSpecOpts, type Rect, type RenderCtx, type ResolvedSegment, type RevealProps, type SafeGuidesProps, type SceneSpec, type ScheduleTarget, type Score, type ScoreFromMusicXMLOpts, type ScoreNote, type ScrollCursorProps, type Section, type SectionMinimapProps, type SegmentAudio, type SegmentAudioCtx, type SegmentTransition, type SpecLayer, type SpectrumInput, type SpectrumProps, type StaffKeyboardRayProps, type TempoMap, type TimeAnchor, type TimelineSegment, activeChord, activeCue, activeSection, animatedSlot, applySchedule, applyToContext, assertGate, audioPlayheadLine, ballArc, ballX, beatGrid, beatPhase, blackKeys, bpmOf, brandingFactory, buildScene, cameraForFollow, cameraTransform, circleOfFifthsDemoSpec, circleOfFifthsFactory, clamp, clickTrackSchedule, contourMinimapDemoSpec, contourPoints, contourPolyline, countInLeadSec, countInSchedule, countdownRemaining, countdownSeconds, countingDegreeDemoSpec, countingTrackFactory, cropAroundBox, ctaFactory, cubicEaseInOut, cueOpacity, degreeLabel, degreeLabelsFactory, distinctOnsets, dotAt, drawCaption, drawHighlight, droneSchedule, duckGainAt, easeIn, easeInOut, easeOut, fallingKeyboardDemoScore, fallingKeyboardDemoSpec, fallingNotesFactory, firstMeasureBox, followBoxAt, followSrcBox, followWindowStart, fracSlotPoint, frameRect, functionColor, functionalHarmonyFactory, getKeyboardLayout, getLayerFactory, getNotationEngraving, harmonyDemoSpec, highlightIntensity, hookFactory, identityCamera, inRange, invLerp, isBlackKey, kenBurns, keyCenterX, keyColumnWidth, keyRect, keySlot, keyboardFactory, keyboardLayout, lerp, lerpBox, lerpCamera, linear, mapBoxThroughLayout, mcqCardFactory, mcqDemoSpec, measureColumnsFromLayout, measureCount, measureSpanBox, measureSpans, timeToX as minimapTimeToX, msPerBeat, notationFactory, notationLayout, noteColor, noteSetXRange, parseKey, parseTimeSig, pcToSlot, pitchAt, pitchContourFactory, pitchRange, playheadLine, portraitFactory, progress01, projectPoint, promoCardsDemoSpec, quizPhase, rayEndpoints, rayPointAt, recordSceneSpec, registerLayer, registeredKeys, resolveAnchor, resolveKeyboardLayout, resolveTimeline, revealFactory, revealProgress, runGate, safeGuidesFactory, scoreFromMusicXML, scorePitchSpan, scrollCursorFactory, sectionMinimapFactory, setFollowLayoutProvider, setKeyboardLayout, setNotationEngraving, slotAngle, slotPc, slotPoint, spectrumFactory, staffAnchor, staffKeyboardRayFactory, staffRayDemoSpec, transitionProgress, validateChordTrack, validateQuiz, validateSections, visualTimelineMs, whiteKeys, worldToViewport };
|
package/dist/scene/index.js
CHANGED
|
@@ -489,6 +489,74 @@ function playheadLine(layout, t01) {
|
|
|
489
489
|
}
|
|
490
490
|
return { x, y0, y1, alpha };
|
|
491
491
|
}
|
|
492
|
+
var clamp01 = (x) => x < 0 ? 0 : x > 1 ? 1 : x;
|
|
493
|
+
function anchorAtColumnPos(cols, pos, padV) {
|
|
494
|
+
const i = Math.min(cols.length - 1, Math.max(0, Math.floor(pos)));
|
|
495
|
+
const m = cols[i];
|
|
496
|
+
const startX = Math.min(m.noteStartX, m.x + m.w);
|
|
497
|
+
const frac = Math.min(1, Math.max(0, pos - i));
|
|
498
|
+
return {
|
|
499
|
+
onsetMs: 0,
|
|
500
|
+
x: startX + frac * (m.x + m.w - startX),
|
|
501
|
+
y0: m.y - padV,
|
|
502
|
+
y1: m.y + m.h + padV
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function noteAnchors(layout, onsetsMs, padV) {
|
|
506
|
+
const cols = measureColumnsFromLayout(layout.measures);
|
|
507
|
+
if (!cols.length || !onsetsMs.length) return [];
|
|
508
|
+
const n = onsetsMs.length;
|
|
509
|
+
return onsetsMs.map((onsetMs, k) => {
|
|
510
|
+
const frac = n === 1 ? 0 : k / (n - 1);
|
|
511
|
+
const a = anchorAtColumnPos(cols, frac * cols.length, padV);
|
|
512
|
+
return { ...a, onsetMs };
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
function distinctOnsets(notes) {
|
|
516
|
+
const set = /* @__PURE__ */ new Set();
|
|
517
|
+
for (const n of notes) set.add(n.onsetMs);
|
|
518
|
+
return [...set].sort((a, b) => a - b);
|
|
519
|
+
}
|
|
520
|
+
function audioPlayheadLine(layout, onsetsMs, tMs) {
|
|
521
|
+
const padV = 10;
|
|
522
|
+
const anchors = noteAnchors(layout, onsetsMs, padV);
|
|
523
|
+
if (!anchors.length) return null;
|
|
524
|
+
const first = anchors[0].onsetMs;
|
|
525
|
+
const last = anchors[anchors.length - 1].onsetMs;
|
|
526
|
+
const span = last - first;
|
|
527
|
+
let a, b, frac;
|
|
528
|
+
if (tMs <= first || anchors.length === 1) {
|
|
529
|
+
a = b = anchors[0];
|
|
530
|
+
frac = 0;
|
|
531
|
+
} else if (tMs >= last) {
|
|
532
|
+
a = b = anchors[anchors.length - 1];
|
|
533
|
+
frac = 0;
|
|
534
|
+
} else {
|
|
535
|
+
let lo = 0;
|
|
536
|
+
let hi = anchors.length - 1;
|
|
537
|
+
while (lo < hi) {
|
|
538
|
+
const mid = lo + hi + 1 >> 1;
|
|
539
|
+
if (anchors[mid].onsetMs <= tMs) lo = mid;
|
|
540
|
+
else hi = mid - 1;
|
|
541
|
+
}
|
|
542
|
+
a = anchors[lo];
|
|
543
|
+
b = anchors[lo + 1];
|
|
544
|
+
const dt = b.onsetMs - a.onsetMs;
|
|
545
|
+
frac = dt > 0 ? cubicEaseInOut((tMs - a.onsetMs) / dt) : 0;
|
|
546
|
+
}
|
|
547
|
+
const x = a.x + (b.x - a.x) * frac;
|
|
548
|
+
const y0 = a.y0 + (b.y0 - a.y0) * frac;
|
|
549
|
+
const y1 = a.y1 + (b.y1 - a.y1) * frac;
|
|
550
|
+
let alpha = 0.85;
|
|
551
|
+
if (span > 0) {
|
|
552
|
+
const fadeIn = Math.min(250, span * 0.5);
|
|
553
|
+
const fadeOut = Math.min(400, span * 0.5);
|
|
554
|
+
if (tMs < first + fadeIn) alpha *= clamp01((tMs - (first - fadeIn)) / (2 * fadeIn));
|
|
555
|
+
if (tMs > last - fadeOut) alpha *= clamp01((last + fadeOut - tMs) / (2 * fadeOut));
|
|
556
|
+
}
|
|
557
|
+
if (alpha <= 0.02) return null;
|
|
558
|
+
return { x, y0, y1, alpha };
|
|
559
|
+
}
|
|
492
560
|
|
|
493
561
|
// src/scene/engravingStore.ts
|
|
494
562
|
var STORE = /* @__PURE__ */ new WeakMap();
|
|
@@ -593,6 +661,7 @@ function followLayoutFor(ctx, camProgress01) {
|
|
|
593
661
|
return notationLayout(eng.rendered, ctx.W, ctx.H, eng.bandTop, eng.bandHeight, { focusBox });
|
|
594
662
|
}
|
|
595
663
|
function scrollCursorLayer() {
|
|
664
|
+
let mode = "audio";
|
|
596
665
|
let musicMs = 0;
|
|
597
666
|
let openingZoomMs = 900;
|
|
598
667
|
let color;
|
|
@@ -601,14 +670,41 @@ function scrollCursorLayer() {
|
|
|
601
670
|
if (phMs < 0 || musicMs <= 0) return 0;
|
|
602
671
|
return Math.min(1, phMs / musicMs);
|
|
603
672
|
}
|
|
673
|
+
function onsetsFor(ctx) {
|
|
674
|
+
const notes = ctx.score?.notes;
|
|
675
|
+
return notes && notes.length ? distinctOnsets(notes) : [];
|
|
676
|
+
}
|
|
677
|
+
function notesProgress(onsetsMs, tMs) {
|
|
678
|
+
if (tMs < openingZoomMs) return 0;
|
|
679
|
+
const n = onsetsMs.length;
|
|
680
|
+
if (n <= 1) return 0;
|
|
681
|
+
const first = onsetsMs[0];
|
|
682
|
+
const last = onsetsMs[n - 1];
|
|
683
|
+
if (tMs <= first) return 0;
|
|
684
|
+
if (tMs >= last || last <= first) return 1;
|
|
685
|
+
let lo = 0, hi = n - 1;
|
|
686
|
+
while (lo < hi) {
|
|
687
|
+
const mid = lo + hi + 1 >> 1;
|
|
688
|
+
if (onsetsMs[mid] <= tMs) lo = mid;
|
|
689
|
+
else hi = mid - 1;
|
|
690
|
+
}
|
|
691
|
+
const segFrac = (tMs - onsetsMs[lo]) / (onsetsMs[lo + 1] - onsetsMs[lo]);
|
|
692
|
+
return (lo + segFrac) / (n - 1);
|
|
693
|
+
}
|
|
694
|
+
function followProgress(ctx, tMs) {
|
|
695
|
+
if (mode === "linear") return camProgress(tMs);
|
|
696
|
+
const onsets = onsetsFor(ctx);
|
|
697
|
+
return onsets.length ? notesProgress(onsets, tMs) : camProgress(tMs);
|
|
698
|
+
}
|
|
604
699
|
function layoutAt(ctx, tMs) {
|
|
605
700
|
const eng = getNotationEngraving(ctx);
|
|
606
|
-
return followLayoutFor(ctx,
|
|
701
|
+
return followLayoutFor(ctx, followProgress(ctx, tMs)) ?? eng.base;
|
|
607
702
|
}
|
|
608
703
|
return {
|
|
609
704
|
key: "scroll-cursor",
|
|
610
705
|
init(ctx, props) {
|
|
611
|
-
|
|
706
|
+
mode = props.mode ?? "audio";
|
|
707
|
+
musicMs = props.musicMs ?? 0;
|
|
612
708
|
openingZoomMs = props.openingZoomMs ?? 900;
|
|
613
709
|
color = props.color;
|
|
614
710
|
setFollowLayoutProvider(ctx, layoutAt);
|
|
@@ -616,9 +712,14 @@ function scrollCursorLayer() {
|
|
|
616
712
|
draw(ctx, tMs) {
|
|
617
713
|
const eng = getNotationEngraving(ctx);
|
|
618
714
|
if (!eng) return;
|
|
619
|
-
const p = camProgress(tMs);
|
|
620
715
|
const layout = layoutAt(ctx, tMs);
|
|
621
|
-
|
|
716
|
+
let line;
|
|
717
|
+
const onsets = mode === "audio" ? onsetsFor(ctx) : [];
|
|
718
|
+
if (mode === "audio" && onsets.length) {
|
|
719
|
+
line = audioPlayheadLine(layout, onsets, tMs);
|
|
720
|
+
} else {
|
|
721
|
+
line = playheadLine(layout, camProgress(tMs));
|
|
722
|
+
}
|
|
622
723
|
if (!line) return;
|
|
623
724
|
const c = ctx.ctx2d;
|
|
624
725
|
c.save();
|
|
@@ -640,8 +741,12 @@ var scrollCursorFactory = {
|
|
|
640
741
|
const errs = [];
|
|
641
742
|
if (props == null || typeof props !== "object") return ["scroll-cursor: props must be an object"];
|
|
642
743
|
const p = props;
|
|
643
|
-
if (
|
|
644
|
-
errs.push(
|
|
744
|
+
if (p.mode != null && p.mode !== "audio" && p.mode !== "linear")
|
|
745
|
+
errs.push('scroll-cursor.mode must be "audio" | "linear"');
|
|
746
|
+
if (p.mode === "linear" && (typeof p.musicMs !== "number" || !(p.musicMs > 0)))
|
|
747
|
+
errs.push('scroll-cursor.musicMs must be a positive number for mode:"linear"');
|
|
748
|
+
if (p.musicMs != null && (typeof p.musicMs !== "number" || !(p.musicMs > 0)))
|
|
749
|
+
errs.push("scroll-cursor.musicMs must be a positive number");
|
|
645
750
|
if (p.followBars != null && (typeof p.followBars !== "number" || p.followBars < 1))
|
|
646
751
|
errs.push("scroll-cursor.followBars must be a number >= 1");
|
|
647
752
|
if (p.openingZoomMs != null && (typeof p.openingZoomMs !== "number" || p.openingZoomMs < 0))
|
|
@@ -656,7 +761,7 @@ var BLACK_PC = /* @__PURE__ */ new Set([1, 3, 6, 8, 10]);
|
|
|
656
761
|
function whiteIndexAtOrBelow(midi) {
|
|
657
762
|
const oct = Math.floor(midi / 12);
|
|
658
763
|
const pc = midi - oct * 12;
|
|
659
|
-
const WHITES_BELOW = [0, 1, 1, 2,
|
|
764
|
+
const WHITES_BELOW = [0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6];
|
|
660
765
|
return oct * 7 + WHITES_BELOW[pc];
|
|
661
766
|
}
|
|
662
767
|
function isBlackKey(midi) {
|
|
@@ -779,15 +884,34 @@ function getKeyboardLayout(ctx) {
|
|
|
779
884
|
}
|
|
780
885
|
|
|
781
886
|
// src/scene/layers/keyboard.ts
|
|
887
|
+
function roundedPath(c, x, y, w, h, radii) {
|
|
888
|
+
c.beginPath();
|
|
889
|
+
const rr = c.roundRect;
|
|
890
|
+
if (typeof rr === "function") {
|
|
891
|
+
rr.call(c, x, y, w, h, radii);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
c.rect(x, y, w, h);
|
|
895
|
+
}
|
|
896
|
+
function shade(hex, f) {
|
|
897
|
+
const n = parseInt(hex.slice(1), 16);
|
|
898
|
+
const t = f < 0 ? 0 : 255, p = Math.abs(f);
|
|
899
|
+
const r = Math.round((n >> 16 & 255) + (t - (n >> 16 & 255)) * p);
|
|
900
|
+
const g = Math.round((n >> 8 & 255) + (t - (n >> 8 & 255)) * p);
|
|
901
|
+
const b = Math.round((n & 255) + (t - (n & 255)) * p);
|
|
902
|
+
return `rgb(${r},${g},${b})`;
|
|
903
|
+
}
|
|
782
904
|
function keyboardLayer() {
|
|
783
905
|
let layout = null;
|
|
784
906
|
let colorBy = "hand";
|
|
785
907
|
let hands = { R: "#7b2436", L: "#c8a55b" };
|
|
908
|
+
let realistic = false;
|
|
786
909
|
return {
|
|
787
910
|
key: "keyboard",
|
|
788
911
|
init(ctx, props) {
|
|
789
912
|
colorBy = props.colorBy ?? "hand";
|
|
790
913
|
hands = props.handColors ?? { R: ctx.theme.accent, L: ctx.theme.gold };
|
|
914
|
+
realistic = props.realistic ?? false;
|
|
791
915
|
const sb = ctx.safeBox;
|
|
792
916
|
const height = props.height ?? 220;
|
|
793
917
|
const bottomY = props.bottomY ?? sb.bottom;
|
|
@@ -810,6 +934,10 @@ function keyboardLayer() {
|
|
|
810
934
|
for (const n of ctx.score?.notes ?? []) {
|
|
811
935
|
if (tMs >= n.onsetMs && tMs < n.onsetMs + n.durMs) lit.set(n.pitchMidi, n.hand);
|
|
812
936
|
}
|
|
937
|
+
if (realistic) {
|
|
938
|
+
drawRealisticKeyboard(c, L, lit, colorBy, hands);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
813
941
|
c.save();
|
|
814
942
|
for (const m of whiteKeys(L)) {
|
|
815
943
|
const r = keyRect(L, m);
|
|
@@ -833,6 +961,78 @@ function keyboardLayer() {
|
|
|
833
961
|
}
|
|
834
962
|
};
|
|
835
963
|
}
|
|
964
|
+
function drawRealisticKeyboard(c, L, lit, colorBy, hands) {
|
|
965
|
+
{
|
|
966
|
+
const wr = keyRect(L, whiteKeys(L)[0] ?? 0);
|
|
967
|
+
const wRad = Math.max(3, Math.min(8, wr.w * 0.18));
|
|
968
|
+
c.save();
|
|
969
|
+
for (const m of whiteKeys(L)) {
|
|
970
|
+
const r = keyRect(L, m);
|
|
971
|
+
const onHand = lit.get(m);
|
|
972
|
+
const x = r.x + 0.75, w = r.w - 1.5;
|
|
973
|
+
roundedPath(c, x, r.y, w, r.h, [2, 2, wRad, wRad]);
|
|
974
|
+
if (onHand) {
|
|
975
|
+
const base = noteColor(m, onHand, colorBy, hands);
|
|
976
|
+
const g = c.createLinearGradient(0, r.y, 0, r.y + r.h);
|
|
977
|
+
g.addColorStop(0, shade(base, 0.18));
|
|
978
|
+
g.addColorStop(1, base);
|
|
979
|
+
c.fillStyle = g;
|
|
980
|
+
} else {
|
|
981
|
+
const g = c.createLinearGradient(0, r.y, 0, r.y + r.h);
|
|
982
|
+
g.addColorStop(0, "#e9e4da");
|
|
983
|
+
g.addColorStop(0.12, "#fbfaf7");
|
|
984
|
+
g.addColorStop(1, "#ffffff");
|
|
985
|
+
c.fillStyle = g;
|
|
986
|
+
}
|
|
987
|
+
c.fill();
|
|
988
|
+
c.strokeStyle = "#cdc6ba";
|
|
989
|
+
c.lineWidth = 1;
|
|
990
|
+
c.stroke();
|
|
991
|
+
}
|
|
992
|
+
const railH = Math.max(4, L.height * 0.03);
|
|
993
|
+
c.fillStyle = "#7b2436";
|
|
994
|
+
c.fillRect(L.x, L.top - railH, L.w, railH);
|
|
995
|
+
const sh = c.createLinearGradient(0, L.top, 0, L.top + L.height * 0.14);
|
|
996
|
+
sh.addColorStop(0, "rgba(0,0,0,0.22)");
|
|
997
|
+
sh.addColorStop(1, "rgba(0,0,0,0)");
|
|
998
|
+
c.fillStyle = sh;
|
|
999
|
+
c.fillRect(L.x, L.top, L.w, L.height * 0.14);
|
|
1000
|
+
for (const m of blackKeys(L)) {
|
|
1001
|
+
const r = keyRect(L, m);
|
|
1002
|
+
const bRad = Math.max(2, r.w * 0.22);
|
|
1003
|
+
const onHand = lit.get(m);
|
|
1004
|
+
roundedPath(c, r.x, r.y, r.w, r.h, [1, 1, bRad, bRad]);
|
|
1005
|
+
if (onHand) {
|
|
1006
|
+
const base = noteColor(m, onHand, colorBy, hands);
|
|
1007
|
+
const g = c.createLinearGradient(0, r.y, 0, r.y + r.h);
|
|
1008
|
+
g.addColorStop(0, shade(base, -0.25));
|
|
1009
|
+
g.addColorStop(1, base);
|
|
1010
|
+
c.fillStyle = g;
|
|
1011
|
+
} else {
|
|
1012
|
+
const g = c.createLinearGradient(0, r.y, 0, r.y + r.h);
|
|
1013
|
+
g.addColorStop(0, "#15110e");
|
|
1014
|
+
g.addColorStop(0.82, "#2c2622");
|
|
1015
|
+
g.addColorStop(1, "#453d36");
|
|
1016
|
+
c.fillStyle = g;
|
|
1017
|
+
}
|
|
1018
|
+
c.fill();
|
|
1019
|
+
c.fillStyle = onHand ? "rgba(255,255,255,0.28)" : "rgba(255,255,255,0.10)";
|
|
1020
|
+
roundedPath(c, r.x + r.w * 0.18, r.y + r.h - Math.max(2, r.h * 0.06), r.w * 0.64, Math.max(1.5, r.h * 0.035), [1, 1, 1, 1]);
|
|
1021
|
+
c.fill();
|
|
1022
|
+
}
|
|
1023
|
+
for (const [m, hand] of lit) {
|
|
1024
|
+
const r = keyRect(L, m);
|
|
1025
|
+
c.save();
|
|
1026
|
+
c.shadowColor = noteColor(m, hand, colorBy, hands);
|
|
1027
|
+
c.shadowBlur = 22;
|
|
1028
|
+
c.globalAlpha = 0.55;
|
|
1029
|
+
c.fillStyle = noteColor(m, hand, colorBy, hands);
|
|
1030
|
+
c.fillRect(r.x + r.w * 0.2, r.y + r.h * 0.5, r.w * 0.6, r.h * 0.3);
|
|
1031
|
+
c.restore();
|
|
1032
|
+
}
|
|
1033
|
+
c.restore();
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
836
1036
|
var keyboardFactory = {
|
|
837
1037
|
key: "keyboard",
|
|
838
1038
|
create: keyboardLayer,
|
|
@@ -848,17 +1048,68 @@ var keyboardFactory = {
|
|
|
848
1048
|
errs.push("keyboard.height must be a positive number");
|
|
849
1049
|
if (p.bottomY != null && typeof p.bottomY !== "number")
|
|
850
1050
|
errs.push("keyboard.bottomY must be a number");
|
|
1051
|
+
if (p.realistic != null && typeof p.realistic !== "boolean")
|
|
1052
|
+
errs.push("keyboard.realistic must be a boolean");
|
|
851
1053
|
return errs;
|
|
852
1054
|
}
|
|
853
1055
|
};
|
|
854
1056
|
|
|
855
1057
|
// src/scene/layers/fallingNotes.ts
|
|
856
1058
|
var DEFAULT_LEAD_MS = 2e3;
|
|
1059
|
+
function rgba(color, a) {
|
|
1060
|
+
if (/^#[0-9a-fA-F]{6}$/.test(color)) {
|
|
1061
|
+
const n = parseInt(color.slice(1), 16);
|
|
1062
|
+
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
|
|
1063
|
+
}
|
|
1064
|
+
return color;
|
|
1065
|
+
}
|
|
1066
|
+
function roundedRect(c, x, y, w, h, r) {
|
|
1067
|
+
c.beginPath();
|
|
1068
|
+
const rr = c.roundRect;
|
|
1069
|
+
if (typeof rr === "function") {
|
|
1070
|
+
rr.call(c, x, y, w, h, r);
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
c.rect(x, y, w, h);
|
|
1074
|
+
}
|
|
1075
|
+
function drawGlowBlock(c, x, y, w, hRaw, color) {
|
|
1076
|
+
const h = Math.max(2, hRaw);
|
|
1077
|
+
const r = Math.min(w * 0.42, 8);
|
|
1078
|
+
const bx = x + 1, bw = Math.max(1, w - 2);
|
|
1079
|
+
c.save();
|
|
1080
|
+
c.shadowColor = rgba(color, 1);
|
|
1081
|
+
c.shadowBlur = 22;
|
|
1082
|
+
c.fillStyle = rgba(color, 0.5);
|
|
1083
|
+
roundedRect(c, bx, y, bw, h, r);
|
|
1084
|
+
c.fill();
|
|
1085
|
+
c.fill();
|
|
1086
|
+
c.shadowBlur = 0;
|
|
1087
|
+
const g = c.createLinearGradient(0, y, 0, y + h);
|
|
1088
|
+
g.addColorStop(0, rgba(color, 0.06));
|
|
1089
|
+
g.addColorStop(0.5, rgba(color, 0.38));
|
|
1090
|
+
g.addColorStop(1, rgba(color, 0.78));
|
|
1091
|
+
c.fillStyle = g;
|
|
1092
|
+
roundedRect(c, bx, y, bw, h, r);
|
|
1093
|
+
c.fill();
|
|
1094
|
+
const hl = c.createLinearGradient(bx, 0, bx + bw, 0);
|
|
1095
|
+
hl.addColorStop(0, "rgba(255,255,255,0)");
|
|
1096
|
+
hl.addColorStop(0.5, "rgba(255,255,255,0.22)");
|
|
1097
|
+
hl.addColorStop(1, "rgba(255,255,255,0)");
|
|
1098
|
+
c.fillStyle = hl;
|
|
1099
|
+
roundedRect(c, bx + bw * 0.18, y, bw * 0.46, h, Math.min(r, bw * 0.2));
|
|
1100
|
+
c.fill();
|
|
1101
|
+
const capH = Math.min(7, h);
|
|
1102
|
+
c.fillStyle = rgba(color, 0.98);
|
|
1103
|
+
roundedRect(c, bx, y + h - capH, bw, capH, Math.min(r, capH / 2));
|
|
1104
|
+
c.fill();
|
|
1105
|
+
c.restore();
|
|
1106
|
+
}
|
|
857
1107
|
function fallingNotesLayer() {
|
|
858
1108
|
let paired = true;
|
|
859
1109
|
let colorBy = "hand";
|
|
860
1110
|
let hands = { R: "#7b2436", L: "#c8a55b" };
|
|
861
1111
|
let hitGlow = true;
|
|
1112
|
+
let glow = false;
|
|
862
1113
|
let ownLayout = null;
|
|
863
1114
|
let topY = 0;
|
|
864
1115
|
let leadMsProp;
|
|
@@ -882,6 +1133,7 @@ function fallingNotesLayer() {
|
|
|
882
1133
|
colorBy = props.colorBy ?? "hand";
|
|
883
1134
|
hands = props.handColors ?? { R: ctx.theme.accent, L: ctx.theme.gold };
|
|
884
1135
|
hitGlow = props.hitGlow ?? true;
|
|
1136
|
+
glow = props.glow ?? false;
|
|
885
1137
|
leadMsProp = props.leadMs;
|
|
886
1138
|
speedProp = props.speed;
|
|
887
1139
|
const sb = ctx.safeBox;
|
|
@@ -919,11 +1171,15 @@ function fallingNotesLayer() {
|
|
|
919
1171
|
const drawTop = Math.max(topY, topEdge);
|
|
920
1172
|
const drawBottom = Math.min(hit, bottomY);
|
|
921
1173
|
const fill = noteColor(n.pitchMidi, n.hand, colorBy, hands);
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1174
|
+
if (glow) {
|
|
1175
|
+
drawGlowBlock(c, cx - w / 2, drawTop, w, drawBottom - drawTop, fill);
|
|
1176
|
+
} else {
|
|
1177
|
+
c.save();
|
|
1178
|
+
c.fillStyle = fill;
|
|
1179
|
+
c.globalAlpha = 0.92;
|
|
1180
|
+
c.fillRect(cx - w / 2, drawTop, w, Math.max(1, drawBottom - drawTop));
|
|
1181
|
+
c.restore();
|
|
1182
|
+
}
|
|
927
1183
|
}
|
|
928
1184
|
if (hitGlow) {
|
|
929
1185
|
for (const n of ctx.score?.notes ?? []) {
|
|
@@ -961,6 +1217,8 @@ var fallingNotesFactory = {
|
|
|
961
1217
|
errs.push("falling-notes.speed must be a positive number");
|
|
962
1218
|
if (p.hitGlow != null && typeof p.hitGlow !== "boolean")
|
|
963
1219
|
errs.push("falling-notes.hitGlow must be a boolean");
|
|
1220
|
+
if (p.glow != null && typeof p.glow !== "boolean")
|
|
1221
|
+
errs.push("falling-notes.glow must be a boolean");
|
|
964
1222
|
if (p.range != null && p.range !== "88" && p.range !== "auto")
|
|
965
1223
|
errs.push('falling-notes.range must be "88" | "auto"');
|
|
966
1224
|
return errs;
|
|
@@ -1173,11 +1431,11 @@ function spectrumLayer() {
|
|
|
1173
1431
|
const src = fromProp ?? resolveFromCtx(ctx, tMs);
|
|
1174
1432
|
if (src && src.length) {
|
|
1175
1433
|
const out = new Array(n);
|
|
1176
|
-
for (let b = 0; b < n; b++) out[b] =
|
|
1434
|
+
for (let b = 0; b < n; b++) out[b] = clamp012(src[Math.min(src.length - 1, b)] ?? 0);
|
|
1177
1435
|
return out;
|
|
1178
1436
|
}
|
|
1179
1437
|
const tSec = tMs / 1e3;
|
|
1180
|
-
return Array.from({ length: n }, (_, b) =>
|
|
1438
|
+
return Array.from({ length: n }, (_, b) => clamp012(syntheticMagnitude(tSec, b, n)));
|
|
1181
1439
|
}
|
|
1182
1440
|
function resolveFromCtx(ctx, tMs) {
|
|
1183
1441
|
const sp = ctx.spectrum;
|
|
@@ -1239,7 +1497,7 @@ function spectrumLayer() {
|
|
|
1239
1497
|
}
|
|
1240
1498
|
};
|
|
1241
1499
|
}
|
|
1242
|
-
function
|
|
1500
|
+
function clamp012(x) {
|
|
1243
1501
|
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
1244
1502
|
}
|
|
1245
1503
|
var spectrumFactory = {
|
|
@@ -3366,6 +3624,7 @@ export {
|
|
|
3366
3624
|
applySchedule,
|
|
3367
3625
|
applyToContext,
|
|
3368
3626
|
assertGate,
|
|
3627
|
+
audioPlayheadLine,
|
|
3369
3628
|
ballArc,
|
|
3370
3629
|
ballX,
|
|
3371
3630
|
beatGrid,
|
|
@@ -3395,6 +3654,7 @@ export {
|
|
|
3395
3654
|
cueOpacity,
|
|
3396
3655
|
degreeLabel,
|
|
3397
3656
|
degreeLabelsFactory,
|
|
3657
|
+
distinctOnsets,
|
|
3398
3658
|
dotAt,
|
|
3399
3659
|
drawCaption,
|
|
3400
3660
|
drawHighlight,
|