@real-music-packages/web-core 0.45.2 → 0.47.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.
@@ -0,0 +1,175 @@
1
+ declare function stampNoteIds(xml: string): string;
2
+ /**
3
+ * Insert `<print new-system="yes"/>` as the FIRST child of the `<measure>`
4
+ * at each 0-based position in `breakBeforeBarIndexes`, in EVERY `<part>`
5
+ * (a score's parts share one measure timeline, so a break must be encoded
6
+ * once per part for Verovio to line the systems up across staves). A
7
+ * position that is ≤ 0 or ≥ that part's own measure count is silently
8
+ * ignored FOR THAT PART (0 is a no-op — a part already starts a new system
9
+ * at its own first measure). Idempotent: a measure that already carries a
10
+ * `<print>` element (from a prior call, or from the source data) gets
11
+ * `new-system="yes"` SET on that existing element rather than gaining a
12
+ * second one — calling this twice with the same positions serializes
13
+ * identically both times. Malformed input (fails to parse) is returned
14
+ * unchanged, same defensive style as `stampNoteIds` above. Only ever ADDS
15
+ * `<print>` elements — never touches a `<note>` — so `stampNoteIds`'s
16
+ * position-based id scheme is unaffected by a prior or subsequent call to
17
+ * this function (see this file's own tests for the pinned invariant).
18
+ */
19
+ /**
20
+ * How many `<measure>` elements the FIRST `<part>` has — the `totalBars`
21
+ * that `barsPerLineBreaks`/`sectionAwareBreaks` plan against.
22
+ *
23
+ * The first part specifically, because that is the coordinate system
24
+ * `injectSystemBreaks` positions live in: a score's parts share one measure
25
+ * timeline, so the first part's ordinals index every part. And ORDINALS, not
26
+ * `<measure number>` attributes — scores skip and repeat bar numbers, so a
27
+ * count is the only safe answer. Malformed input, or a document with no
28
+ * part, returns 0 rather than throwing.
29
+ */
30
+ declare function measureCount(xml: string): number;
31
+ declare function injectSystemBreaks(xml: string, breakBeforeBarIndexes: readonly number[]): string;
32
+ /**
33
+ * Evenly-spread system-break positions for `totalMeasures` measures across
34
+ * `systems` systems: `[per, 2·per, …]` (each strictly `< totalMeasures`),
35
+ * with `per = Math.ceil(totalMeasures / systems)` — `Math.ceil` naturally
36
+ * front-loads any remainder into the EARLIER systems (e.g. 7 measures / 2
37
+ * systems -> per=4 -> systems of 4+3, never 3+4), which is what keeps a
38
+ * later system from being the short/orphaned one. `systems <= 1` (nothing to
39
+ * break between) returns `[]`.
40
+ */
41
+ declare function balancedSystemBreaks(totalMeasures: number, systems: number): number[];
42
+ /**
43
+ * Balanced system-break positions for a target BARS-PER-LINE layout — the
44
+ * caller-facing sibling of `balancedSystemBreaks`, which takes a system
45
+ * COUNT. Rather than stepping `every` bars at a time from bar 0 (which
46
+ * strands a short remainder — a 5-bar excerpt at `every: 4` renders 4+1, a
47
+ * widow of our own making), this decides how many LINES the excerpt needs
48
+ * (`ceil(totalBars / every)`) and hands that count to
49
+ * `balancedSystemBreaks`. `(5, 4)` is `[3]` (3+2) instead of `[4]` (4+1);
50
+ * `(9, 4)` is `[3, 6]` (3+3+3) instead of `[4, 8]` (4+4+1).
51
+ *
52
+ * WIDOW BACK-OFF: a small `every` against a small `totalBars` can make the
53
+ * straight `ceil(totalBars / every)` line count land on a trailing line of
54
+ * exactly 1 bar — the same widow this function exists to remove, reappearing
55
+ * at a different total/every combination (`lines = 3` on 7 bars: `per = 3`
56
+ * fills two 3-bar lines and leaves 1 bar for the third). The line count is
57
+ * therefore backed down (never below 1) until the trailing line holds at
58
+ * least 2 bars — 7 bars lands on 2 lines (4+3) instead of 3 (3+3+1), which
59
+ * is the same "fewer, fuller lines" direction `every` already asks for.
60
+ * This never changes the result for any total/every whose straight `ceil`
61
+ * count already avoids a 1-bar trailing line.
62
+ *
63
+ * `every <= 0` (the "auto — no forced breaks" sentinel callers resolve to)
64
+ * or a non-positive/non-finite `totalBars` returns `[]`. Never throws.
65
+ *
66
+ * Ported from stave-web-sightread (`src/lib/bach/xmlTransforms.ts`), whose
67
+ * copy this replaces — see the CONTRACT DUPLICATION NOTICE above. Reached by
68
+ * consumers through `VerovioLayoutOptions.barsPerLine` rather than called
69
+ * directly, so the player owns both the plan and its interaction with the
70
+ * widow pass and the readability floor.
71
+ */
72
+ declare function barsPerLineBreaks(totalBars: number, every: number): number[];
73
+ /**
74
+ * Like `barsPerLineBreaks`, but each SECTION's span is balanced on its own
75
+ * instead of one balance running across the whole excerpt — so a line never
76
+ * ends part-way into a neighbouring section's opening bars, and a section's
77
+ * own bars are never split into a widowed last line either.
78
+ *
79
+ * `sectionStarts` are 0-BASED POSITIONS into the excerpt (the same
80
+ * coordinate `injectSystemBreaks` consumes), NOT MusicXML `<measure
81
+ * number>` attributes — scores skip bar numbers, so a caller holding
82
+ * bar-numbered section labels must convert first. `0` (the excerpt's own
83
+ * start) is never a break and is ignored if present.
84
+ *
85
+ * Every section start > 0 is a HARD break in its own right — a new section
86
+ * always begins its own line, not subject to balancing — unioned with
87
+ * `barsPerLineBreaks`' balanced positions computed WITHIN each section's
88
+ * span (its start up to the next section's start, or `totalBars` for the
89
+ * last) and offset back into excerpt-absolute positions.
90
+ *
91
+ * `every <= 0` suppresses the balanced component, matching
92
+ * `barsPerLineBreaks`, but section-start breaks still apply — that is the
93
+ * "sections only, otherwise let the engraver decide" mode. Out-of-range and
94
+ * non-integer starts are ignored. The result is sorted, de-duplicated, and
95
+ * every entry is a valid `injectSystemBreaks` position (integer, `> 0`,
96
+ * `< totalBars`). Never throws.
97
+ */
98
+ declare function sectionAwareBreaks(totalBars: number, sectionStarts: readonly number[], every: number): number[];
99
+ /** One `<note>`'s MODEL identity — the ground truth `EngravedNote`
100
+ * (notationPlayerSvg.ts) needs that Verovio's rendered SVG cannot supply on
101
+ * its own (a rendered `g.note`/`g.rest` carries geometry, not pitch/tie/
102
+ * duration semantics). Keyed by the note's stamped `id` in
103
+ * `notePositions()`'s return of `verovioEngravedNotes`
104
+ * (notationPlayerVerovio.ts). */
105
+ interface NoteModel {
106
+ /** `12*(octave+1) + stepSemitone + alter` — the standard MusicXML→MIDI
107
+ * conversion (the same formula stave-web-sightread's own
108
+ * `practice/probeInputs.ts:pitchMidi` uses — a universal formula, not app
109
+ * logic, so this is not an owned-layer violation to restate here). `null`
110
+ * for a rest or an `<unpitched>` note (no `<pitch>` child) — mirrors
111
+ * `EngravedNote.midi`'s own "null covers both" contract. */
112
+ midi: number | null;
113
+ /** Has a `<rest/>` child. */
114
+ isRest: boolean;
115
+ /** True for the STOP half of a tie — a direct `<tie type="stop">` child OR
116
+ * `<notations><tied type="stop">` (exporters vary on which they emit;
117
+ * either counts) — matches `EngravedNote.tieContinuation`'s "continuation
118
+ * note of a tie, not the struck start" contract. */
119
+ tieContinuation: boolean;
120
+ /** 0-based, matching `EngravedNote.staffIndex`'s "0 = top staff of the
121
+ * system" contract: every `<part>` is walked in document order, and
122
+ * every DISTINCT staff within it (by `<attributes><staves>` when
123
+ * present, else the highest `<staff>` number any of its notes uses, else
124
+ * 1) is assigned the next index — so a single-part 2-staff piano score
125
+ * numbers 0/1 by `<staff>`, and a 2-part 1-staff-per-part reduction (this
126
+ * app's own chord+bass shape) numbers 0/1 by PART, with neither case
127
+ * needing different code. */
128
+ staffIndex: number;
129
+ /** Whole-note fraction (0.25 = quarter) — `duration / divisions / 4`,
130
+ * divisions tracked per-part from the LAST `<attributes><divisions>`
131
+ * seen at or before this note (MusicXML: divisions persist until
132
+ * overridden, default 1). 0 for a grace note (no `<duration>` child —
133
+ * the true, spec-correct signal; never guessed from `<type>`). */
134
+ durationReal: number;
135
+ /** 0-based position of this note's `<measure>` among its OWN `<part>`'s
136
+ * measure children, in document order. Informational only — the live
137
+ * join (`verovioOnsetColumns`/`verovioEngravedNotes`, both in
138
+ * notationPlayerVerovio.ts) resolves the note's RENDERED measure index
139
+ * from the live SVG DOM independently (Verovio's own render order, which
140
+ * is what geometry/hit-testing must agree with), never from this field. */
141
+ measureIndex: number;
142
+ /** `print-object="no"` on the source `<note>` (stave's `hideDoubledNotes`
143
+ * sets this on editorially-doubled notes/rests before handing MusicXML to
144
+ * this player). Verovio's importer HONORS this for `<note>` elements
145
+ * carrying a `<pitch>` (renders `visibility="hidden"` on its own,
146
+ * empirically confirmed against a real 6.2.0 render) but does NOT honor
147
+ * it for `<rest>` notes (the `<g class="rest">` renders fully visible
148
+ * regardless — same empirical check). `createVerovioNotationPlayer`
149
+ * reads this field to force `visibility="hidden"` after every render for
150
+ * ANY id where it's true — a no-op re-application on notes Verovio
151
+ * already hid, and the actual fix on the rests it doesn't (see that
152
+ * module's `applyPrintObjectHiding`). */
153
+ hidden: boolean;
154
+ }
155
+ /**
156
+ * Pure(ish) — MusicXML-in, `id → NoteModel`-out. Walks every `<part>` in
157
+ * document order, then every `<measure>` in document order, then every
158
+ * DIRECT child in document order — `<attributes>` updates the part's own
159
+ * `divisions` cursor; every other non-`<note>` child (`<backup>`,
160
+ * `<forward>`, `<direction>`, …) is a structural/timeline element this
161
+ * function has NO use for (it reads each note's OWN `<duration>` directly,
162
+ * never a cursor POSITION — see `durationReal`'s doc — so unlike
163
+ * `walkMeasureNotes`-style position walks, `<backup>`/`<forward>` need no
164
+ * special handling here beyond being correctly skipped, which plain
165
+ * tag-name filtering already does) — and every `<note>` becomes one model
166
+ * entry, keyed by its `id` attribute (a note with NO `id` — i.e. input that
167
+ * was never run through `stampNoteIds` — is silently skipped: it has no key
168
+ * to join the render against, so there is nothing useful to record).
169
+ *
170
+ * Never throws: malformed input (fails to parse, or no `<score-partwise>`
171
+ * root) returns an empty Map.
172
+ */
173
+ declare function noteModelFromXml(stampedXml: string): Map<string, NoteModel>;
174
+
175
+ export { type NoteModel, balancedSystemBreaks, barsPerLineBreaks, injectSystemBreaks, measureCount, noteModelFromXml, sectionAwareBreaks, stampNoteIds };
@@ -0,0 +1,19 @@
1
+ import {
2
+ balancedSystemBreaks,
3
+ barsPerLineBreaks,
4
+ injectSystemBreaks,
5
+ measureCount,
6
+ noteModelFromXml,
7
+ sectionAwareBreaks,
8
+ stampNoteIds
9
+ } from "./chunk-JCDNEJBT.js";
10
+ export {
11
+ balancedSystemBreaks,
12
+ barsPerLineBreaks,
13
+ injectSystemBreaks,
14
+ measureCount,
15
+ noteModelFromXml,
16
+ sectionAwareBreaks,
17
+ stampNoteIds
18
+ };
19
+ //# sourceMappingURL=notationXml.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -2,7 +2,7 @@ import { S as Score, a as ScoreFromMusicXMLOpts, T as TempoMap, b as ScoreNote }
2
2
  export { s as scoreFromMusicXML } from '../score-CLwSiAjn.js';
