@braccato/core 1.13.2 → 1.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/README.md CHANGED
@@ -345,14 +345,15 @@ refactor. Import them from `@braccato/core/constants` instead of typing them out
345
345
 
346
346
  ## Stylesheets
347
347
 
348
- Three sheets ship with the package, and loading them is yours, the way any package's CSS is. Leave
348
+ Four sheets ship with the package, and loading them is yours, the way any package's CSS is. Leave
349
349
  them out and you get lines that are in the document and unstyled, rather than lines that are missing.
350
350
 
351
351
  | File | What it carries |
352
352
  | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
353
- | `@braccato/core/styles/variables.css` | Every `--blyrics-*` default. It goes first, because the other two read from it. |
353
+ | `@braccato/core/styles/variables.css` | Every `--blyrics-*` default. It goes first, because the others read from it. |
354
354
  | `@braccato/core/styles/lyrics.css` | The container, the lines, the words and the sweep, plus two `@property` registrations the word animation interpolates through. |
355
355
  | `@braccato/core/styles/instrumental.css` | The waveform that fills a bar nobody sings over, and the animation that walks it. |
356
+ | `@braccato/core/styles/stage.css` | Placement for the stage layout, and nothing else. Only a renderer created with `layout: "stage"` needs it. |
356
357
 
357
358
  One thing they do not do for you. The module measures the room the first and last lines need to reach
358
359
  the view's target scroll position and writes it on the root as `--blyrics-padding-top` and
@@ -382,6 +383,26 @@ catch the layout up: the hung line floats into place while the lines around it s
382
383
  rather than the rest of the lyric jumping down. `--blyrics-animate-decoration-entry` set to `0` drops
383
384
  both for an instant insert, and reduced-motion does the same.
384
385
 
386
+ `createLyricsRenderer({ layout: "stage" })` builds a view for subtitles over a video: one line at a
387
+ time, and only while it is being sung. It fills its nearest positioned ancestor. There is no scroll
388
+ element to hand it, no scroll padding written and no line culling. Unsynced lyrics show nothing, because
389
+ a stage places lines by their time and unsynced lines have none.
390
+
391
+ The engine owns where a line is and whether you can see it. It moves each line with a `translate` Web
392
+ Animation and fades it through `--blyrics-stage-opacity`, blurring it slightly on the way in and out so
393
+ one line reads as turning into the next. Each line carries `data-stage-role` (`current`, `previous`,
394
+ `queued` or `gone`) and the container carries `data-layout="stage"`. `stage.css` sets a stage line's
395
+ `opacity` from that property with `!important` inside a cascade layer, and keeps the line
396
+ `visibility: hidden` until it is marked `data-stage-visible`. Layered important declarations outrank
397
+ unlayered ones whatever their specificity, so a theme that forces `opacity: 1 !important` on active
398
+ lines, which some do, can neither reveal a queued line nor hold one that is leaving. In a duet (any line sung by `v2` or `v3`) the
399
+ container also gets `data-stage-duet` and each singer keeps to their side, mirrored for right-to-left
400
+ lines; lines for everyone stay centred.
401
+
402
+ Everything else is the theme's. Lines are styled exactly as they would be in a scrolling view.
403
+ `host.onStageLayout(box)` reports the box around the sung lines in the container's coordinates, or null
404
+ when nothing sung is on stage, so you can draw a backdrop outside the container the theme styles.
405
+
385
406
  `@braccato/core/element` registers `<braccato-lyrics>`, and `<better-lyrics>` beside it, on import.
386
407
  Registration is a side effect, which is why it is entered separately.
387
408
 
@@ -429,7 +450,10 @@ swapping `audio.src` between songs without playing goes on reporting the old pos
429
450
 
430
451
  Two renderers in one document write over each other, so the module supports one. It is a constraint
431
452
  rather than a setting, and it is stated rather than enforced: none of the points where two of them
432
- collide is a crash.
453
+ collide is a crash. A stage renderer is the exception. It writes no scroll padding, so it can sit in
454
+ the same document as a scrolling one, with three rules: give both the same theme, tick both with the
455
+ same time and wall time in the same frame (whichever ticks first is the one that sees a seek), and if
456
+ the stage renderer applied the theme first, destroy it first, because its `<style>` element goes with it.
433
457
 
434
458
  Two things are written per document and belong to whichever renderer wrote them last: the theme's
435
459
  `<style>` element, and the scroll padding on the root. Two more are per bundle, because a settings
