@braccato/core 1.1.0 → 1.2.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
@@ -179,6 +179,13 @@ The ones a theme reaches for first. `variables.css` declares the rest.
179
179
  `line-height`. Every word is given the glow, so a theme that wants it to mean something selects on
180
180
  `data-long-word`, which the module sets on any part held past `blyrics-long-word-threshold`.
181
181
 
182
+ The instrumental ripple is the one place a property carries geometry rather than a value.
183
+ `--blyrics-instrumental-wave-path-high` and `--blyrics-instrumental-wave-path-low` are the two shapes
184
+ it morphs between, each a `path()`, and a theme redrawing them is how the wave changes amplitude or
185
+ frequency. Both must use the same commands in the same order with the same number of arguments: a
186
+ browser only interpolates two paths smoothly when their command sequences match, and a mismatched
187
+ pair snaps at the halfway point instead of flowing.
188
+
182
189
  ### Class names
183
190
 
184
191
  These are published API rather than implementation. Renaming one costs a migration rather than a
package/dist/README.md CHANGED
@@ -179,6 +179,13 @@ The ones a theme reaches for first. `variables.css` declares the rest.
179
179
  `line-height`. Every word is given the glow, so a theme that wants it to mean something selects on
180
180
  `data-long-word`, which the module sets on any part held past `blyrics-long-word-threshold`.
181
181
 
182
+ The instrumental ripple is the one place a property carries geometry rather than a value.
183
+ `--blyrics-instrumental-wave-path-high` and `--blyrics-instrumental-wave-path-low` are the two shapes
184
+ it morphs between, each a `path()`, and a theme redrawing them is how the wave changes amplitude or
185
+ frequency. Both must use the same commands in the same order with the same number of arguments: a
186
+ browser only interpolates two paths smoothly when their command sequences match, and a mismatched
187
+ pair snaps at the halfway point instead of flowing.
188
+
182
189
  ### Class names
183
190
 
184
191
  These are published API rather than implementation. Renaming one costs a migration rather than a
package/dist/engine.d.ts CHANGED
@@ -184,6 +184,8 @@ interface AnimationConfig {
184
184
  waveFrom: string;
185
185
  waveTo: string;
186
186
  waveEasing: string;
187
+ wavePathHigh: string;
188
+ wavePathLow: string;
187
189
  waveOscillationDurationMs: number;
188
190
  waveOscillationEasing: string;
189
191
  };
package/dist/engine.js CHANGED
@@ -17,6 +17,7 @@
17
17
  // module is bundled into the isolated world and the page world separately, so those are two clocks
18
18
  // that never meet. `themeSettings.ts` holds the third thing under that rule.
19
19
  import { ANIMATING_CLASS, CURRENT_LYRICS_CLASS, FOOTER_CLASS, LINE_CLASS, PAUSED_CLASS, USER_SCROLLING_CLASS, } from "./constants.js";
20
+ import { INSTRUMENTAL_WAVE_PATH_HIGH, INSTRUMENTAL_WAVE_PATH_LOW } from "./instrumental.js";
20
21
  import { registerThemeSetting } from "./themeSettings.js";
21
22
  import { clamp, getRelativeLayoutBounds, positiveModulo, roundedMs, toMs } from "./util.js";
22
23
  const NO_LYRICS_ELEMENT_LOG = "No lyrics element found on the page, skipping lyrics injection";
@@ -326,8 +327,8 @@ const WORD_HIGHLIGHT_SELECTOR = ".blyrics-word-highlight";
326
327
  const INSTRUMENTAL_FILL_SELECTOR = ".blyrics--instrumental-fill";
327
328
  const INSTRUMENTAL_WAVE_CLIP_SELECTOR = ".blyrics--wave-clip";
328
329
  const INSTRUMENTAL_WAVE_PATH_SELECTOR = ".blyrics--wave-path";
329
- const INSTRUMENTAL_WAVE_PATH_HIGH = 'path("M -4 3 Q 1 2 5 3 Q 10 4 14 3 Q 18 2 22 3 Q 26 4 30 3 L 30 4 L -4 4 Z")';
330
- const INSTRUMENTAL_WAVE_PATH_LOW = 'path("M -4 3 Q 1 4 5 3 Q 10 2 14 3 Q 18 4 22 3 Q 26 2 30 3 L 30 4 L -4 4 Z")';
330
+ const INSTRUMENTAL_WAVE_PATH_HIGH_FALLBACK = `path("${INSTRUMENTAL_WAVE_PATH_HIGH}")`;
331
+ const INSTRUMENTAL_WAVE_PATH_LOW_FALLBACK = `path("${INSTRUMENTAL_WAVE_PATH_LOW}")`;
331
332
  const LINE_SCROLL_INDEX_PROPERTY = "--blyrics-line-scroll-index";
332
333
  const LINE_SCROLL_ACTIVE_INDEX_PROPERTY = "--blyrics-line-scroll-active-index";
