@real-music-packages/web-core 0.13.0 → 0.14.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 +68 -1
- package/dist/scene/index.js +176 -16
- package/dist/scene/index.js.map +1 -1
- package/package.json +1 -1
package/dist/scene/index.d.ts
CHANGED
|
@@ -227,6 +227,14 @@ declare const linear: Easing;
|
|
|
227
227
|
declare const easeInOut: Easing;
|
|
228
228
|
declare const easeIn: Easing;
|
|
229
229
|
declare const easeOut: Easing;
|
|
230
|
+
/**
|
|
231
|
+
* Eased 0..1 progress of a transition at absolute time `tMs`, given the incoming
|
|
232
|
+
* segment's `startMs` and the transition `durationMs`. Returns 0 at/before the
|
|
233
|
+
* start, 1 at/after start+durationMs, eased in between. Pure fn of t. Shared by
|
|
234
|
+
* the runner so the same envelope drives crossfade alpha, push translate, and
|
|
235
|
+
* wipe clip. `durationMs <= 0` → an instant 1 (degenerate hard-cut).
|
|
236
|
+
*/
|
|
237
|
+
declare const transitionProgress: (tMs: number, startMs: number, durationMs: number, easing?: Easing) => number;
|
|
230
238
|
|
|
231
239
|
/** A rectangle in WORLD coordinates (the layers' natural pixel space). */
|
|
232
240
|
interface Rect {
|
|
@@ -326,12 +334,52 @@ interface SegmentAudioCtx {
|
|
|
326
334
|
interface ScheduleTarget {
|
|
327
335
|
triggerAttackRelease: (note: unknown, dur: unknown, time?: unknown, velocity?: unknown) => void;
|
|
328
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Optional ENTER transition for a segment — how it comes IN from the PREVIOUS
|
|
339
|
+
* segment (the one immediately before it in the timeline). Purely VISUAL and
|
|
340
|
+
* fully additive: a segment with no `transition` hard-cuts exactly as before
|
|
341
|
+
* (existing specs are byte-for-byte unchanged). The audio timeline / per-segment
|
|
342
|
+
* `audio` hook is unaffected (transitions are a visual envelope only).
|
|
343
|
+
*
|
|
344
|
+
* During the first `durationMs` of the incoming segment, the runner renders BOTH
|
|
345
|
+
* the previous segment (its last drawn state, held) AND the incoming segment,
|
|
346
|
+
* applying an eased envelope (`easeInOut` by default):
|
|
347
|
+
* - `crossfade` — outgoing opacity 1→0, incoming 0→1 (the keystone; always on).
|
|
348
|
+
* - `push` — both slide along `direction`: incoming enters from off-frame
|
|
349
|
+
* while outgoing exits the opposite way (a hard slide, no fade).
|
|
350
|
+
* - `wipe` — incoming is revealed by a moving clip edge along `direction`;
|
|
351
|
+
* the outgoing shows through the un-wiped region (no fade).
|
|
352
|
+
*
|
|
353
|
+
* The overlap does NOT add time — the transition lives INSIDE the incoming
|
|
354
|
+
* segment's own window, so `visualTimelineMs` (and the gate's A/V-duration check)
|
|
355
|
+
* are unchanged. `durationMs` is clamped to the shorter of the two adjoining
|
|
356
|
+
* segment lengths so a transition can't run past either segment.
|
|
357
|
+
*/
|
|
358
|
+
interface SegmentTransition {
|
|
359
|
+
/** Transition style. Start with `crossfade`. */
|
|
360
|
+
type: 'crossfade' | 'push' | 'wipe';
|
|
361
|
+
/** Length of the enter window in ms (from the incoming segment's start). */
|
|
362
|
+
durationMs: number;
|
|
363
|
+
/**
|
|
364
|
+
* Slide / wipe direction for `push` and `wipe` (ignored by `crossfade`).
|
|
365
|
+
* Names the edge the INCOMING segment comes FROM. Default `left`.
|
|
366
|
+
*/
|
|
367
|
+
direction?: 'left' | 'right' | 'up' | 'down';
|
|
368
|
+
/** Eased envelope, 0..1 → 0..1. Defaults to `easeInOut`. */
|
|
369
|
+
easing?: Easing;
|
|
370
|
+
}
|
|
329
371
|
interface TimelineSegment {
|
|
330
372
|
/** [start, end] in SECONDS; end may be "end" / "end-N". */
|
|
331
373
|
at: [number, TimeAnchor];
|
|
332
374
|
layers: SpecLayer[];
|
|
333
375
|
/** Optional sound scheduled at this segment's start (see SegmentAudio). */
|
|
334
376
|
audio?: SegmentAudio;
|
|
377
|
+
/**
|
|
378
|
+
* Optional ENTER transition from the PREVIOUS segment (see SegmentTransition).
|
|
379
|
+
* Omit for today's hard cut. Has no effect on the first segment (nothing to
|
|
380
|
+
* transition from) unless `loop` is set on the spec.
|
|
381
|
+
*/
|
|
382
|
+
transition?: SegmentTransition;
|
|
335
383
|
}
|
|
336
384
|
interface SceneSpec {
|
|
337
385
|
/** [width, height] px. */
|
|
@@ -357,6 +405,7 @@ interface ResolvedSegment {
|
|
|
357
405
|
endMs: number;
|
|
358
406
|
layers: SpecLayer[];
|
|
359
407
|
audio?: SegmentAudio;
|
|
408
|
+
transition?: SegmentTransition;
|
|
360
409
|
}
|
|
361
410
|
/** Resolve every segment's [start,end] to ms against the total clip length. */
|
|
362
411
|
declare function resolveTimeline(spec: SceneSpec, totalSec: number): ResolvedSegment[];
|
|
@@ -397,7 +446,25 @@ interface BuildSceneOpts {
|
|
|
397
446
|
/** Override the camera pose per absolute tMs (pan/zoom director). Identity by
|
|
398
447
|
* default. */
|
|
399
448
|
camera?: (tMs: number) => CameraState;
|
|
449
|
+
/**
|
|
450
|
+
* Offscreen-buffer factory used ONLY by segment `transition`s (a segment's
|
|
451
|
+
* stack is rendered to a buffer, then composited onto the main canvas with the
|
|
452
|
+
* transition envelope — so the envelope is robust even when inner layers set
|
|
453
|
+
* their own `globalAlpha`). Unused when no segment has a transition.
|
|
454
|
+
*
|
|
455
|
+
* Default: `OffscreenCanvas` if available, else `document.createElement
|
|
456
|
+
* ('canvas')`. Node/headless callers (and the node-canvas golden tests) pass a
|
|
457
|
+
* factory backed by `createCanvas`. If no factory is resolvable AND a
|
|
458
|
+
* transition fires, the runner falls back to a direct `globalAlpha` envelope
|
|
459
|
+
* (crossfade only; degrades gracefully) rather than throwing.
|
|
460
|
+
*/
|
|
461
|
+
bufferFactory?: BufferFactory;
|
|
400
462
|
}
|
|
463
|
+
/** Makes an offscreen drawing surface for transition compositing. */
|
|
464
|
+
type BufferFactory = (w: number, h: number) => {
|
|
465
|
+
ctx: CanvasRenderingContext2D;
|
|
466
|
+
image: CanvasImageSource;
|
|
467
|
+
};
|
|
401
468
|
/**
|
|
402
469
|
* Instantiate + init every layer in the spec and return a deterministic renderer.
|
|
403
470
|
* Throws if a referenced layer key isn't registered or its props don't validate
|
|
@@ -1465,4 +1532,4 @@ interface SectionMinimapProps {
|
|
|
1465
1532
|
}
|
|
1466
1533
|
declare const sectionMinimapFactory: LayerFactory<SectionMinimapProps>;
|
|
1467
1534
|
|
|
1468
|
-
export { type Affine, type AudioClock, type AudioEvent, type BackgroundProps, type BeatGridOpts, type BeatTick, type BrandingProps, 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 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, validateChordTrack, validateQuiz, validateSections, visualTimelineMs, whiteKeys, worldToViewport };
|
|
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 };
|
package/dist/scene/index.js
CHANGED
|
@@ -177,6 +177,10 @@ var easeOut = (t) => {
|
|
|
177
177
|
const c = clamp(t, 0, 1);
|
|
178
178
|
return 1 - (1 - c) * (1 - c);
|
|
179
179
|
};
|
|
180
|
+
var transitionProgress = (tMs, startMs, durationMs, easing = easeInOut) => {
|
|
181
|
+
if (durationMs <= 0) return 1;
|
|
182
|
+
return easing(invLerp(startMs, startMs + durationMs, tMs));
|
|
183
|
+
};
|
|
180
184
|
|
|
181
185
|
// src/scene/caption.ts
|
|
182
186
|
function activeCue(script, tMs) {
|
|
@@ -2748,7 +2752,13 @@ function resolveTimeline(spec, totalSec) {
|
|
|
2748
2752
|
return spec.timeline.map((seg) => {
|
|
2749
2753
|
const startSec = resolveAnchor(seg.at[0], totalSec);
|
|
2750
2754
|
const endSec = resolveAnchor(seg.at[1], totalSec);
|
|
2751
|
-
return {
|
|
2755
|
+
return {
|
|
2756
|
+
startMs: startSec * 1e3,
|
|
2757
|
+
endMs: endSec * 1e3,
|
|
2758
|
+
layers: seg.layers,
|
|
2759
|
+
audio: seg.audio,
|
|
2760
|
+
transition: seg.transition
|
|
2761
|
+
};
|
|
2752
2762
|
});
|
|
2753
2763
|
}
|
|
2754
2764
|
function visualTimelineMs(resolved) {
|
|
@@ -2764,6 +2774,24 @@ var SCREEN_PINNED_KEYS = /* @__PURE__ */ new Set([
|
|
|
2764
2774
|
"safe-guides",
|
|
2765
2775
|
"mcq-card"
|
|
2766
2776
|
]);
|
|
2777
|
+
function defaultBufferFactory() {
|
|
2778
|
+
const g = globalThis;
|
|
2779
|
+
if (typeof g.OffscreenCanvas === "function") {
|
|
2780
|
+
return (w, h) => {
|
|
2781
|
+
const c = new g.OffscreenCanvas(w, h);
|
|
2782
|
+
return { ctx: c.getContext("2d"), image: c };
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
if (typeof g.document !== "undefined" && g.document?.createElement) {
|
|
2786
|
+
return (w, h) => {
|
|
2787
|
+
const c = g.document.createElement("canvas");
|
|
2788
|
+
c.width = w;
|
|
2789
|
+
c.height = h;
|
|
2790
|
+
return { ctx: c.getContext("2d"), image: c };
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
return void 0;
|
|
2794
|
+
}
|
|
2767
2795
|
async function buildScene(opts) {
|
|
2768
2796
|
const { spec, theme, score } = opts;
|
|
2769
2797
|
const [W, H] = spec.size;
|
|
@@ -2771,6 +2799,8 @@ async function buildScene(opts) {
|
|
|
2771
2799
|
const resolved = resolveTimeline(spec, opts.totalSec);
|
|
2772
2800
|
const safe = safeBox(W, H);
|
|
2773
2801
|
const camera = opts.camera ?? (() => identityCamera(W, H));
|
|
2802
|
+
const bufferFactory = opts.bufferFactory ?? defaultBufferFactory();
|
|
2803
|
+
const hasTransitions = resolved.some((s) => s.transition && s.transition.durationMs > 0);
|
|
2774
2804
|
const clock = { nowMs: () => 0 };
|
|
2775
2805
|
const baseCtx = {
|
|
2776
2806
|
W,
|
|
@@ -2782,7 +2812,8 @@ async function buildScene(opts) {
|
|
|
2782
2812
|
fps
|
|
2783
2813
|
};
|
|
2784
2814
|
const bound = [];
|
|
2785
|
-
for (
|
|
2815
|
+
for (let segIndex = 0; segIndex < resolved.length; segIndex++) {
|
|
2816
|
+
const seg = resolved[segIndex];
|
|
2786
2817
|
for (const sl of seg.layers) {
|
|
2787
2818
|
const factory = getLayerFactory(sl.k);
|
|
2788
2819
|
if (!factory) {
|
|
@@ -2798,32 +2829,160 @@ async function buildScene(opts) {
|
|
|
2798
2829
|
layer,
|
|
2799
2830
|
startMs: seg.startMs,
|
|
2800
2831
|
endMs: seg.endMs,
|
|
2832
|
+
segIndex,
|
|
2801
2833
|
screenPinned: SCREEN_PINNED_KEYS.has(sl.k)
|
|
2802
2834
|
});
|
|
2803
2835
|
}
|
|
2804
2836
|
}
|
|
2805
2837
|
const durationMs = visualTimelineMs(resolved);
|
|
2806
|
-
function
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2838
|
+
function dirVec(dir) {
|
|
2839
|
+
switch (dir) {
|
|
2840
|
+
case "right":
|
|
2841
|
+
return { x: 1, y: 0 };
|
|
2842
|
+
case "up":
|
|
2843
|
+
return { x: 0, y: -1 };
|
|
2844
|
+
case "down":
|
|
2845
|
+
return { x: 0, y: 1 };
|
|
2846
|
+
case "left":
|
|
2847
|
+
default:
|
|
2848
|
+
return { x: -1, y: 0 };
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
function transitionEnvs(tr, p) {
|
|
2852
|
+
if (tr.type === "push") {
|
|
2853
|
+
const v = dirVec(tr.direction);
|
|
2854
|
+
const span = Math.abs(v.x) ? W : H;
|
|
2855
|
+
return {
|
|
2856
|
+
incoming: { alpha: 1, dx: -v.x * span * (1 - p), dy: -v.y * span * (1 - p) },
|
|
2857
|
+
outgoing: { alpha: 1, dx: v.x * span * p, dy: v.y * span * p }
|
|
2858
|
+
};
|
|
2859
|
+
}
|
|
2860
|
+
if (tr.type === "wipe") {
|
|
2861
|
+
const v = dirVec(tr.direction);
|
|
2862
|
+
let inClip;
|
|
2863
|
+
let outClip;
|
|
2864
|
+
if (v.x !== 0) {
|
|
2865
|
+
const w = W * p;
|
|
2866
|
+
if (v.x < 0) {
|
|
2867
|
+
inClip = { x: 0, y: 0, w, h: H };
|
|
2868
|
+
outClip = { x: w, y: 0, w: W - w, h: H };
|
|
2869
|
+
} else {
|
|
2870
|
+
inClip = { x: W - w, y: 0, w, h: H };
|
|
2871
|
+
outClip = { x: 0, y: 0, w: W - w, h: H };
|
|
2872
|
+
}
|
|
2873
|
+
} else {
|
|
2874
|
+
const h = H * p;
|
|
2875
|
+
if (v.y < 0) {
|
|
2876
|
+
inClip = { x: 0, y: 0, w: W, h };
|
|
2877
|
+
outClip = { x: 0, y: h, w: W, h: H - h };
|
|
2878
|
+
} else {
|
|
2879
|
+
inClip = { x: 0, y: H - h, w: W, h };
|
|
2880
|
+
outClip = { x: 0, y: 0, w: W, h: H - h };
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
return {
|
|
2884
|
+
incoming: { alpha: 1, dx: 0, dy: 0, clip: inClip },
|
|
2885
|
+
outgoing: { alpha: 1, dx: 0, dy: 0, clip: outClip }
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
return {
|
|
2889
|
+
incoming: { alpha: p, dx: 0, dy: 0 },
|
|
2890
|
+
outgoing: { alpha: 1 - p, dx: 0, dy: 0 }
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2893
|
+
function frameplan(tMs) {
|
|
2894
|
+
const plan = [];
|
|
2895
|
+
for (let i = 0; i < resolved.length; i++) {
|
|
2896
|
+
const seg = resolved[i];
|
|
2897
|
+
if (tMs < seg.startMs || tMs >= seg.endMs) continue;
|
|
2898
|
+
const tr = seg.transition;
|
|
2899
|
+
const prev = i > 0 ? resolved[i - 1] : void 0;
|
|
2900
|
+
const enterEnd = tr ? seg.startMs + Math.max(0, tr.durationMs) : seg.startMs;
|
|
2901
|
+
const inEnter = tr && tr.durationMs > 0 && tMs < enterEnd && prev;
|
|
2902
|
+
if (!inEnter) {
|
|
2903
|
+
plan.push({ segIndex: i, drawTMs: tMs, env: null });
|
|
2904
|
+
continue;
|
|
2905
|
+
}
|
|
2906
|
+
const p = transitionProgress(tMs, seg.startMs, tr.durationMs, tr.easing ?? easeInOut);
|
|
2907
|
+
const { incoming, outgoing } = transitionEnvs(tr, p);
|
|
2908
|
+
const heldTMs = clamp(tMs, prev.startMs, prev.endMs - 1);
|
|
2909
|
+
plan.push({ segIndex: i - 1, drawTMs: heldTMs, env: outgoing });
|
|
2910
|
+
plan.push({ segIndex: i, drawTMs: tMs, env: incoming });
|
|
2911
|
+
}
|
|
2912
|
+
return plan;
|
|
2913
|
+
}
|
|
2914
|
+
function drawSegmentInto(target, segIndex, drawTMs) {
|
|
2915
|
+
const ctx = { ...baseCtx, ctx2d: target };
|
|
2916
|
+
const cam = camera(drawTMs);
|
|
2917
|
+
target.save();
|
|
2918
|
+
applyToContext(target, cam, W, H);
|
|
2812
2919
|
for (const b of bound) {
|
|
2813
|
-
if (b.screenPinned) continue;
|
|
2814
|
-
if (
|
|
2815
|
-
b.layer.draw(ctx,
|
|
2920
|
+
if (b.segIndex !== segIndex || b.screenPinned) continue;
|
|
2921
|
+
if (drawTMs < b.startMs || drawTMs >= b.endMs) continue;
|
|
2922
|
+
b.layer.draw(ctx, drawTMs);
|
|
2816
2923
|
}
|
|
2817
|
-
|
|
2924
|
+
target.restore();
|
|
2925
|
+
target.save();
|
|
2926
|
+
target.setTransform(1, 0, 0, 1, 0, 0);
|
|
2927
|
+
for (const b of bound) {
|
|
2928
|
+
if (b.segIndex !== segIndex || !b.screenPinned) continue;
|
|
2929
|
+
if (drawTMs < b.startMs || drawTMs >= b.endMs) continue;
|
|
2930
|
+
b.layer.draw(ctx, drawTMs);
|
|
2931
|
+
}
|
|
2932
|
+
target.restore();
|
|
2933
|
+
}
|
|
2934
|
+
function compositeEntry(ctx2d, e) {
|
|
2935
|
+
if (e.env === null) {
|
|
2936
|
+
drawSegmentInto(ctx2d, e.segIndex, e.drawTMs);
|
|
2937
|
+
return;
|
|
2938
|
+
}
|
|
2939
|
+
const env = e.env;
|
|
2940
|
+
if (!bufferFactory) {
|
|
2941
|
+
ctx2d.save();
|
|
2942
|
+
ctx2d.globalAlpha = ctx2d.globalAlpha * env.alpha;
|
|
2943
|
+
drawSegmentInto(ctx2d, e.segIndex, e.drawTMs);
|
|
2944
|
+
ctx2d.restore();
|
|
2945
|
+
return;
|
|
2946
|
+
}
|
|
2947
|
+
const buf = bufferFactory(W, H);
|
|
2948
|
+
drawSegmentInto(buf.ctx, e.segIndex, e.drawTMs);
|
|
2818
2949
|
ctx2d.save();
|
|
2819
2950
|
ctx2d.setTransform(1, 0, 0, 1, 0, 0);
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2951
|
+
if (env.alpha !== 1) ctx2d.globalAlpha = ctx2d.globalAlpha * env.alpha;
|
|
2952
|
+
if (env.clip) {
|
|
2953
|
+
ctx2d.beginPath();
|
|
2954
|
+
ctx2d.rect(env.clip.x, env.clip.y, env.clip.w, env.clip.h);
|
|
2955
|
+
ctx2d.clip();
|
|
2824
2956
|
}
|
|
2957
|
+
ctx2d.drawImage(buf.image, env.dx, env.dy, W, H);
|
|
2825
2958
|
ctx2d.restore();
|
|
2826
2959
|
}
|
|
2960
|
+
function renderFrame(ctx2d, tMs) {
|
|
2961
|
+
clock.nowMs = () => tMs;
|
|
2962
|
+
const ctx = { ...baseCtx, ctx2d };
|
|
2963
|
+
const cam = camera(tMs);
|
|
2964
|
+
if (!hasTransitions) {
|
|
2965
|
+
ctx2d.save();
|
|
2966
|
+
applyToContext(ctx2d, cam, W, H);
|
|
2967
|
+
for (const b of bound) {
|
|
2968
|
+
if (b.screenPinned) continue;
|
|
2969
|
+
if (tMs < b.startMs || tMs >= b.endMs) continue;
|
|
2970
|
+
b.layer.draw(ctx, tMs);
|
|
2971
|
+
}
|
|
2972
|
+
ctx2d.restore();
|
|
2973
|
+
ctx2d.save();
|
|
2974
|
+
ctx2d.setTransform(1, 0, 0, 1, 0, 0);
|
|
2975
|
+
for (const b of bound) {
|
|
2976
|
+
if (!b.screenPinned) continue;
|
|
2977
|
+
if (tMs < b.startMs || tMs >= b.endMs) continue;
|
|
2978
|
+
b.layer.draw(ctx, tMs);
|
|
2979
|
+
}
|
|
2980
|
+
ctx2d.restore();
|
|
2981
|
+
return;
|
|
2982
|
+
}
|
|
2983
|
+
const plan = frameplan(tMs);
|
|
2984
|
+
for (const e of plan) compositeEntry(ctx2d, e);
|
|
2985
|
+
}
|
|
2827
2986
|
function scheduleAudio(instrument, baseSec) {
|
|
2828
2987
|
let fired = 0;
|
|
2829
2988
|
for (const seg of resolved) {
|
|
@@ -3327,6 +3486,7 @@ export {
|
|
|
3327
3486
|
staffAnchor,
|
|
3328
3487
|
staffKeyboardRayFactory,
|
|
3329
3488
|
staffRayDemoSpec,
|
|
3489
|
+
transitionProgress,
|
|
3330
3490
|
validateChordTrack,
|
|
3331
3491
|
validateQuiz,
|
|
3332
3492
|
validateSections,
|