@@ -479,3 +503,42 @@ loading regional CJK web fonts should set `--noto-sans-universal` on these eleme
479
503
  `--blyrics-font-family` and `--blyrics-translated-font-family` remain explicit theme overrides.
480
504
  They are unset by default; custom CSS that reads them directly should use
481
505
  `var(--blyrics-font-family, var(--blyrics-default-font-family))` for the default stack.
506
+
507
+ ### Theme-provided image fills
508
+
509
+ Image highlights are optional. Enable them in a theme comment:
510
+
511
+ ```css
512
+ /* blyrics-image-highlights = true; */
513
+ .blyrics-container {
514
+ --blyrics-highlight-image: url("my-image.jpg");
515
+ --blyrics-image-glow-opacity-from: 0.5;
516
+ --blyrics-image-glow-opacity-to: 0;
517
+ }
518
+ ```
519
+
520
+ Changing this setting requests a lyric rebuild through the existing `setTheme` return value.
521
+ Without it, the renderer adds no image or glow layers and keeps its shadow animation. With it,
522
+ the sharp fill and a separate blurred copy share the word reveal, letter motion, opacity, pause,
523
+ seek and playback-rate handling. Existing glow radius, duration and easing properties still apply;
524
+ the two image-glow opacity properties replace the solid shadow color's alpha.
525
+
526
+ The image may be an SDR texture or an HDR gain-map asset. The theme owns HDR media queries
527
+ and `dynamic-range-limit`; the core never opts into extra display brightness. Ordinary active
528
+ text color backs up the image fill, while the note keeps its original color path until its
529
+ SVG image loads. The note image reuses the original animated wave clip and fade group.
530
+
531
+ The glow layer is outside the paint's mask, avoiding clipped halos at word edges. Bidi-sensitive
532
+ runs and fragmented long words use a parallel inline text run for the glow, so the browser
533
+ reproduces the same ordering and wrapped fragments. Its reveal and visibility use the same
534
+ animation clock as the sharp text. No generated text or per-frame geometry reads are needed.
535
+ Joining-script word groups retain contextual joining by using whole-word sweeps instead of
536
+ inline-block letter waves, including when image highlights are disabled.
537
+ Forced colors suppress the image and halo in favor of system text colors.
538
+
539
+ For real-browser regression checks, build the package, serve the repository root and open
540
+ `tooling/browser/image-highlights.html`. Import `image-highlights-checks.js` from that page and
541
+ call `runImageHighlightChecks(testView)`. The default fixture uses an SDR image; a `theme` query
542
+ parameter can point at a consumer's HDR stylesheet for physical-display testing.
543
+
544
+ Words that mix RTL characters with LTR letters or numbers retain native text shaping instead of per-letter motion. Image glow, letter masks and letter motion retain their current paint during the line exit fade.
package/dist/README.md CHANGED
@@ -345,14 +345,15 @@ refactor. Import them from `@braccato/core/constants` instead of typing them out
345
345
 
346
346
  ## Stylesheets
347
347
 
348
- Three sheets ship with the package, and loading them is yours, the way any package's CSS is. Leave
348
+ Four sheets ship with the package, and loading them is yours, the way any package's CSS is. Leave
349
349
  them out and you get lines that are in the document and unstyled, rather than lines that are missing.
350
350
 
351
351
  | File | What it carries |
352
352
  | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
353
- | `@braccato/core/styles/variables.css` | Every `--blyrics-*` default. It goes first, because the other two read from it. |
353
+ | `@braccato/core/styles/variables.css` | Every `--blyrics-*` default. It goes first, because the others read from it. |
354
354
  | `@braccato/core/styles/lyrics.css` | The container, the lines, the words and the sweep, plus two `@property` registrations the word animation interpolates through. |
355
355
  | `@braccato/core/styles/instrumental.css` | The waveform that fills a bar nobody sings over, and the animation that walks it. |
356
+ | `@braccato/core/styles/stage.css` | Placement for the stage layout, and nothing else. Only a renderer created with `layout: "stage"` needs it. |
356
357
 
357
358
  One thing they do not do for you. The module measures the room the first and last lines need to reach
358
359
  the view's target scroll position and writes it on the root as `--blyrics-padding-top` and
@@ -382,6 +383,26 @@ catch the layout up: the hung line floats into place while the lines around it s
382
383
  rather than the rest of the lyric jumping down. `--blyrics-animate-decoration-entry` set to `0` drops