3
3
  import { A as AudioClock, a as LayerFactory, R as RenderCtx } from '../waveform-CdulBoeO.js';
4
4
  export { L as Layer, S as SpectrumInput, b as SpectrumProps, W as WaveformInput, c as WaveformProps, s as spectrumFactory, w as waveformFactory } from '../waveform-CdulBoeO.js';
5
- import { PromoTheme, RecordOpts, Scene, SafeBox } from '../video.js';
5
+ import { SafeBox, PromoTheme, RecordOpts, Scene } from '../video.js';
6
6
  import { RenderedNotation, Box } from '../promo.js';
7
7
  import { N as NotationLayout } from '../notationGeometry-DqVBgL7F.js';
8
8
  export { F as FOLLOW_BARS, a as FOLLOW_PAD, b as NotationLayoutOpts, c as NotationRect, P as PlayheadLine, d as audioPlayheadLine, e as cropAroundBox, f as cubicEaseInOut, g as distinctMeasureIndices, h as distinctOnsets, i as firstMeasureBox, j as followBoxAt, k as followWindowStart, m as lerpBox, n as measureColumnsFromLayout, o as measureCount, p as measureSpanBox, q as measureSystemMap, r as notationLayout, s as playheadLine, t as systemBox, v as vstackAudioPlayheadLine, u as vstackFollowBox } from '../notationGeometry-DqVBgL7F.js';
