@braccato/core 1.2.1 → 1.4.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
@@ -235,7 +235,11 @@ package's stylesheets are yours to load for the same reason.
235
235
  `@braccato/core` is the facade and registers nothing. `createLyricsRenderer(options)` returns one
236
236
  `LyricsRenderer`: give it lyrics, tick it, and it owns the DOM it builds and every re-measurement
237
237
  that DOM needs. `resetPlaybackClock`, `resumeAllAutoscroll`, `injectRomanization` and
238
- `injectTranslation` are published beside it, for what one instance cannot answer for on its own.
238
+ `injectTranslation` are published beside it, for what one instance cannot answer for on its own. Hang
239
+ one onto a built line and call `renderer.scheduleLyricPositionUpdate` the way you already would to
240
+ catch the layout up: the hung line floats into place while the lines around it slide to make room,
241
+ rather than the rest of the lyric jumping down. `--blyrics-animate-decoration-entry` set to `0` drops
242
+ both for an instant insert, and reduced-motion does the same.
239
243
 
240
244
  `@braccato/core/element` registers `<braccato-lyrics>`, and `<better-lyrics>` beside it, on import.
241
245
  Registration is a side effect, which is why it is entered separately.
package/dist/README.md CHANGED
@@ -235,7 +235,11 @@ package's stylesheets are yours to load for the same reason.
235
235
  `@braccato/core` is the facade and registers nothing. `createLyricsRenderer(options)` returns one
236
236
  `LyricsRenderer`: give it lyrics, tick it, and it owns the DOM it builds and every re-measurement
237
237
  that DOM needs. `resetPlaybackClock`, `resumeAllAutoscroll`, `injectRomanization` and
238
- `injectTranslation` are published beside it, for what one instance cannot answer for on its own.
238
+ `injectTranslation` are published beside it, for what one instance cannot answer for on its own. Hang
239
+ one onto a built line and call `renderer.scheduleLyricPositionUpdate` the way you already would to
240
+ catch the layout up: the hung line floats into place while the lines around it slide to make room,
241
+ rather than the rest of the lyric jumping down. `--blyrics-animate-decoration-entry` set to `0` drops
242
+ both for an instant insert, and reduced-motion does the same.
239
243
 
240
244
  `@braccato/core/element` registers `<braccato-lyrics>`, and `<better-lyrics>` beside it, on import.
241
245
  Registration is a side effect, which is why it is entered separately.
package/dist/engine.js CHANGED
@@ -16,7 +16,7 @@
16
16
  // playback clock the last tick wrote. Their unit is a bundle rather than a document, and this
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
- import { ANIMATING_CLASS, CURRENT_LYRICS_CLASS, FOOTER_CLASS, LINE_CLASS, PAUSED_CLASS, USER_SCROLLING_CLASS, } from "./constants.js";
19
+ import { ANIMATING_CLASS, CURRENT_LYRICS_CLASS, FOOTER_CLASS, LINE_CLASS, PAUSED_CLASS, ROMANIZED_LYRICS_CLASS, TRANSLATED_LYRICS_CLASS, USER_SCROLLING_CLASS, } from "./constants.js";
20
20
  import { INSTRUMENTAL_WAVE_PATH_HIGH, INSTRUMENTAL_WAVE_PATH_LOW } from "./instrumental.js";
21
21
  import { registerThemeSetting } from "./themeSettings.js";
22
22
  import { clamp, getRelativeLayoutBounds, positiveModulo, roundedMs, toMs } from "./util.js";
@@ -304,7 +304,9 @@ function resetLineAnimationState(lineData) {
304
304
  }
305
305
  function togglePartClass(part, className, force) {
306
306
  part.lyricElement.classList.toggle(className, force);
307
- part.highlightElement?.classList.toggle(className, force);
307
+ if ("highlightElement" in part) {
308
+ part.highlightElement.classList.toggle(className, force);
309
+ }
308
310
  }
309
311
  function setAnimationsPlayState(lineData, isPlaying) {
310
312
  const children = [lineData, ...lineData.parts];
@@ -1928,10 +1930,61 @@ export function relayout(engine, measureLines) {
1928
1930
  const bounds = getRelativeLayoutBounds(lyricsElement, line.lyricElement);
1929
1931
  line.position = bounds.y;
1930
1932
  line.height = bounds.height;
1933
+ line.decorations = new Map(lineDecorators(line.lyricElement).map(element => [element, getRelativeLayoutBounds(lyricsElement, element).y]));
1931
1934
  }
1932
1935
  engine.wasUserScrolling = true; // trigger rescrolls
1933
1936
  engine.host.debug?.resize();
1934
1937
  }
