@braccato/core 1.10.0 → 1.12.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 +33 -1
- package/dist/README.md +33 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/engine.d.ts +2 -0
- package/dist/engine.js +42 -14
- package/dist/inject.d.ts +1 -0
- package/dist/inject.js +1 -0
- package/dist/renderer.js +1 -0
- package/dist/styles/lyrics.css +20 -5
- package/dist/styles/variables.css +5 -0
- package/dist/view.d.ts +2 -0
- package/dist/view.js +19 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,9 @@ than a shadow root, so the CSS already on your page reaches them.
|
|
|
7
7
|
Extracted from the [Better Lyrics](https://better-lyrics.boidu.dev) rendering engine, which is still
|
|
8
8
|
where it runs.
|
|
9
9
|
|
|
10
|
+
The [renderer guide](https://docs.betterlyrics.org/braccato/renderer) on the docs site covers properties, events,
|
|
11
|
+
scrolling and theming with lyrics from the Better Lyrics API. This README is still the full reference.
|
|
12
|
+
|
|
10
13
|
## Install
|
|
11
14
|
|
|
12
15
|
```bash
|
|
@@ -68,6 +71,15 @@ A `Lyric` is `{ startTimeMs, durationMs, words }`, with an optional `parts` arra
|
|
|
68
71
|
fields for syllable or word timing, and optional `translation`, `romanization` and
|
|
69
72
|
`timedRomanization` beside them.
|
|
70
73
|
|
|
74
|
+
Who wrote the song travels beside the lines rather than in them. Hand the names to `lyricsOptions` and
|
|
75
|
+
the view closes with a "Written by" line after the last lyric. The parsers read them out of the file:
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
const parser = detectParser(text);
|
|
79
|
+
view.lyricsOptions = { songwriters: parser.metadata(text).songwriters };
|
|
80
|
+
view.lyrics = parser.parse(text, player.duration * 1000);
|
|
81
|
+
```
|
|
82
|
+
|
|
71
83
|
## Properties
|
|
72
84
|
|
|
73
85
|
Every one of these may be written before the element is in a document. The renderer is built when it
|
|
@@ -76,7 +88,7 @@ connects, and everything it was handed by then is applied at once.
|
|
|
76
88
|
| Property | Attribute | Type | Default | Description |
|
|
77
89
|
| --------------- | -------------- | ----------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
78
90
|
| `lyrics` | | `Lyric[] \| null` | `null` | The song. Null means it was never given one, and an empty array clears the view, so there is a way to say both. |
|
|
79
|
-
| `lyricsOptions` | | `{ loaderVisible?, noLyrics?, language? }`
|
|
91
|
+
| `lyricsOptions` | | `{ loaderVisible?, noLyrics?, language?, songwriters? }` | `{}` | How the lines are built. `noLyrics` marks a message as a placeholder rather than a song, which keeps passive scrolling from drifting it. `songwriters` closes the view with a credits line. |
|
|
80
92
|
| `source` | `source` | `string \| HTMLMediaElement \| null` | `null` | A selector or the media element itself. See Following a media element. |
|
|
81
93
|
| `mediaElement` | | `HTMLMediaElement \| null` (get) | `null` | What `source` resolved to. Null while disconnected, and null for a selector that missed. |
|
|
82
94
|
| `currentTime` | `current-time` | `number` | `0` | Playback position in **seconds**. Writing it renders the view again, so whoever holds the clock drives the lyrics by writing this. |
|
|
@@ -226,6 +238,24 @@ frequency. Both must use the same commands in the same order with the same numbe
|
|
|
226
238
|
browser only interpolates two paths smoothly when their command sequences match, and a mismatched
|
|
227
239
|
pair snaps at the halfway point instead of flowing.
|
|
228
240
|
|
|
241
|
+
### Credits
|
|
242
|
+
|
|
243
|
+
Given `songwriters`, the view ends with `A, B & C` after the last line. It sits dim through the song,
|
|
244
|
+
and once the last line has ended it brightens and takes the scroll focus, the way Apple Music closes a
|
|
245
|
+
song. Seeking back hands the focus to the lines again. Unsynced lyrics have no end, so their credits
|
|
246
|
+
show at full strength from the start.
|
|
247
|
+
|
|
248
|
+
| Custom property | Default | What it sets |
|
|
249
|
+
| ------------------------------------ | ---------------- | ---------------------------------------------------- |
|
|
250
|
+
| `--blyrics-credits-label` | `"Written by"` | The words before the names. Localise it here. |
|
|
251
|
+
| `--blyrics-credits-font-size` | `max(0.4em, 12px)` | The size of the whole line. |
|
|
252
|
+
| `--blyrics-credits-opacity` | `0.2` | While the song plays. |
|
|
253
|
+
| `--blyrics-credits-focused-opacity` | `0.85` | Once the song has ended. |
|
|
254
|
+
|
|
255
|
+
The container carries `data-credits-focused` while the credits hold the focus. To hide them, a
|
|
256
|
+
stylesheet can set `.blyrics-credits { display: none; }`, which the scroll then ignores, or a theme can
|
|
257
|
+
declare `/* blyrics-hide-credits = true; */` so they are never built.
|
|
258
|
+
|
|
229
259
|
### Letter wave (experimental)
|
|
230
260
|
|
|
231
261
|
On by default; a theme opts out with `/* blyrics-letter-wave = false; */`. It splits every word into
|
|
@@ -292,6 +322,8 @@ refactor. Import them from `@braccato/core/constants` instead of typing them out
|
|
|
292
322
|
| `BACKGROUND_LYRIC_CLASS` | `blyrics-background-lyric` | A background vocal, sung over the line it answers. |
|
|
293
323
|
| `USER_SCROLLING_CLASS` | `blyrics-user-scrolling` | Set while a reader has scrolled away and autoscroll waits. |
|
|
294
324
|
| `TRANSLATED_LYRICS_CLASS` | `blyrics--translated` | A translation hung off a line that was already built. |
|
|
325
|
+
| `EXPLICIT_WORD_CLASS` | `blyrics-explicit` | A word the lyrics flag as explicit. Unstyled unless a theme styles it. |
|
|
326
|
+
| `CREDITS_CLASS` | `blyrics-credits` | The songwriter credits after the last line. |
|
|
295
327
|
| `CUSTOM_THEME_STYLE_ID` | `blyrics-custom-style` | The id of the `<style>` the theme lands in. |
|
|
296
328
|
|
|
297
329
|
## Stylesheets
|
package/dist/README.md
CHANGED
|
@@ -7,6 +7,9 @@ than a shadow root, so the CSS already on your page reaches them.
|
|
|
7
7
|
Extracted from the [Better Lyrics](https://better-lyrics.boidu.dev) rendering engine, which is still
|
|
8
8
|
where it runs.
|
|
9
9
|
|
|
10
|
+
The [renderer guide](https://docs.betterlyrics.org/braccato/renderer) on the docs site covers properties, events,
|
|
11
|
+
scrolling and theming with lyrics from the Better Lyrics API. This README is still the full reference.
|
|
12
|
+
|
|
10
13
|
## Install
|
|
11
14
|
|
|
12
15
|
```bash
|
|
@@ -68,6 +71,15 @@ A `Lyric` is `{ startTimeMs, durationMs, words }`, with an optional `parts` arra
|
|
|
68
71
|
fields for syllable or word timing, and optional `translation`, `romanization` and
|
|
69
72
|
`timedRomanization` beside them.
|
|
70
73
|
|
|
74
|
+
Who wrote the song travels beside the lines rather than in them. Hand the names to `lyricsOptions` and
|
|
75
|
+
the view closes with a "Written by" line after the last lyric. The parsers read them out of the file:
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
const parser = detectParser(text);
|
|
79
|
+
view.lyricsOptions = { songwriters: parser.metadata(text).songwriters };
|
|
80
|
+
view.lyrics = parser.parse(text, player.duration * 1000);
|
|
81
|
+
```
|
|
82
|
+
|
|
71
83
|
## Properties
|
|
72
84
|
|
|
73
85
|
Every one of these may be written before the element is in a document. The renderer is built when it
|
|
@@ -76,7 +88,7 @@ connects, and everything it was handed by then is applied at once.
|
|
|
76
88
|
| Property | Attribute | Type | Default | Description |
|
|
77
89
|
| --------------- | -------------- | ----------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
78
90
|
| `lyrics` | | `Lyric[] \| null` | `null` | The song. Null means it was never given one, and an empty array clears the view, so there is a way to say both. |
|
|
79
|
-
| `lyricsOptions` | | `{ loaderVisible?, noLyrics?, language? }`
|
|
91
|
+
| `lyricsOptions` | | `{ loaderVisible?, noLyrics?, language?, songwriters? }` | `{}` | How the lines are built. `noLyrics` marks a message as a placeholder rather than a song, which keeps passive scrolling from drifting it. `songwriters` closes the view with a credits line. |
|
|
80
92
|
| `source` | `source` | `string \| HTMLMediaElement \| null` | `null` | A selector or the media element itself. See Following a media element. |
|
|
81
93
|
| `mediaElement` | | `HTMLMediaElement \| null` (get) | `null` | What `source` resolved to. Null while disconnected, and null for a selector that missed. |
|
|
82
94
|
| `currentTime` | `current-time` | `number` | `0` | Playback position in **seconds**. Writing it renders the view again, so whoever holds the clock drives the lyrics by writing this. |
|
|
@@ -226,6 +238,24 @@ frequency. Both must use the same commands in the same order with the same numbe
|
|
|
226
238
|
browser only interpolates two paths smoothly when their command sequences match, and a mismatched
|
|
227
239
|
pair snaps at the halfway point instead of flowing.
|
|
228
240
|
|
|
241
|
+
### Credits
|
|
242
|
+
|
|
243
|
+
Given `songwriters`, the view ends with `A, B & C` after the last line. It sits dim through the song,
|
|
244
|
+
and once the last line has ended it brightens and takes the scroll focus, the way Apple Music closes a
|
|
245
|
+
song. Seeking back hands the focus to the lines again. Unsynced lyrics have no end, so their credits
|
|
246
|
+
show at full strength from the start.
|
|
247
|
+
|
|
248
|
+
| Custom property | Default | What it sets |
|
|
249
|
+
| ------------------------------------ | ---------------- | ---------------------------------------------------- |
|
|
250
|
+
| `--blyrics-credits-label` | `"Written by"` | The words before the names. Localise it here. |
|
|
251
|
+
| `--blyrics-credits-font-size` | `max(0.4em, 12px)` | The size of the whole line. |
|
|
252
|
+
| `--blyrics-credits-opacity` | `0.2` | While the song plays. |
|
|
253
|
+
| `--blyrics-credits-focused-opacity` | `0.85` | Once the song has ended. |
|
|
254
|
+
|
|
255
|
+
The container carries `data-credits-focused` while the credits hold the focus. To hide them, a
|
|
256
|
+
stylesheet can set `.blyrics-credits { display: none; }`, which the scroll then ignores, or a theme can
|
|
257
|
+
declare `/* blyrics-hide-credits = true; */` so they are never built.
|
|
258
|
+
|
|
229
259
|
### Letter wave (experimental)
|
|
230
260
|
|
|
231
261
|
On by default; a theme opts out with `/* blyrics-letter-wave = false; */`. It splits every word into
|
|
@@ -292,6 +322,8 @@ refactor. Import them from `@braccato/core/constants` instead of typing them out
|
|
|
292
322
|
| `BACKGROUND_LYRIC_CLASS` | `blyrics-background-lyric` | A background vocal, sung over the line it answers. |
|
|
293
323
|
| `USER_SCROLLING_CLASS` | `blyrics-user-scrolling` | Set while a reader has scrolled away and autoscroll waits. |
|
|
294
324
|
| `TRANSLATED_LYRICS_CLASS` | `blyrics--translated` | A translation hung off a line that was already built. |
|
|
325
|
+
| `EXPLICIT_WORD_CLASS` | `blyrics-explicit` | A word the lyrics flag as explicit. Unstyled unless a theme styles it. |
|
|
326
|
+
| `CREDITS_CLASS` | `blyrics-credits` | The songwriter credits after the last line. |
|
|
295
327
|
| `CUSTOM_THEME_STYLE_ID` | `blyrics-custom-style` | The id of the `<style>` the theme lands in. |
|
|
296
328
|
|
|
297
329
|
## Stylesheets
|
package/dist/constants.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export declare const LINE_CLASS: "blyrics--line";
|
|
|
4
4
|
export declare const WORD_CLASS: "blyrics--word";
|
|
5
5
|
export declare const LETTER_CLASS: "blyrics--letter";
|
|
6
6
|
export declare const FOOTER_CLASS: "blyrics-footer";
|
|
7
|
+
export declare const CREDITS_CLASS: "blyrics-credits";
|
|
8
|
+
export declare const CREDITS_NAMES_CLASS: "blyrics-credits__names";
|
|
7
9
|
export declare const CURRENT_LYRICS_CLASS: "blyrics--active";
|
|
8
10
|
export declare const ANIMATING_CLASS: "blyrics--animating";
|
|
9
11
|
export declare const PAUSED_CLASS: "blyrics--paused";
|
package/dist/constants.js
CHANGED
|
@@ -7,6 +7,8 @@ export const LINE_CLASS = "blyrics--line";
|
|
|
7
7
|
export const WORD_CLASS = "blyrics--word";
|
|
8
8
|
export const LETTER_CLASS = "blyrics--letter";
|
|
9
9
|
export const FOOTER_CLASS = "blyrics-footer";
|
|
10
|
+
export const CREDITS_CLASS = "blyrics-credits";
|
|
11
|
+
export const CREDITS_NAMES_CLASS = "blyrics-credits__names";
|
|
10
12
|
// -- Playback state --------------------------------------------
|
|
11
13
|
export const CURRENT_LYRICS_CLASS = "blyrics--active";
|
|
12
14
|
export const ANIMATING_CLASS = "blyrics--animating";
|
package/dist/engine.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export interface AnimationEngineInstance extends AnimEngineViewState {
|
|
|
40
40
|
host: LyricsRendererHost;
|
|
41
41
|
cachedTabRendererHeight: number | null;
|
|
42
42
|
cachedFooterItem: LineScrollItem | null;
|
|
43
|
+
cachedCreditsItem: LineScrollItem | null;
|
|
44
|
+
creditsFocused: boolean;
|
|
43
45
|
cachedLineScrollTiming: Map<string, LineScrollTiming>;
|
|
44
46
|
tabRendererResizeObserver: ResizeObserver | null;
|
|
45
47
|
observedTabRenderer: HTMLElement | null;
|
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, ROMANIZED_LYRICS_CLASS, RTL_CLASS, TRANSLATED_LYRICS_CLASS, USER_SCROLLING_CLASS, WORD_STATE_ACTIVE, WORD_STATE_ATTR, WORD_STATE_PAST, WORD_STATE_UPCOMING, } from "./constants.js";
|
|
19
|
+
import { ANIMATING_CLASS, CREDITS_CLASS, CURRENT_LYRICS_CLASS, FOOTER_CLASS, LINE_CLASS, PAUSED_CLASS, ROMANIZED_LYRICS_CLASS, RTL_CLASS, TRANSLATED_LYRICS_CLASS, USER_SCROLLING_CLASS, WORD_STATE_ACTIVE, WORD_STATE_ATTR, WORD_STATE_PAST, WORD_STATE_UPCOMING, } 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";
|
|
@@ -129,6 +129,8 @@ export function createAnimationEngineInstance(engineDocument, engineWindow, host
|
|
|
129
129
|
passiveLastWallTime: 0,
|
|
130
130
|
cachedTabRendererHeight: null,
|
|
131
131
|
cachedFooterItem: null,
|
|
132
|
+
cachedCreditsItem: null,
|
|
133
|
+
creditsFocused: false,
|
|
132
134
|
cachedLineScrollTiming: new Map(),
|
|
133
135
|
tabRendererResizeObserver: null,
|
|
134
136
|
observedTabRenderer: null,
|
|
@@ -280,6 +282,8 @@ export function clearLyrics(engine) {
|
|
|
280
282
|
stopPassiveScrollLoop(engine);
|
|
281
283
|
engine.lines = [];
|
|
282
284
|
engine.cachedFooterItem = null;
|
|
285
|
+
engine.cachedCreditsItem = null;
|
|
286
|
+
engine.creditsFocused = false;
|
|
283
287
|
engine.lyricsContainer = null;
|
|
284
288
|
engine.waveAnimationPool.length = 0;
|
|
285
289
|
}
|
|
@@ -1464,19 +1468,19 @@ function updateVisibleLyricWillChange(engine, lines, fromScrollTop, toScrollTop,
|
|
|
1464
1468
|
}
|
|
1465
1469
|
engine.visibleWillChangeElements = nextVisibleElements;
|
|
1466
1470
|
}
|
|
1467
|
-
//
|
|
1468
|
-
// a
|
|
1471
|
+
// The credits and the footer after the last line scroll with the lines. Their bounds come from the
|
|
1472
|
+
// relayout-time cache, not a getBoundingClientRect here, which would force a synchronous recalc
|
|
1473
|
+
// mid-tick after the scroll commit dirtied style.
|
|
1469
1474
|
function getLineScrollItems(engine, lines) {
|
|
1470
|
-
|
|
1475
|
+
const trailing = [engine.cachedCreditsItem, engine.cachedFooterItem].filter(item => item !== null);
|
|
1476
|
+
return trailing.length > 0 ? [...lines, ...trailing] : lines;
|
|
1471
1477
|
}
|
|
1472
|
-
function
|
|
1473
|
-
const
|
|
1474
|
-
if (!
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
}
|
|
1478
|
-
const footerBounds = getRelativeLayoutBounds(lyricsElement, footer);
|
|
1479
|
-
engine.cachedFooterItem = { lyricElement: footer, position: footerBounds.y, height: footerBounds.height };
|
|
1478
|
+
function measureTrailingItem(lyricsElement, className) {
|
|
1479
|
+
const element = lyricsElement.querySelector(`:scope > .${className}`);
|
|
1480
|
+
if (!element)
|
|
1481
|
+
return null;
|
|
1482
|
+
const bounds = getRelativeLayoutBounds(lyricsElement, element);
|
|
1483
|
+
return { lyricElement: element, position: bounds.y, height: bounds.height };
|
|
1480
1484
|
}
|
|
1481
1485
|
function prepareLineScrollOffsets(engine, lines, activeLineIndex, scrollDeltaPx, fromScrollTop, toScrollTop, viewportHeight, config) {
|
|
1482
1486
|
if (!config.enabled.scroll || activeLineIndex < 0) {
|
|
@@ -1989,6 +1993,18 @@ export function tickView(engine, currentTime, options) {
|
|
|
1989
1993
|
setAnimationsPlayState(lineData, false);
|
|
1990
1994
|
}
|
|
1991
1995
|
}
|
|
1996
|
+
const lastSungLine = lines.findLast(lineData => lineData.lyricElement.dataset.instrumental !== "true");
|
|
1997
|
+
const creditsFocused = lastSungLine !== undefined &&
|
|
1998
|
+
(engine.cachedCreditsItem?.height ?? 0) > 0 &&
|
|
1999
|
+
lyricScrollTime >= lastSungLine.time + lastSungLine.duration;
|
|
2000
|
+
if (creditsFocused !== engine.creditsFocused) {
|
|
2001
|
+
engine.creditsFocused = creditsFocused;
|
|
2002
|
+
if (creditsFocused)
|
|
2003
|
+
lyricsElement.dataset.creditsFocused = "true";
|
|
2004
|
+
else
|
|
2005
|
+
delete lyricsElement.dataset.creditsFocused;
|
|
2006
|
+
newLyricSelected = true;
|
|
2007
|
+
}
|
|
1992
2008
|
if (engine.scrollResumeTime < Date.now() || engine.scrollPos === -1) {
|
|
1993
2009
|
if (activeElems.length == 0) {
|
|
1994
2010
|
activeElems.push(lines[0]);
|
|
@@ -2014,6 +2030,10 @@ export function tickView(engine, currentTime, options) {
|
|
|
2014
2030
|
scrollPos = Math.max(scrollPos, lastActiveLyric.position - tabRendererHeight + lastActiveLyric.height);
|
|
2015
2031
|
// Make sure top of last active lyric is visible.
|
|
2016
2032
|
scrollPos = Math.min(scrollPos, lastActiveLyric.position);
|
|
2033
|
+
const credits = engine.cachedCreditsItem;
|
|
2034
|
+
if (engine.creditsFocused && credits) {
|
|
2035
|
+
scrollPos = credits.position + credits.height / 2 - scrollPosOffset;
|
|
2036
|
+
}
|
|
2017
2037
|
// Past either end the browser clamps the write and reports nothing, leaving the view aiming
|
|
2018
2038
|
// at a position it never reached and re-aiming once per remaining line.
|
|
2019
2039
|
scrollPos = clamp(scrollPos, 0, maxScrollTop);
|
|
@@ -2183,7 +2203,14 @@ function applyScrollPadding(engine) {
|
|
|
2183
2203
|
const lyricLines = lyricsElement.querySelectorAll(`:scope > .${LINE_CLASS}`);
|
|
2184
2204
|
const firstLyric = lyricLines[0] ?? null;
|
|
2185
2205
|
const lastLyric = lyricLines[lyricLines.length - 1] ?? null;
|
|
2186
|
-
const
|
|
2206
|
+
const credits = lyricsElement.querySelector(`:scope > .${CREDITS_CLASS}`);
|
|
2207
|
+
const creditsBounds = credits ? getRelativeLayoutBounds(lyricsElement, credits) : null;
|
|
2208
|
+
// The credits take the focus once the song ends, so they are the last thing the scroll must reach.
|
|
2209
|
+
const lastLyricBounds = creditsBounds && creditsBounds.height > 0
|
|
2210
|
+
? creditsBounds
|
|
2211
|
+
: lastLyric
|
|
2212
|
+
? getRelativeLayoutBounds(lyricsElement, lastLyric)
|
|
2213
|
+
: null;
|
|
2187
2214
|
const footer = lyricsElement.querySelector(`:scope > .${FOOTER_CLASS}`);
|
|
2188
2215
|
const { top, bottom } = computeScrollPadding({
|
|
2189
2216
|
viewportHeight: tabRendererHeight,
|
|
@@ -2227,7 +2254,8 @@ export function relayout(engine, measureLines) {
|
|
|
2227
2254
|
line.height = bounds.height;
|
|
2228
2255
|
line.decorations = new Map(lineDecorators(line.lyricElement).map(element => [element, getRelativeLayoutBounds(lyricsElement, element).y]));
|
|
2229
2256
|
}
|
|
2230
|
-
|
|
2257
|
+
engine.cachedCreditsItem = measureTrailingItem(lyricsElement, CREDITS_CLASS);
|
|
2258
|
+
engine.cachedFooterItem = measureTrailingItem(lyricsElement, FOOTER_CLASS);
|
|
2231
2259
|
// Re-arm from the fresh measurements, so a line skipped after this holds its new placeholder height.
|
|
2232
2260
|
setupLineCullObserver(engine);
|
|
2233
2261
|
engine.wasUserScrolling = true; // trigger rescrolls
|
package/dist/inject.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type WordState } from "./constants.js";
|
|
2
2
|
import type { Lyric, LyricPart, LyricSyncType } from "./types.js";
|
|
3
3
|
export declare let disableRichsync: import("./themeSettings.js").Setting;
|
|
4
|
+
export declare const hideCredits: import("./themeSettings.js").Setting;
|
|
4
5
|
export declare function findNearestAgent(lyrics: Lyric[], fromIndex: number): string | undefined;
|
|
5
6
|
export declare function isNearestLyricRtl(lyrics: Lyric[], fromIndex: number): boolean;
|
|
6
7
|
export declare function deriveSyncType(lyrics: Lyric[]): LyricSyncType;
|
package/dist/inject.js
CHANGED
|
@@ -21,6 +21,7 @@ let lineSyncedAnimationDelay = registerThemeSetting("blyrics-line-synced-animati
|
|
|
21
21
|
let longWordThreshold = registerThemeSetting("blyrics-long-word-threshold", 1500, true);
|
|
22
22
|
let longWordWrapThreshold = registerThemeSetting("blyrics-long-word-wrap-threshold", 10, true);
|
|
23
23
|
let letterWave = registerThemeSetting("blyrics-letter-wave", true, true);
|
|
24
|
+
export const hideCredits = registerThemeSetting("blyrics-hide-credits", false, true);
|
|
24
25
|
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
25
26
|
const RTL_SCRIPT_REGEX = /[\p{Script=Arabic}\p{Script=Hebrew}\p{Script=Syriac}\p{Script=Thaana}]/u;
|
|
26
27
|
const LTR_SCRIPT_REGEX = /[\p{Script=Latin}\p{Script=Greek}\p{Script=Cyrillic}\p{Script=Han}\p{Script=Hangul}\p{Script=Hiragana}\p{Script=Katakana}]/u;
|
package/dist/renderer.js
CHANGED
|
@@ -260,6 +260,7 @@ export function createLyricsRenderer(rendererOptions) {
|
|
|
260
260
|
language: options?.language,
|
|
261
261
|
loaderVisible: options?.loaderVisible ?? false,
|
|
262
262
|
noLyrics: options?.noLyrics ?? false,
|
|
263
|
+
songwriters: options?.songwriters,
|
|
263
264
|
});
|
|
264
265
|
currentLyrics = lyrics;
|
|
265
266
|
currentLanguage = normalizeLanguage(options?.language);
|
package/dist/styles/lyrics.css
CHANGED
|
@@ -215,11 +215,6 @@
|
|
|
215
215
|
color: color-mix(in srgb, var(--blyrics-lyric-inactive-color) 78%, transparent);
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
.blyrics-explicit {
|
|
219
|
-
text-decoration: line-through;
|
|
220
|
-
text-decoration-thickness: 0.08em;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
218
|
.blyrics--translated,
|
|
224
219
|
.blyrics--romanized {
|
|
225
220
|
animation: blyrics-decoration-in calc(var(--blyrics-animate-decoration-entry) * 0.35s)
|
|
@@ -244,6 +239,26 @@
|
|
|
244
239
|
}
|
|
245
240
|
}
|
|
246
241
|
|
|
242
|
+
.blyrics-container > .blyrics-credits {
|
|
243
|
+
cursor: default;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.blyrics-credits {
|
|
247
|
+
color: var(--blyrics-ui-text-color);
|
|
248
|
+
font-size: var(--blyrics-credits-font-size);
|
|
249
|
+
opacity: var(--blyrics-credits-opacity);
|
|
250
|
+
transition: opacity 0.35s cubic-bezier(0.25, 1, 0.5, 1);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.blyrics-credits::before {
|
|
254
|
+
content: var(--blyrics-credits-label) " ";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.blyrics-container[data-credits-focused] > .blyrics-credits,
|
|
258
|
+
.blyrics-container[data-sync="none"] > .blyrics-credits {
|
|
259
|
+
opacity: var(--blyrics-credits-focused-opacity);
|
|
260
|
+
}
|
|
261
|
+
|
|
247
262
|
.blyrics--romanized {
|
|
248
263
|
background: rgba(255, 255, 255, 0.05);
|
|
249
264
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
@@ -50,6 +50,11 @@
|
|
|
50
50
|
display-p3 1 1 1 / var(--blyrics-translated-opacity, 0.6)
|
|
51
51
|
);
|
|
52
52
|
|
|
53
|
+
--blyrics-credits-label: "Written by";
|
|
54
|
+
--blyrics-credits-font-size: max(0.4em, 12px);
|
|
55
|
+
--blyrics-credits-opacity: 0.2;
|
|
56
|
+
--blyrics-credits-focused-opacity: 0.85;
|
|
57
|
+
|
|
53
58
|
--blyrics-footer-font-family: Satoshi, Roboto, Noto Naskh Arabic UI, Arial, sans-serif;
|
|
54
59
|
--blyrics-footer-font-size: 14px;
|
|
55
60
|
--blyrics-footer-font-weight: 400;
|
package/dist/view.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface SetLyricsOptions {
|
|
|
11
11
|
* These lyrics are a "not found" placeholder rather than a real result.
|
|
12
12
|
*/
|
|
13
13
|
noLyrics: boolean;
|
|
14
|
+
/** Who wrote the song, shown after the last line. */
|
|
15
|
+
songwriters?: readonly string[];
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
18
|
* Replaces whatever the mount holds with a container built from these lyrics, and hands the render
|
package/dist/view.js
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
// This is also the one place outside the engine that writes engine view state, because building the
|
|
10
10
|
// lines is where those values are first knowable: the container, the records, the sync type, the
|
|
11
11
|
// size they were measured at, and the scrolls to swallow before the view has settled.
|
|
12
|
-
import { LINE_CLASS, LYRICS_CLASS, RTL_CLASS } from "./constants.js";
|
|
12
|
+
import { CREDITS_CLASS, CREDITS_NAMES_CLASS, LINE_CLASS, LYRICS_CLASS, RTL_CLASS } from "./constants.js";
|
|
13
13
|
import { setupLineCullObserver } from "./engine.js";
|
|
14
|
-
import { addSeekHandler, applyDirection, buildLineSyncedParts, createLyricsLine, deriveSyncType, disableRichsync, findNearestAgent, isNearestLyricRtl, newLineData, } from "./inject.js";
|
|
14
|
+
import { addSeekHandler, applyDirection, buildLineSyncedParts, createLyricsLine, deriveSyncType, disableRichsync, findNearestAgent, hideCredits, isNearestLyricRtl, newLineData, } from "./inject.js";
|
|
15
15
|
import { createInstrumentalElement } from "./instrumental.js";
|
|
16
16
|
import { applyLyricLanguage, resolveLyricLanguages } from "./language.js";
|
|
17
17
|
const INITIAL_SKIP_SCROLLS = 2;
|
|
@@ -41,6 +41,19 @@ function buildSungLine(doc, lyricElement, lyricItem, line) {
|
|
|
41
41
|
lyricElement.dataset.agent = lyricItem.agent;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
function formatSongwriters(names) {
|
|
45
|
+
return names.length > 1 ? `${names.slice(0, -1).join(", ")} & ${names.at(-1)}` : (names[0] ?? "");
|
|
46
|
+
}
|
|
47
|
+
function buildCredits(doc, songwriters) {
|
|
48
|
+
const credits = doc.createElement("div");
|
|
49
|
+
credits.className = CREDITS_CLASS;
|
|
50
|
+
const names = doc.createElement("span");
|
|
51
|
+
names.className = CREDITS_NAMES_CLASS;
|
|
52
|
+
names.dir = "auto";
|
|
53
|
+
names.textContent = formatSongwriters(songwriters);
|
|
54
|
+
credits.appendChild(names);
|
|
55
|
+
return credits;
|
|
56
|
+
}
|
|
44
57
|
/**
|
|
45
58
|
* Replaces whatever the mount holds with a container built from these lyrics, and hands the render
|
|
46
59
|
* records, the container and its measured size to the engine that animates them.
|
|
@@ -79,6 +92,10 @@ export function setLyrics(engine, mount, lyrics, options) {
|
|
|
79
92
|
}
|
|
80
93
|
container.appendChild(lyricElement);
|
|
81
94
|
}
|
|
95
|
+
const songwriters = options.songwriters ?? [];
|
|
96
|
+
if (songwriters.length > 0 && !options.noLyrics && !hideCredits.getBooleanValue()) {
|
|
97
|
+
container.appendChild(buildCredits(doc, songwriters));
|
|
98
|
+
}
|
|
82
99
|
engine.skipScrolls = INITIAL_SKIP_SCROLLS;
|
|
83
100
|
engine.skipScrollsDecayTimes = Array.from({ length: INITIAL_SKIP_SCROLLS }, () => Date.now() + SKIP_SCROLL_DECAY_MS);
|
|
84
101
|
engine.scrollResumeTime = 0;
|