@@ -153,6 +153,52 @@ declare function applyToContext(ctx: CanvasRenderingContext2D, cam: CameraState,
153
153
  /** The identity (no pan/zoom) camera: world == viewport. */
154
154
  declare function identityCamera(W: number, H: number): CameraState;
155
155
 
156
+ /** A sub-rect of the frame, in fractions of W/H (0..1, top-left origin). */
157
+ interface Region {
158
+ x: number;
159
+ y: number;
160
+ w: number;
161
+ h: number;
162
+ }
163
+ /** The whole frame. A layer with this region renders exactly as an unregioned one. */
164
+ declare const FULL_REGION: Region;
165
+ /** True when `r` covers the entire frame (the runner then skips the transform). */
166
+ declare function isFullRegion(r: Region): boolean;
167
+ /** A region's pixel rect in FRAME space. */
168
+ declare function regionRect(r: Region, W: number, H: number): {
169
+ x: number;
170
+ y: number;
171
+ w: number;
172
+ h: number;
173
+ };
174
+ /**
175
+ * Structural validation for a region (used by buildScene + the gate so a bad
176
+ * pane fails before capture rather than rendering off-frame).
177
+ * Returns [] when valid.
178
+ */
179
+ declare function validateRegion(r: unknown): string[];
180
+ /**
181
+ * The usable content rect for a layer drawing inside `r`, in PANE-LOCAL px.
182
+ *
183
+ * This is `safeBox(W, H)` clipped to the pane and shifted into pane
184
+ * coordinates — NOT `safeBox(r.w*W, r.h*H)`, which re-applies the frame's
185
+ * percentage insets to the pane and silently invents room that phone chrome
186
+ * covers.
187
+ *
188
+ * Worked example, 1080x1920, bottom pane {y:0.42, h:0.58} (pane top 806.4,
189
+ * pane height 1113.6):
190
+ * naive safeBox(1080, 1113.6).bottom = 801.8 pane-local = 1608.2 frame-space
191
+ * paneSafeBox(...).bottom = 576.0 pane-local = 1382.4 frame-space
192
+ * The frame's real bottom-safe line is 0.72*1920 = 1382.4, so the naive value
193
+ * hands the layer ~226px of content area underneath TikTok's caption + action
194
+ * rail. The top pane fails inverted (naive top-safe 121 vs the real 288),
195
+ * putting a title under the search bar.
196
+ *
197
+ * A pane that misses the frame safe box entirely yields a zero-size box (w/h 0)
198
+ * anchored at the clamped edge, rather than a negative-size one.
199
+ */
200
+ declare function paneSafeBox(r: Region, W: number, H: number): SafeBox;
201
+
156
202
  /** A timeline endpoint: a number (ms? no — seconds), "end", or "end-N" (N s before end). */
157
203
  type TimeAnchor = number | 'end' | `end-${number}`;
158
204
  interface SpecLayer {
@@ -160,6 +206,19 @@ interface SpecLayer {
160
206
  k: string;
161
207
  /** Props passed to the layer's init(). */
162
208
  p?: unknown;
209
+ /**
210
+ * Optional sub-rect of the frame to draw this layer into (fractions of W/H).
211
+ * The layer is clipped + translated into the pane and sees a RenderCtx whose
212
+ * `W`/`H` are the PANE's size and whose `safeBox` is `paneSafeBox` (the frame
213
+ * safe box intersected with the pane) — so any existing layer composites into
214
+ * a split-screen pane unchanged. Omit (or pass the full frame) for today's
215
+ * behavior; a full-frame region is detected and short-circuited so the op
216
+ * stream stays byte-for-byte identical.
217
+ *
218
+ * The camera transform is FRAME-space and is applied before the region
219
+ * transform, so the camera pans a pane's CONTENTS, not the pane itself.
220
+ */
221
+ region?: Region;
163
222
  }
164
223
  /**
165
224
  * Optional per-segment audio. Lets a segment SCHEDULE sound at its own start
@@ -374,6 +433,355 @@ interface RecordSceneSpecOpts {
374
433
  */
375
434
  declare function recordSceneSpec(opts: RecordSceneSpecOpts): Promise<Blob>;
376
435
 
436
+ type VideoFit = 'cover' | 'contain';
437
+ type VideoClockMode = 'play' | 'seek';
438
+ interface VideoSourceProps {
439
+ /** A URL (loaded + awaited in init) or any ready CanvasImageSource. */
440
+ src: string | CanvasImageSource;
441
+ /** How the source fills the box. Default 'cover'. */
442
+ fit?: VideoFit;
443
+ /**
444
+ * Focal point of the source for `cover` cropping, in source fractions.
445
+ * Default {x: 0.5, y: 0.38} — faces sit above the vertical centre, and a
446
+ * centred crop of a portrait clip cuts the forehead off.
447
+ */
448
+ focus?: {
449
+ x: number;
450
+ y: number;
451
+ };
452
+ /** Absolute clip time (ms) at which this source starts playing. Default 0. */
453
+ startMs?: number;
454
+ /** In-point within the SOURCE (ms). Default 0. */
455
+ sourceStartMs?: number;
456
+ /** Loop [sourceStartMs, duration) when the segment outlasts the source. Default true. */
457
+ loop?: boolean;
458
+ /** Playback rate. Default 1. */
459
+ rate?: number;
460
+ /** Mute the element. Default true — reaction audio is discarded in v1. */
461
+ mute?: boolean;
462
+ /** Constant alpha. Default 1. */
463
+ opacity?: number;
464
+ /** 'play' (real-time capture, default) or 'seek' (offline; not yet supported). */
465
+ clockMode?: VideoClockMode;
466
+ /** Drift past which `play` mode issues a corrective seek, ms. Default 120. */
467
+ driftTolMs?: number;
468
+ /**
469
+ * Source duration in ms. Read from the element when it can be; required when
470
+ * `src` is a bare CanvasImageSource AND `loop` is on (nothing to read it from).
471
+ */
472
+ durationMs?: number;
473
+ }
474
+ /** Source-crop + destination rects for one drawImage call. */
475
+ interface FitBoxes {
476
+ sx: number;
477
+ sy: number;
478
+ sw: number;
479
+ sh: number;
480
+ dx: number;
481
+ dy: number;
482
+ dw: number;
483
+ dh: number;
484
+ }
485
+ /**
486
+ * Fit a source (iW×iH) into a box (bw×bh at the origin).
487
+ * - 'cover' : fills the box, crops the source around `focus` (clamped so the
488
+ * crop window never runs off the source).
489
+ * - 'contain' : whole source visible, letterboxed and centred in the box.
490
+ */
491
+ declare function fitBoxes(iW: number, iH: number, bw: number, bh: number, mode: VideoFit, focus: {
492
+ x: number;
493
+ y: number;
494
+ }): FitBoxes;
495
+ /**
496
+ * The source position (ms) to show at absolute clip time `tMs`.
497
+ * Before `startMs` the in-point is held. When `loop`, playback wraps within
498
+ * [sourceStartMs, durationMs); otherwise the last frame is held.
499
+ */
500
+ declare function sourceTimeMs(tMs: number, o: {
501
+ startMs: number;
502
+ sourceStartMs: number;
503
+ rate: number;
504
+ loop: boolean;
505
+ durationMs: number;
506
+ }): number;
507
+ /** Whether `play` mode should issue a corrective seek this frame. */
508
+ declare function needsSeek(currentMs: number, wantMs: number, tolMs: number): boolean;
509
+ /** Intrinsic size of any CanvasImageSource we might be handed. */
510
+ declare function sourceSize(src: unknown): {
511
+ w: number;
512
+ h: number;
513
+ };
514
+ declare const videoSourceFactory: LayerFactory<VideoSourceProps>;
515
+
516
+ type SeamStyle = 'line' | 'shadow';
517
+ interface PaneSeamProps {
518
+ /** Where the seam sits, as a fraction of the frame (H for 'h', W for 'v'). */
519
+ atFrac: number;
520
+ /** Seam axis. 'h' = a horizontal rule between stacked panes. Default 'h'. */
521
+ orientation?: 'h' | 'v';
522
+ /** 'line' = a hard rule; 'shadow' = a rule plus a soft falloff either side. */
523
+ style?: SeamStyle;
524
+ /** Rule colour. Default theme.ink. */
525
+ color?: string;
526
+ /** Rule thickness in px. Default 4. */
527
+ thicknessPx?: number;
528
+ /** Falloff depth either side for 'shadow', px. Default 28. */
529
+ shadowPx?: number;
530
+ /** Rule alpha. Default 0.9. */
531
+ opacity?: number;
532
+ }
533
+ declare const paneSeamFactory: LayerFactory<PaneSeamProps>;
534
+
535
+ interface ReactionSplitOpts {
536
+ /** The top pane: a reaction clip. */
537
+ reaction: VideoSourceProps;
538
+ /**
539
+ * The bottom pane: ANY layers from the registry. A live music scene
540
+ * (`notation` + `scroll-cursor`, `falling-notes` + `keyboard`) and a
541
+ * pre-recorded app screen capture (another `video-source`) are both just
542
+ * SpecLayers — the region machinery does not care which.
543
+ */
544
+ app: SpecLayer[];
545
+ /** The text hook, drawn over BOTH panes and pinned to the frame. */
546
+ hook: string;
547
+ /** Top-pane height as a fraction of the frame. Default 0.42. */
548
+ split?: number;
549
+ /** Clip length. Default 8. */
550
+ durationSec?: number;
551
+ /** Divider treatment between the panes. Default 'line'. */
552
+ seam?: SeamStyle | 'none';
553
+ /** Optional follow/CTA card, entering at `atSec`. */
554
+ endCard?: {
555
+ text?: string;
556
+ handle?: string;
557
+ atSec: number;
558
+ };
559
+ /** Frame size. Default [1080, 1920]. */
560
+ size?: [number, number];
561
+ /** Theme key. Default 'default'. */
562
+ theme?: string;
563
+ /** Capture fps. Default 30. */
564
+ fps?: number;
565
+ /** Hook vertical centre as a fraction of the frame. Default just below the
566
+ * frame's top-safe line, so it clears the platform's tabs/search row. */
567
+ hookCenterFrac?: number;
568
+ /** Hook font px. Default 84. */
569
+ hookFontPx?: number;
570
+ /** Hook colour. Default the theme's paper (hooks sit over footage). */
571
+ hookColor?: string;
572
+ }
573
+ /** Default hook centre: inside the safe area, high enough to read as a caption. */
574
+ declare const DEFAULT_HOOK_CENTER_FRAC: number;
575
+ /**
576
+ * The reaction-overlay short-form layout: a reaction clip above, the app doing
577
+ * one small thing below, a text hook over both.
578
+ *
579
+ * Throws on a structurally impossible split so the caller fails here rather
580
+ * than at buildScene.
581
+ */
582
+ declare function reactionSplitSpec(o: ReactionSplitOpts): SceneSpec;
583
+
584
+ /** The emotional register a clip reads as — the axis variants are drawn along. */
585
+ type ReactionEmotion = 'confused' | 'shocked' | 'impressed' | 'delighted' | 'deadpan' | 'skeptical';
586
+ declare const REACTION_EMOTIONS: readonly ReactionEmotion[];
587
+ interface ReactionClip {
588
+ /** Stable id, unique within the manifest. */
589
+ id: string;
590
+ /** File name / path, resolved against the manifest's base URL. */
591
+ file: string;
592
+ emotion: ReactionEmotion;
593
+ durationMs: number;
594
+ /** [x, y, w, h] in source fractions — where the face sits. Drives the crop. */
595
+ faceBox?: [number, number, number, number];
596
+ /** REQUIRED provenance: where the footage came from (e.g. "envato-elements"). */
597
+ source: string;
598
+ /** REQUIRED provenance: the licence/receipt id proving the right to use it. */
599
+ licenseId: string;
600
+ /** ISO date the licence was obtained. */
601
+ acquiredAt?: string;
602
+ notes?: string;
603
+ }
604
+ interface ReactionManifest {
605
+ clips: ReactionClip[];
606
+ }
607
+ /** Focus used when a clip declares no faceBox — faces sit above centre. */
608
+ declare const DEFAULT_FOCUS: {
609
+ x: number;
610
+ y: number;
611
+ };
612
+ /**
613
+ * Validate a parsed manifest. Returns [] when valid; every problem is reported,
614
+ * not just the first, so one audit run fixes the whole file.
615
+ */
616
+ declare function validateReactionManifest(json: unknown): string[];
617
+ /** Throwing form, for a build script. */
618
+ declare function assertReactionManifest(json: unknown): ReactionManifest;
619
+ /** The cover-crop focal point for a clip: its face centre, or the default. */
620
+ declare function clipFocus(clip: Pick<ReactionClip, 'faceBox'>): {
621
+ x: number;
622
+ y: number;
623
+ };
624
+ interface ReactionPropsOpts {
625
+ /** Prefix joined to `clip.file` (e.g. "/assets/reactions/"). Default "". */
626
+ baseUrl?: string;
627
+ /** Absolute clip time the reaction starts at. Default 0. */
628
+ startMs?: number;
629
+ /** In-point within the source. Default 0. */
630
+ sourceStartMs?: number;
631
+ loop?: boolean;
632
+ rate?: number;
633
+ }
634
+ /** Turn a manifest entry into `video-source` props, focus already aimed. */
635
+ declare function reactionProps(clip: ReactionClip, o?: ReactionPropsOpts): VideoSourceProps;
636
+ /** Clips matching an emotion (all of them when `emotion` is omitted). */
637
+ declare function pickClips(m: ReactionManifest, emotion?: ReactionEmotion): ReactionClip[];
638
+
639
+ interface CheckFrame {
640
+ w: number;
641
+ h: number;
642
+ data: Uint8Array | Uint8ClampedArray | number[];
643
+ }
644
+ interface SampledFrames {
645
+ show: {
646
+ tSec: number;
647
+ f: CheckFrame;
648
+ }[];
649
+ /** Frame size the samples were decoded at (may differ from the source size). */
650
+ frameW: number;
651
+ frameH: number;
652
+ }
653
+ interface CheckResult {
654
+ name: string;
655
+ pass: boolean;
656
+ detail: string;
657
+ }
658
+ /** Luminance floor below which a pixel counts as ink on a paper background. */
659
+ declare const PAPER_LUM_FLOOR = 200;
660
+ /**
661
+ * Fraction of a row's [x0,x1) span that reads as content rather than paper:
662
+ * clearly darker than paper, or clearly chromatic. Mirrors the heuristic the
663
+ * existing promo checks use so thresholds carry over.
664
+ */
665
+ declare function inkFractionInRow(f: CheckFrame, y: number, x0: number, x1: number): number;
666
+ interface PaneSafetyOpts {
667
+ /** The content pane to enforce (usually the app pane). */
668
+ region: Region;
669
+ /** Source-space frame size the region fractions refer to. Default 1080x1920. */
670
+ srcW?: number;
671
+ srcH?: number;
672
+ /** Ink fraction tolerated in the unsafe strip. Default 0.08. */
673
+ maxInkFrac?: number;
674
+ }
675
+ /**
676
+ * The regression guard for the paneSafeBox trap: a layer handed a pane must not
677
+ * draw content BELOW the frame's bottom-safe line, where the platform's caption
678
+ * and action rail sit. Computing a pane's safe box as `safeBox(paneW, paneH)`
679
+ * instead of intersecting with the frame silently grants ~226px of that strip,
680
+ * and the resulting clip only looks wrong once it is posted.
681
+ *
682
+ * Only meaningful for panes whose content is meant to be READ (notation, text,
683
+ * a keyboard). A full-bleed footage pane fills its whole rect by design and
684
+ * should not be enforced.
685
+ */
686
+ declare function checkPaneSafety(frames: SampledFrames, o: PaneSafetyOpts): CheckResult;
687
+ interface SeamPresentOpts {
688
+ /** Where the seam should be, as a fraction of the frame height. */
689
+ splitFrac: number;
690
+ srcH?: number;
691
+ /** Rows either side to search for the rule. Default 6 (source px). */
692
+ tolPx?: number;
693
+ /** Minimum span of the row that must read as seam ink. Default 0.9. */
694
+ minSpan?: number;
695
+ }
696
+ /**
697
+ * A composition regression guard: if the split silently collapsed (one pane
698
+ * covering the frame, a preset emitting no seam), the divider row disappears.
699
+ * Cheap to check and it fails loudly on the whole-layout failure mode that the
700
+ * per-layer checks cannot see.
701
+ */
702
+ declare function checkSeamPresent(frames: SampledFrames, o: SeamPresentOpts): CheckResult;
703
+ interface FaceInPaneOpts {
704
+ /** [x, y, w, h] fractions of the SOURCE. */
705
+ faceBox: [number, number, number, number];
706
+ /** Intrinsic source size. */
707
+ srcW: number;
708
+ srcH: number;
709
+ /** The pane the source is drawn into. */
710
+ region: Region;
711
+ frameW?: number;
712
+ frameH?: number;
713
+ fit?: VideoFit;
714
+ focus: {
715
+ x: number;
716
+ y: number;
717
+ };
718
+ }
719
+ interface FaceInPaneResult {
720
+ ok: boolean;
721
+ /** How much of the declared face box survives the crop, 0..1. */
722
+ visibleFrac: number;
723
+ /** The face's rect in FRAME pixels (empty when fully cropped out). */
724
+ frameRect: {
725
+ x: number;
726
+ y: number;
727
+ w: number;
728
+ h: number;
729
+ };
730
+ reason?: string;
731
+ }
732
+ /**
733
+ * Whether the face a clip declares actually lands inside its pane after the
734
+ * cover-crop. This is pure geometry, not pixel face detection — the manifest
735
+ * already states where the face is, so the honest check is "did our own crop
736
+ * math keep it", which is exact rather than heuristic.
737
+ */
738
+ declare function faceLandsInPane(o: FaceInPaneOpts): FaceInPaneResult;
739
+
740
+ /** One ending treatment. `null` means "no end card on this variant". */
741
+ type VariantEnding = NonNullable<ReactionSplitOpts['endCard']> | null;
742
+ interface ReactionVariantsOpts {
743
+ /** Text hooks to rotate through. At least one. */
744
+ hooks: string[];
745
+ /** Reaction clips to rotate through. At least one. */
746
+ clips: ReactionClip[];
747
+ /** Ending treatments. Default `[null]` (no end card). */
748
+ endings?: VariantEnding[];
749
+ /** Everything the layout needs that is NOT a variant axis (app layers, split…). */
750
+ base: Omit<ReactionSplitOpts, 'reaction' | 'hook' | 'endCard'>;
751
+ /** How many variants to emit. Default: the full product (every combination). */
752
+ n?: number;
753
+ /** Prefix joined to each clip's file. */
754
+ baseUrl?: string;
755
+ /** Deterministic starting offset — change it to get a different ordering. */
756
+ seed?: number;
757
+ }
758
+ interface ReactionVariant {
759
+ /** Stable id derived from the axis picks; the same picks always yield it. */
760
+ id: string;
761
+ hook: string;
762
+ clipId: string;
763
+ endingIndex: number;
764
+ spec: SceneSpec;
765
+ }
766
+ /**
767
+ * A step size co-prime to `total`, near the golden-ratio fraction of it so the
768
+ * walk spreads rather than marching. Co-primality is what guarantees the walk
769
+ * visits every index exactly once before repeating.
770
+ */
771
+ declare function coprimeStride(total: number): number;
772
+ /**
773
+ * `n` index tuples over axes of the given `sizes`, distinct until the product is
774
+ * exhausted. Exported so the decorrelation is testable without building specs.
775
+ */
776
+ declare function variantIndices(n: number, sizes: number[], seed?: number): number[][];
777
+ /** Lowercase, hyphenated, ascii-ish slug for a variant id. */
778
+ declare function slugify(s: string, max?: number): string;
779
+ /**
780
+ * Generate `n` distinct reaction-split clips across the hook / clip / ending
781
+ * axes. Throws on an empty axis rather than silently emitting nothing.
782
+ */
783
+ declare function reactionVariants(o: ReactionVariantsOpts): ReactionVariant[];
784
+
377
785
  /** Register (or replace) a layer factory under its key. */
378
786
  declare function registerLayer(factory: LayerFactory<any>): void;
379
787
  /** Look up a factory by key, or undefined if not registered. */
@@ -1802,4 +2210,4 @@ interface HandIndicatorProps {
1802
2210
  }
1803
2211
  declare const handIndicatorFactory: LayerFactory<HandIndicatorProps>;
1804
2212
 
1805
- export { type Affine, AudioClock, type AudioEvent, type BackgroundProps, type BeatGridOpts, type BeatPulseProps, type BeatTick, type BrandingProps, type BufferFactory, type BuildSceneOpts, type BuiltScene, type CameraState, type CaptionCue, type CaptionProps, type CaptionScript, type CaptionStyle, type ChordRibbonProps, type ChordSpan, type CircleOfFifthsProps, type CirclePoint, type ClickTrackOpts, type ColorBy, type CompositeRhythmProps, type ContourPlot, type ContourPoint, type CountInOpts, type CountdownProps, type CountingTrackProps, type CtaProps, DEFAULT_FUNCTION_COLORS, type DegreeLabel, type DegreeLabelsProps, type DroneOpts, type DuckWindow, type Easing, type EndCardProps, type ExtendedDemoOpts, FIFTHS_MAJOR, FIFTHS_MINOR, type FallingKeyboardDemoOpts, type FallingNotesProps, type FretboardProps, type FunctionColors, type FunctionalHarmonyProps, type GateError, type GateInput, type HandColors, type HandIndicatorProps, type HarmonicFunction, type HarmonyMode, type HighlightRegion, type HookProps, type ImageRevealMode, type ImageRevealProps, type IntervalArcsProps, type KaraokeCaptionProps, type KaraokeWord, type KeyRect, type KeyboardLayout, type KeyboardLayoutOpts, type KeyboardProps, type KineticMode, type KineticTextProps, type LabelMode, LayerFactory, type McqCardProps, type NotationEngraving, NotationLayout, type NotationProps, type OutputProbe, PIANO_HIGH, PIANO_LOW, type ParticleBurstProps, type PitchContourProps, type Placement, type PortraitProps, type ProgressRingProps, type PromoCardsDemoOpts, type PulseStyle, type Quiz, type QuizOption, type QuizPhase, type RadialSpectrumProps, type RayEndpoints, type RecordSceneSpecOpts, type Rect, RenderCtx, type ResolvedSegment, type RevealEasing, type RevealProps, SCALE_INTERVALS, type SafeGuidesProps, type ScaleHighlightProps, type SceneSpec, type ScheduleTarget, Score, ScoreFromMusicXMLOpts, ScoreNote, type ScrollCursorProps, type Section, type SectionMinimapProps, type SegmentAudio, type SegmentAudioCtx, type SegmentTransition, type SpecLayer, type StaffKeyboardRayProps, type StatCounterProps, TempoMap, type TensionGraphProps, type TensionPoint, type TextureProps, type TimeAnchor, type TimelineSegment, activeChord, activeCue, activeSection, animatedSlot, applySchedule, applyToContext, assertGate, ballArc, ballX, beatGrid, beatPhase, beatPulseFactory, blackKeys, bpmOf, brandingFactory, buildScene, cameraForFollow, cameraTransform, chordRibbonFactory, circleOfFifthsDemoSpec, circleOfFifthsFactory, clamp, clickTrackSchedule, compositeRhythmFactory, contourMinimapDemoSpec, contourPoints, contourPolyline, countInLeadSec, countInSchedule, countdownFactory, countdownRemaining, countdownSeconds, countingDegreeDemoSpec, countingTrackFactory, ctaFactory, cueOpacity, degreeLabel, degreeLabelsFactory, dotAt, drawCaption, drawHighlight, droneSchedule, duckGainAt, easeIn, easeInOut, easeOut, endCardFactory, fallingKeyboardDemoScore, fallingKeyboardDemoSpec, fallingNotesFactory, followSrcBox, fracSlotPoint, frameRect, fretboardFactory, functionColor, functionalHarmonyFactory, getKeyboardLayout, getLayerFactory, getNotationEngraving, handIndicatorFactory, harmonyDemoSpec, highlightIntensity, hookFactory, identityCamera, imageRevealFactory, inRange, intervalArcsFactory, invLerp, isBlackKey, karaokeCaptionFactory, kenBurns, keyCenterX, keyColumnWidth, keyRect, keySlot, keyboardFactory, keyboardLayout, kineticTextFactory, lerp, lerpCamera, linear, mapBoxThroughLayout, mcqCardFactory, mcqDemoSpec, measureSpans, timeToX as minimapTimeToX, msPerBeat, notationFactory, noteColor, noteSetXRange, parseKey, parseTimeSig, particleBurstFactory, pcToSlot, pitchAt, pitchContourFactory, pitchRange, portraitFactory, progress01, progressRingFactory, projectPoint, promoCardsDemoSpec, pulseAt, quizPhase, radialSpectrumFactory, rayEndpoints, rayPointAt, recordSceneSpec, registerLayer, registeredKeys, resolveAnchor, resolveKeyboardLayout, resolveTimeline, revealFactory, revealProgress, runGate, safeGuidesFactory, scaleHighlightFactory, scorePitchSpan, scrollCursorFactory, sectionMinimapFactory, setFollowLayoutProvider, setKeyboardLayout, setNotationEngraving, slotAngle, slotPc, slotPoint, staffAnchor, staffKeyboardRayFactory, staffRayDemoSpec, statCounterFactory, tensionGraphFactory, textureFactory, transitionProgress, validateChordTrack, validateQuiz, validateSections, visualTimelineMs, whiteKeys, worldToViewport };
2213
+ export { type Affine, AudioClock, type AudioEvent, type BackgroundProps, type BeatGridOpts, type BeatPulseProps, type BeatTick, type BrandingProps, type BufferFactory, type BuildSceneOpts, type BuiltScene, type CameraState, type CaptionCue, type CaptionProps, type CaptionScript, type CaptionStyle, type CheckFrame, type CheckResult, type ChordRibbonProps, type ChordSpan, type CircleOfFifthsProps, type CirclePoint, type ClickTrackOpts, type ColorBy, type CompositeRhythmProps, type ContourPlot, type ContourPoint, type CountInOpts, type CountdownProps, type CountingTrackProps, type CtaProps, DEFAULT_FOCUS, DEFAULT_FUNCTION_COLORS, DEFAULT_HOOK_CENTER_FRAC, type DegreeLabel, type DegreeLabelsProps, type DroneOpts, type DuckWindow, type Easing, type EndCardProps, type ExtendedDemoOpts, FIFTHS_MAJOR, FIFTHS_MINOR, FULL_REGION, type FaceInPaneOpts, type FaceInPaneResult, type FallingKeyboardDemoOpts, type FallingNotesProps, type FitBoxes, type FretboardProps, type FunctionColors, type FunctionalHarmonyProps, type GateError, type GateInput, type HandColors, type HandIndicatorProps, type HarmonicFunction, type HarmonyMode, type HighlightRegion, type HookProps, type ImageRevealMode, type ImageRevealProps, type IntervalArcsProps, type KaraokeCaptionProps, type KaraokeWord, type KeyRect, type KeyboardLayout, type KeyboardLayoutOpts, type KeyboardProps, type KineticMode, type KineticTextProps, type LabelMode, LayerFactory, type McqCardProps, type NotationEngraving, NotationLayout, type NotationProps, type OutputProbe, PAPER_LUM_FLOOR, PIANO_HIGH, PIANO_LOW, type PaneSafetyOpts, type PaneSeamProps, type ParticleBurstProps, type PitchContourProps, type Placement, type PortraitProps, type ProgressRingProps, type PromoCardsDemoOpts, type PulseStyle, type Quiz, type QuizOption, type QuizPhase, REACTION_EMOTIONS, type RadialSpectrumProps, type RayEndpoints, type ReactionClip, type ReactionEmotion, type ReactionManifest, type ReactionPropsOpts, type ReactionSplitOpts, type ReactionVariant, type ReactionVariantsOpts, type RecordSceneSpecOpts, type Rect, type Region, RenderCtx, type ResolvedSegment, type RevealEasing, type RevealProps, SCALE_INTERVALS, type SafeGuidesProps, type SampledFrames, type ScaleHighlightProps, type SceneSpec, type ScheduleTarget, Score, ScoreFromMusicXMLOpts, ScoreNote, type ScrollCursorProps, type SeamPresentOpts, type SeamStyle, type Section, type SectionMinimapProps, type SegmentAudio, type SegmentAudioCtx, type SegmentTransition, type SpecLayer, type StaffKeyboardRayProps, type StatCounterProps, TempoMap, type TensionGraphProps, type TensionPoint, type TextureProps, type TimeAnchor, type TimelineSegment, type VariantEnding, type VideoClockMode, type VideoFit, type VideoSourceProps, activeChord, activeCue, activeSection, animatedSlot, applySchedule, applyToContext, assertGate, assertReactionManifest, ballArc, ballX, beatGrid, beatPhase, beatPulseFactory, blackKeys, bpmOf, brandingFactory, buildScene, cameraForFollow, cameraTransform, checkPaneSafety, checkSeamPresent, chordRibbonFactory, circleOfFifthsDemoSpec, circleOfFifthsFactory, clamp, clickTrackSchedule, clipFocus, compositeRhythmFactory, contourMinimapDemoSpec, contourPoints, contourPolyline, coprimeStride, countInLeadSec, countInSchedule, countdownFactory, countdownRemaining, countdownSeconds, countingDegreeDemoSpec, countingTrackFactory, ctaFactory, cueOpacity, degreeLabel, degreeLabelsFactory, dotAt, drawCaption, drawHighlight, droneSchedule, duckGainAt, easeIn, easeInOut, easeOut, endCardFactory, faceLandsInPane, fallingKeyboardDemoScore, fallingKeyboardDemoSpec, fallingNotesFactory, fitBoxes, followSrcBox, fracSlotPoint, frameRect, fretboardFactory, functionColor, functionalHarmonyFactory, getKeyboardLayout, getLayerFactory, getNotationEngraving, handIndicatorFactory, harmonyDemoSpec, highlightIntensity, hookFactory, identityCamera, imageRevealFactory, inRange, inkFractionInRow, intervalArcsFactory, invLerp, isBlackKey, isFullRegion, karaokeCaptionFactory, kenBurns, keyCenterX, keyColumnWidth, keyRect, keySlot, keyboardFactory, keyboardLayout, kineticTextFactory, lerp, lerpCamera, linear, mapBoxThroughLayout, mcqCardFactory, mcqDemoSpec, measureSpans, timeToX as minimapTimeToX, msPerBeat, needsSeek, notationFactory, noteColor, noteSetXRange, paneSafeBox, paneSeamFactory, parseKey, parseTimeSig, particleBurstFactory, pcToSlot, pickClips, pitchAt, pitchContourFactory, pitchRange, portraitFactory, progress01, progressRingFactory, projectPoint, promoCardsDemoSpec, pulseAt, quizPhase, radialSpectrumFactory, rayEndpoints, rayPointAt, reactionProps, reactionSplitSpec, reactionVariants, recordSceneSpec, regionRect, registerLayer, registeredKeys, resolveAnchor, resolveKeyboardLayout, resolveTimeline, revealFactory, revealProgress, runGate, safeGuidesFactory, scaleHighlightFactory, scorePitchSpan, scrollCursorFactory, sectionMinimapFactory, setFollowLayoutProvider, setKeyboardLayout, setNotationEngraving, slotAngle, slotPc, slotPoint, slugify, sourceSize, sourceTimeMs, staffAnchor, staffKeyboardRayFactory, staffRayDemoSpec, statCounterFactory, tensionGraphFactory, textureFactory, transitionProgress, validateChordTrack, validateQuiz, validateReactionManifest, validateRegion, validateSections, variantIndices, videoSourceFactory, visualTimelineMs, whiteKeys, worldToViewport };