333
334
  const LINE_SCROLL_RELATIVE_INDEX_PROPERTY = "--blyrics-line-scroll-relative-index";
@@ -856,9 +857,9 @@ function startInstrumentalAnimations(engine, lineData, config, currentTime, appl
856
857
  fill: "both",
857
858
  }, { appliedTimingOffsetMs, offsetMs: 0 });
858
859
  waveOscillationAnimation = animateInstrumentalChild(engine, lineData, INSTRUMENTAL_WAVE_PATH_SELECTOR, [
859
- { d: INSTRUMENTAL_WAVE_PATH_HIGH },
860
- { d: INSTRUMENTAL_WAVE_PATH_LOW, offset: 0.5 },
861
- { d: INSTRUMENTAL_WAVE_PATH_HIGH },
860
+ { d: config.instrumental.wavePathHigh },
861
+ { d: config.instrumental.wavePathLow, offset: 0.5 },
862
+ { d: config.instrumental.wavePathHigh },
862
863
  ], {
863
864
  duration: config.instrumental.waveOscillationDurationMs,
864
865
  easing: config.instrumental.waveOscillationEasing,
@@ -1016,6 +1017,8 @@ function readAnimationConfig(engine, lyricsElement) {
1016
1017
  waveFrom: getCSSValue(engine, lyricsElement, "--blyrics-instrumental-wave-transform-from", "scaleY(1.2)"),
1017
1018
  waveTo: getCSSValue(engine, lyricsElement, "--blyrics-instrumental-wave-transform-to", "scaleY(0.0001)"),
1018
1019
  waveEasing: getCSSValue(engine, lyricsElement, "--blyrics-instrumental-wave-easing", "ease-in"),
1020
+ wavePathHigh: getCSSValue(engine, lyricsElement, "--blyrics-instrumental-wave-path-high", INSTRUMENTAL_WAVE_PATH_HIGH_FALLBACK),
1021
+ wavePathLow: getCSSValue(engine, lyricsElement, "--blyrics-instrumental-wave-path-low", INSTRUMENTAL_WAVE_PATH_LOW_FALLBACK),
1019
1022
  waveOscillationDurationMs: getCSSDurationWithFallback(engine, lyricsElement, "--blyrics-instrumental-wave-oscillation-duration", "1.25s"),
1020
1023
  waveOscillationEasing: getCSSValue(engine, lyricsElement, "--blyrics-instrumental-wave-oscillation-easing", "ease-in-out"),
1021
1024
  },
@@ -1560,6 +1563,7 @@ export function tickView(engine, currentTime, options) {
1560
1563
  }
1561
1564
  const tabRendererHeight = engine.cachedTabRendererHeight ?? tabRenderer.getBoundingClientRect().height;
1562
1565
  let scrollTop = tabRenderer.scrollTop;
1566
+ const maxScrollTop = Math.max(0, tabRenderer.scrollHeight - tabRenderer.clientHeight);
1563
1567
  if (animationConfig.enabled.scroll) {
1564
1568
  updateVisibleLyricWillChange(engine, lines, scrollTop, engine.pendingLineScroll?.toScrollTop ?? scrollTop, tabRendererHeight);
1565
1569
  }
@@ -1713,8 +1717,9 @@ export function tickView(engine, currentTime, options) {
1713
1717
  scrollPos = Math.max(scrollPos, lastActiveLyric.position - tabRendererHeight + lastActiveLyric.height);
1714
1718
  // Make sure top of last active lyric is visible.
1715
1719
  scrollPos = Math.min(scrollPos, lastActiveLyric.position);
1716
- // Make sure we're not trying to scroll to negative values
1717
- scrollPos = Math.max(0, scrollPos);
1720
+ // Past either end the browser clamps the write and reports nothing, leaving the view aiming
1721
+ // at a position it never reached and re-aiming once per remaining line.
1722
+ scrollPos = clamp(scrollPos, 0, maxScrollTop);
1718
1723
  if (ENABLE_DEBUG_RENDER.getBooleanValue()) {
1719
1724
  let transform = engine.window.getComputedStyle(lyricsElement).transform;
1720
1725
  const matrix = new engine.window.DOMMatrix(transform);
@@ -1899,6 +1904,9 @@ function applyScrollPadding(engine) {
1899
1904
  });
1900
1905
  engine.document.documentElement.style.setProperty("--blyrics-padding-top", top + "px");
1901
1906
  engine.document.documentElement.style.setProperty("--blyrics-padding-bottom", bottom + "px");
1907
+ // Inline, because a theme's own `.blyrics-container { padding }` is appended after the package's
1908
+ // stylesheet at the same weight and would otherwise take this room away.
1909
+ lyricsElement.style.setProperty("padding-bottom", bottom + "px");
1902
1910
  }
1903
1911
  /**
1904
1912
  * Re-reads the view's layout: the scroll padding first, then the line positions the padding moved.
@@ -1,3 +1,5 @@
1
+ export declare const INSTRUMENTAL_WAVE_PATH_HIGH = "M -4 3 Q 1 2 5 3 Q 10 4 14 3 Q 18 2 22 3 Q 26 4 30 3 L 30 4 L -4 4 Z";
2
+ export declare const INSTRUMENTAL_WAVE_PATH_LOW = "M -4 3 Q 1 4 5 3 Q 10 2 14 3 Q 18 4 22 3 Q 26 2 30 3 L 30 4 L -4 4 Z";
1
3
  /**
2
4
  * Creates an HTML element representing an instrumental break in the lyrics.
3
5
  *
@@ -1,3 +1,5 @@
1
+ export const INSTRUMENTAL_WAVE_PATH_HIGH = "M -4 3 Q 1 2 5 3 Q 10 4 14 3 Q 18 2 22 3 Q 26 4 30 3 L 30 4 L -4 4 Z";
2
+ export const INSTRUMENTAL_WAVE_PATH_LOW = "M -4 3 Q 1 4 5 3 Q 10 2 14 3 Q 18 4 22 3 Q 26 2 30 3 L 30 4 L -4 4 Z";
1
3
  /**
2
4
  * Creates an HTML element representing an instrumental break in the lyrics.
3
5
  *
@@ -56,8 +58,7 @@ export function createInstrumentalElement(doc, container, durationMs, lineIndex)
56
58
  // This only contains the surface water. It closes at y=4.
57
59
  const wavePath = doc.createElementNS(svgNS, "path");
58
60
  wavePath.classList.add("blyrics--wave-path");
59
- // Initial draw (matches the 0% keyframe below)
60
- wavePath.setAttribute("d", "M -4 3 Q 1 2 5 3 Q 10 4 14 3 Q 18 2 22 3 Q 26 4 30 3 L 30 4 L -4 4 Z");
61
+ wavePath.setAttribute("d", INSTRUMENTAL_WAVE_PATH_HIGH);
61
62
  clipPath.appendChild(wavePath);
62
63
  defs.appendChild(clipPath);
63
64
  svg.appendChild(defs);
package/dist/renderer.js CHANGED
@@ -269,6 +269,10 @@ export function createLyricsRenderer(rendererOptions) {
269
269
  // Everything the engine resolved off the document was resolved against the theme that just
270
270
  // went away.
271
271
  clearEngineStyleCaches(engine);
272
+ // The target scroll position is one of the settings a theme carries, and the scroll padding is
273
+ // sized against it, so a theme that moves the target moves the layout even when no rule in it
274
+ // does.
275
+ measure();
272
276
  return needsLyricRebuild;
273
277
  },
274
278
  tick(currentTimeS, options) {
@@ -69,6 +69,8 @@
69
69
 
70
70
  /* Prepare the WAAPI flatten transition. */
71
71
  transform: scaleY(1.2);
72
+
73
+ d: var(--blyrics-instrumental-wave-path-high);
72
74
  }
73
75
 
74
76
  /* Trigger the Flattening */
@@ -90,14 +92,3 @@
90
92
  transition-timing-function: ease-out;
91
93
  transition-delay: 0s;
92
94
  }
