@real-music-packages/web-core 0.14.0 → 0.15.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 +33 -5
- package/dist/scene/index.js +116 -9
- 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;
|
|
@@ -1532,4 +1560,4 @@ interface SectionMinimapProps {
|
|
|
1532
1560
|
}
|
|
1533
1561
|
declare const sectionMinimapFactory: LayerFactory<SectionMinimapProps>;
|
|
1534
1562
|
|
|
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 };
|
|
1563
|
+
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))
|
|
@@ -1173,11 +1278,11 @@ function spectrumLayer() {
|
|
|
1173
1278
|
const src = fromProp ?? resolveFromCtx(ctx, tMs);
|
|
1174
1279
|
if (src && src.length) {
|
|
1175
1280
|
const out = new Array(n);
|
|
1176
|
-
for (let b = 0; b < n; b++) out[b] =
|
|
1281
|
+
for (let b = 0; b < n; b++) out[b] = clamp012(src[Math.min(src.length - 1, b)] ?? 0);
|
|
1177
1282
|
return out;
|
|
1178
1283
|
}
|
|
1179
1284
|
const tSec = tMs / 1e3;
|
|
1180
|
-
return Array.from({ length: n }, (_, b) =>
|
|
1285
|
+
return Array.from({ length: n }, (_, b) => clamp012(syntheticMagnitude(tSec, b, n)));
|
|
1181
1286
|
}
|
|
1182
1287
|
function resolveFromCtx(ctx, tMs) {
|
|
1183
1288
|
const sp = ctx.spectrum;
|
|
@@ -1239,7 +1344,7 @@ function spectrumLayer() {
|
|
|
1239
1344
|
}
|
|
1240
1345
|
};
|
|
1241
1346
|
}
|
|
1242
|
-
function
|
|
1347
|
+
function clamp012(x) {
|
|
1243
1348
|
return x < 0 ? 0 : x > 1 ? 1 : x;
|
|
1244
1349
|
}
|
|
1245
1350
|
var spectrumFactory = {
|
|
@@ -3366,6 +3471,7 @@ export {
|
|
|
3366
3471
|
applySchedule,
|
|
3367
3472
|
applyToContext,
|
|
3368
3473
|
assertGate,
|
|
3474
|
+
audioPlayheadLine,
|
|
3369
3475
|
ballArc,
|
|
3370
3476
|
ballX,
|
|
3371
3477
|
beatGrid,
|
|
@@ -3395,6 +3501,7 @@ export {
|
|
|
3395
3501
|
cueOpacity,
|
|
3396
3502
|
degreeLabel,
|
|
3397
3503
|
degreeLabelsFactory,
|
|
3504
|
+
distinctOnsets,
|
|
3398
3505
|
dotAt,
|
|
3399
3506
|
drawCaption,
|
|
3400
3507
|
drawHighlight,
|