1938
+ // -- Decoration slide --------------------------
1939
+ const DECORATION_SLIDE_MS = 350;
1940
+ const DECORATION_SLIDE_EASING = "cubic-bezier(0.25, 1, 0.5, 1)";
1941
+ const DECORATION_MIN_SHIFT_PX = 0.5;
1942
+ function slideElementBy(element, dy) {
1943
+ element.animate({ translate: [`0 ${dy}px`, "0 0"] }, { duration: DECORATION_SLIDE_MS, easing: DECORATION_SLIDE_EASING, composite: "add" });
1944
+ }
1945
+ function decorationSlideAllowed(engine, container) {
1946
+ if (engine.window.matchMedia(REDUCED_MOTION_QUERY).matches)
1947
+ return false;
1948
+ return (engine.window.getComputedStyle(container).getPropertyValue("--blyrics-animate-decoration-entry").trim() !== "0");
1949
+ }
1950
+ function lineDecorators(lineElement) {
1951
+ return [
1952
+ ...lineElement.querySelectorAll(`.${ROMANIZED_LYRICS_CLASS}`),
1953
+ ...lineElement.querySelectorAll(`.${TRANSLATED_LYRICS_CLASS}`),
1954
+ ];
1955
+ }
1956
+ /**
1957
+ * Snapshots where the lines and their decorators sit now, so the remeasure that follows can slide
1958
+ * each one from there to wherever a streamed decoration just pushed it. Returns null when nothing
1959
+ * should animate, and otherwise a function to call once the new layout is measured: the line rides
1960
+ * its own shift and a decorator rides only the part of its shift its line did not already carry, so
1961
+ * a survivor whose line held still still slides into the gap a removed sibling left.
1962
+ */
1963
+ function captureDecorationSlide(engine) {
1964
+ const container = engine.lyricsContainer;
1965
+ if (!container || !decorationSlideAllowed(engine, container))
1966
+ return null;
1967
+ const before = engine.lines.map(line => ({
1968
+ line,
1969
+ position: line.position,
1970
+ decorations: new Map(line.decorations),
1971
+ }));
1972
+ return () => {
1973
+ for (const { line, position, decorations } of before) {
1974
+ const lineShift = position - line.position;
1975
+ if (Math.abs(lineShift) >= DECORATION_MIN_SHIFT_PX)
1976
+ slideElementBy(line.lyricElement, lineShift);
1977
+ for (const [element, was] of decorations) {
1978
+ const now = line.decorations.get(element);
1979
+ if (now === undefined)
1980
+ continue;
1981
+ const shift = was - now - lineShift;
1982
+ if (Math.abs(shift) >= DECORATION_MIN_SHIFT_PX)
1983
+ slideElementBy(element, shift);
1984
+ }
1985
+ }
1986
+ };
1987
+ }
1935
1988
  // -- Debounced Lyrics Update --------------------------