93
-
94
- /* Update Keyframes for the "Split" shape */
95
- /* These paths must close at 'L 30 4 L -4 4' to match the static rect overlap */
96
- @keyframes blyrics-wave {
97
- 0%, 100% {
98
- d: path("M -4 3 Q 1 2 5 3 Q 10 4 14 3 Q 18 2 22 3 Q 26 4 30 3 L 30 4 L -4 4 Z");
99
- }
100
- 50% {
101
- d: path("M -4 3 Q 1 4 5 3 Q 10 2 14 3 Q 18 4 22 3 Q 26 2 30 3 L 30 4 L -4 4 Z");
102
- }
103
- }
@@ -117,6 +117,8 @@
117
117
  --blyrics-instrumental-wave-transform-from: scaleY(1.2);
118
118
  --blyrics-instrumental-wave-transform-to: scaleY(0.0001);
119
119
  --blyrics-instrumental-wave-easing: ease-in;
120
+ --blyrics-instrumental-wave-path-high: path("M -4 3 Q 1 2 5 3 Q 10 4 14 3 Q 18 2 22 3 Q 26 4 30 3 L 30 4 L -4 4 Z");
121
+ --blyrics-instrumental-wave-path-low: path("M -4 3 Q 1 4 5 3 Q 10 2 14 3 Q 18 4 22 3 Q 26 2 30 3 L 30 4 L -4 4 Z");
120
122
  --blyrics-instrumental-wave-oscillation-duration: 1.25s;
121
123
  --blyrics-instrumental-wave-oscillation-easing: ease-in-out;
122
124
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@braccato/core",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Synchronized lyrics renderer with word-by-word animations",
5
5
  "type": "module",
6
6
  "license": "MIT",