383
384
  both for an instant insert, and reduced-motion does the same.
384
385
 
386
+ `createLyricsRenderer({ layout: "stage" })` builds a view for subtitles over a video: one line at a
387
+ time, and only while it is being sung. It fills its nearest positioned ancestor. There is no scroll
388
+ element to hand it, no scroll padding written and no line culling. Unsynced lyrics show nothing, because
389
+ a stage places lines by their time and unsynced lines have none.
390
+
391
+ The engine owns where a line is and whether you can see it. It moves each line with a `translate` Web
392
+ Animation and fades it through `--blyrics-stage-opacity`, blurring it slightly on the way in and out so
393
+ one line reads as turning into the next. Each line carries `data-stage-role` (`current`, `previous`,
394
+ `queued` or `gone`) and the container carries `data-layout="stage"`. `stage.css` sets a stage line's
395
+ `opacity` from that property with `!important` inside a cascade layer, and keeps the line
396
+ `visibility: hidden` until it is marked `data-stage-visible`. Layered important declarations outrank
397
+ unlayered ones whatever their specificity, so a theme that forces `opacity: 1 !important` on active
398
+ lines, which some do, can neither reveal a queued line nor hold one that is leaving. In a duet (any line sung by `v2` or `v3`) the
399
+ container also gets `data-stage-duet` and each singer keeps to their side, mirrored for right-to-left
400
+ lines; lines for everyone stay centred.
401
+
402
+ Everything else is the theme's. Lines are styled exactly as they would be in a scrolling view.
403
+ `host.onStageLayout(box)` reports the box around the sung lines in the container's coordinates, or null
404
+ when nothing sung is on stage, so you can draw a backdrop outside the container the theme styles.
405
+
385
406
  `@braccato/core/element` registers `<braccato-lyrics>`, and `<better-lyrics>` beside it, on import.
386
407
  Registration is a side effect, which is why it is entered separately.
387
408
 
@@ -429,7 +450,10 @@ swapping `audio.src` between songs without playing goes on reporting the old pos
429
450
 
430
451
  Two renderers in one document write over each other, so the module supports one. It is a constraint
431
452
  rather than a setting, and it is stated rather than enforced: none of the points where two of them
432
- collide is a crash.
453
+ collide is a crash. A stage renderer is the exception. It writes no scroll padding, so it can sit in
454
+ the same document as a scrolling one, with three rules: give both the same theme, tick both with the
455
+ same time and wall time in the same frame (whichever ticks first is the one that sees a seek), and if
456
+ the stage renderer applied the theme first, destroy it first, because its `<style>` element goes with it.
433
457
 
434
458
  Two things are written per document and belong to whichever renderer wrote them last: the theme's
435
459
  `<style>` element, and the scroll padding on the root. Two more are per bundle, because a settings
@@ -479,3 +503,42 @@ loading regional CJK web fonts should set `--noto-sans-universal` on these eleme
479
503
  `--blyrics-font-family` and `--blyrics-translated-font-family` remain explicit theme overrides.
480
504
  They are unset by default; custom CSS that reads them directly should use
481
505
  `var(--blyrics-font-family, var(--blyrics-default-font-family))` for the default stack.