1936
1989
  function cancelLyricPositionUpdate(engine) {
1937
1990
  if (engine.pendingLyricsUpdateFrame === null)
@@ -1969,7 +2022,9 @@ export function scheduleLyricPositionUpdate(engine, isViewRendering, retick) {
1969
2022
  engine.pendingLyricsUpdateFrame = engine.window.requestAnimationFrame(() => {
1970
2023
  engine.pendingLyricsUpdateFrame = null;
1971
2024
  const isRendering = isViewRendering();
2025
+ const playSlide = isRendering ? captureDecorationSlide(engine) : null;
1972
2026
  relayout(engine, isRendering);
2027
+ playSlide?.();
1973
2028
  if (!isRendering)
1974
2029
  return;
1975
2030
  retick();
package/dist/inject.d.ts CHANGED
@@ -3,7 +3,7 @@ export declare let disableRichsync: import("./themeSettings.js").Setting;
3
3
  export declare function findNearestAgent(lyrics: Lyric[], fromIndex: number): string | undefined;
4
4
  export declare function isNearestLyricRtl(lyrics: Lyric[], fromIndex: number): boolean;
5
5
  export declare function deriveSyncType(lyrics: Lyric[]): LyricSyncType;
6
- export interface PartData {
6
+ export interface AnimationData {
7
7
  /**
8
8
  * Time of this part in seconds
9
9
  */
@@ -14,7 +14,9 @@ export interface PartData {
14
14
  duration: number;
15
15
  lyricElement: HTMLElement;
16
16
  animations: Animation[];
17
- highlightElement?: HTMLElement;
17
+ }
18
+ export interface PartData extends AnimationData {
19
+ highlightElement: HTMLElement;
18
20
  }
19
21
  export type LineData = {
20
22
  parts: PartData[];
@@ -26,7 +28,8 @@ export type LineData = {
26
28
  isSelected: boolean;
27
29
  height: number;
28
30
  position: number;
29
- } & PartData;
31
+ decorations: Map<HTMLElement, number>;
32
+ } & AnimationData;
30
33
  export declare function newLineData(lyricElement: HTMLElement, startTimeMs: number, durationMs: number): LineData;
31
34
  export declare function applyDirection(element: HTMLElement, text: string): void;
32
35
  export declare function createLyricsLine(doc: Document, parts: LyricPart[], line: LineData, lyricElement: HTMLElement, options?: {
package/dist/inject.js CHANGED
@@ -83,6 +83,7 @@ export function newLineData(lyricElement, startTimeMs, durationMs) {
83
83
  isSelected: false,
84
84
  height: -1,
85
85
  position: -1,
86
+ decorations: new Map(),
86
87
  animations: [],
87
88
  };
88
89
  }
@@ -184,6 +184,8 @@
184
184
 
185
185
  .blyrics--translated,
186
186
  .blyrics--romanized {
187
+ animation: blyrics-decoration-in calc(var(--blyrics-animate-decoration-entry) * 0.35s)
188
+ cubic-bezier(0.25, 1, 0.5, 1) both;
187
189
  color: var(--blyrics-translated-color);
188
190
  display: block;
189
191
  font-family: var(--blyrics-translated-font-family);
@@ -197,6 +199,13 @@
197
199
  white-space: normal;
198
200
  }
199
201
 
202
+ @keyframes blyrics-decoration-in {
203
+ from {
204
+ opacity: 0;
205
+ transform: translateY(-0.4em);
206
+ }
207
+ }
208
+
200
209
  .blyrics--romanized {
201
210
  background: rgba(255, 255, 255, 0.05);
202
211
  border: 1px solid rgba(255, 255, 255, 0.1);
@@ -63,6 +63,7 @@
63
63
  --blyrics-animate-highlight-fade: 1;
64
64
  --blyrics-animate-scroll: 1;
65
65
  --blyrics-animate-instrumental: 1;
66
+ --blyrics-animate-decoration-entry: 1;
66
67
 
67
68
  --blyrics-loader-transition-duration: 0.6s;
68
69
  --blyrics-loader-transition-easing: cubic-bezier(0.22, 1, 0.36, 1);
@@ -176,6 +177,7 @@
176
177
  --blyrics-animate-line-scale: 0;
177
178
  --blyrics-animate-word-wobble: 0;
178
179
  --blyrics-animate-instrumental: 0;
180
+ --blyrics-animate-decoration-entry: 0;
179
181
 
180
182
  --blyrics-scale: 1;
181
183
  --blyrics-active-scale: 1;
package/dist/types.d.ts CHANGED
@@ -163,9 +163,14 @@ export interface LyricsRenderer {
163
163
  */
164
164
  clearOnScreenLyrics(): boolean;
165
165
  /**
166
- * Re-measures the lines on the next frame and renders the view again against them. The predicate
167
- * is the caller's half of whether that frame does anything, and only that half: whether the lines
168
- * are on the screen to be measured at all is the renderer's own question, and it asks it itself.
166
+ * Re-measures the lines on the next frame and renders the view again against them. This is the door
167
+ * a consumer knocks on after streaming in a translation or romanization, and it is what slides the
168
+ * lines the new decoration moved from where they were to where it put them, together with any
169
+ * surviving decorator that shifted within its own line, so the rest of the lyric eases down rather
170
+ * than jumping. The slide honours reduced motion and `--blyrics-animate-decoration-entry`; with
171
+ * either off the lines are re-measured with no slide. The predicate is the caller's half of whether
172
+ * that frame does anything, and only that half: whether the lines are on the screen to be measured
173
+ * at all is the renderer's own question, and it asks it itself.
169
174
  *
170
175
  * @param isTicking - Whether whoever drives this view is still driving it, asked on the frame
171
176
  * rather than now. A driver that has stopped is one whose lines may no longer be rendered, and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@braccato/core",
3
- "version": "1.2.1",
3
+ "version": "1.4.0",
4
4
  "description": "Synchronized lyrics renderer with word-by-word animations",
5
5
  "type": "module",
6
6
  "license": "MIT",