506
+
507
+ ### Theme-provided image fills
508
+
509
+ Image highlights are optional. Enable them in a theme comment:
510
+
511
+ ```css
512
+ /* blyrics-image-highlights = true; */
513
+ .blyrics-container {
514
+ --blyrics-highlight-image: url("my-image.jpg");
515
+ --blyrics-image-glow-opacity-from: 0.5;
516
+ --blyrics-image-glow-opacity-to: 0;
517
+ }
518
+ ```
519
+
520
+ Changing this setting requests a lyric rebuild through the existing `setTheme` return value.
521
+ Without it, the renderer adds no image or glow layers and keeps its shadow animation. With it,
522
+ the sharp fill and a separate blurred copy share the word reveal, letter motion, opacity, pause,
523
+ seek and playback-rate handling. Existing glow radius, duration and easing properties still apply;
524
+ the two image-glow opacity properties replace the solid shadow color's alpha.
525
+
526
+ The image may be an SDR texture or an HDR gain-map asset. The theme owns HDR media queries
527
+ and `dynamic-range-limit`; the core never opts into extra display brightness. Ordinary active
528
+ text color backs up the image fill, while the note keeps its original color path until its
529
+ SVG image loads. The note image reuses the original animated wave clip and fade group.
530
+
531
+ The glow layer is outside the paint's mask, avoiding clipped halos at word edges. Bidi-sensitive
532
+ runs and fragmented long words use a parallel inline text run for the glow, so the browser
533
+ reproduces the same ordering and wrapped fragments. Its reveal and visibility use the same
534
+ animation clock as the sharp text. No generated text or per-frame geometry reads are needed.
535
+ Joining-script word groups retain contextual joining by using whole-word sweeps instead of
536
+ inline-block letter waves, including when image highlights are disabled.
537
+ Forced colors suppress the image and halo in favor of system text colors.
538
+
539
+ For real-browser regression checks, build the package, serve the repository root and open
540
+ `tooling/browser/image-highlights.html`. Import `image-highlights-checks.js` from that page and
541
+ call `runImageHighlightChecks(testView)`. The default fixture uses an SDR image; a `theme` query
542
+ parameter can point at a consumer's HDR stylesheet for physical-display testing.
543
+
544
+ Words that mix RTL characters with LTR letters or numbers retain native text shaping instead of per-letter motion. Image glow, letter masks and letter motion retain their current paint during the line exit fade.
package/dist/engine.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { LineData } from "./inject.js";
2
- import type { LyricsRendererHost, LyricSyncType, ResolvedTickOptions, TickOptions } from "./types.js";
2
+ import { type StageBox, type StageMetrics } from "./stage.js";
3
+ import type { LyricsLayout, LyricsRendererHost, LyricSyncType, ResolvedTickOptions, TickOptions } from "./types.js";
3
4
  /**
4
5
  * Everything that belongs to a single rendered lyrics view: one scroll container, one selection,
5
6
  * one set of pending scroll work.
@@ -69,6 +70,15 @@ export interface AnimationEngineInstance extends AnimEngineViewState {
69
70
  pendingLyricsUpdateFrame: number | null;
70
71
  learnedAnimationTimingOffsetMs: number;
71
72
  animationTimingVisibilityLogUntil: number;
73
+ layout: LyricsLayout;
74
+ stageKey: string;
75
+ stageBox: StageBox | null;
76
+ stageMetrics: Map<HTMLElement, StageMetrics>;
77
+ stageY: Map<HTMLElement, number>;
78
+ stageMoves: Map<HTMLElement, Animation>;
79
+ stageFades: Map<HTMLElement, Animation>;
80
+ stageBlurs: Map<HTMLElement, Animation>;
81
+ stageFontSize: number;
72
82
  /**
73
83
  * Releases everything the instance holds on its window: the reduced motion listener, the tab
74
84
  * renderer observer and any frame it still has queued.
@@ -80,7 +90,7 @@ export interface AnimationEngineInstance extends AnimEngineViewState {
80
90
  * deliberately not used inside the tick, which runs per frame and per line.
81
91
  */
82
92
  export declare function forEveryLiveView(runOperation: (engine: AnimationEngineInstance) => void): void;
83
- export declare function createAnimationEngineInstance(engineDocument: Document, engineWindow: EngineWindow, host: LyricsRendererHost): AnimationEngineInstance;
93
+ export declare function createAnimationEngineInstance(engineDocument: Document, engineWindow: EngineWindow, host: LyricsRendererHost, layout?: LyricsLayout): AnimationEngineInstance;
84
94
  /**
85
95
  * Forgets the last player snapshot, so the next tick is treated as the first one of a new song
86
96
  * rather than as a jump away from the previous one.
@@ -171,6 +181,10 @@ interface AnimationConfig {
171
181
  swipeEndFrom: string;
172
182
  swipeStartTo: string;
173
183
  swipeEndTo: string;
184
+ imageGlowFrom: string;
185
+ imageGlowTo: string;
186
+ imageGlowOpacityFrom: string;
187
+ imageGlowOpacityTo: string;
174
188
  glowFrom: string;
175
189
  glowTo: string;
176
190
  glowDurationRatio: number;
@@ -334,6 +348,7 @@ export type AnimationTickStatus = "ok" | "lyrics-missing";
334
348
  * matching no line at all.
335
349
  */
336
350
  export declare function resolveTickOptions(options: TickOptions): ResolvedTickOptions;
351
+ export declare function measureStage(engine: AnimationEngineInstance): void;
337
352
  /**
338
353
  * Renders one view against a tick with nothing left out.
339
354
  */