@braccato/core 1.6.3 → 1.7.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 +25 -0
- package/dist/README.md +25 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +4 -0
- package/dist/engine.d.ts +1 -0
- package/dist/engine.js +26 -1
- package/dist/inject.d.ts +2 -0
- package/dist/inject.js +18 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -213,6 +213,31 @@ The split multiplies the DOM per character and reruns the karaoke sweep per lett
|
|
|
213
213
|
does not want the cost turns it off with `/* blyrics-letter-wave = false; */`. `blyrics-letter-wave`
|
|
214
214
|
reloads the lines when it changes, the way every build-time setting does.
|
|
215
215
|
|
|
216
|
+
### Word state
|
|
217
|
+
|
|
218
|
+
The renderer writes `data-word-state` on every word as the song crosses it, so a theme can tell a
|
|
219
|
+
word being sung from one already sung or one not yet reached. It carries `upcoming` before the word
|
|
220
|
+
starts, `active` while it is being sung, and `past` once it has finished, and it is written on both
|
|
221
|
+
layers of a word, the base `.blyrics--word` and its highlight overlay, so a theme can key either. A
|
|
222
|
+
word stays `active` for exactly as long as it is sung, so a transition on the flip is the whole of a
|
|
223
|
+
per-word karaoke effect, the one ramansg/Max-Performance built before the swept overlay.
|
|
224
|
+
|
|
225
|
+
```css
|
|
226
|
+
.blyrics--word {
|
|
227
|
+
color: var(--blyrics-lyric-inactive-color);
|
|
228
|
+
transition: color 180ms ease;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.blyrics--word[data-word-state="active"],
|
|
232
|
+
.blyrics--word[data-word-state="past"] {
|
|
233
|
+
color: var(--blyrics-lyric-active-color);
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
It is written only when a word's state changes rather than every frame, so selecting on it costs a
|
|
238
|
+
theme nothing per tick. A line synced word, which has no duration of its own, is `active` from its
|
|
239
|
+
start until the next word begins.
|
|
240
|
+
|
|
216
241
|
### Class names
|
|
217
242
|
|
|
218
243
|
These are published API rather than implementation. Renaming one costs a migration rather than a
|
package/dist/README.md
CHANGED
|
@@ -213,6 +213,31 @@ The split multiplies the DOM per character and reruns the karaoke sweep per lett
|
|
|
213
213
|
does not want the cost turns it off with `/* blyrics-letter-wave = false; */`. `blyrics-letter-wave`
|
|
214
214
|
reloads the lines when it changes, the way every build-time setting does.
|
|
215
215
|
|
|
216
|
+
### Word state
|
|
217
|
+
|
|
218
|
+
The renderer writes `data-word-state` on every word as the song crosses it, so a theme can tell a
|
|
219
|
+
word being sung from one already sung or one not yet reached. It carries `upcoming` before the word
|
|
220
|
+
starts, `active` while it is being sung, and `past` once it has finished, and it is written on both
|
|
221
|
+
layers of a word, the base `.blyrics--word` and its highlight overlay, so a theme can key either. A
|
|
222
|
+
word stays `active` for exactly as long as it is sung, so a transition on the flip is the whole of a
|
|
223
|
+
per-word karaoke effect, the one ramansg/Max-Performance built before the swept overlay.
|
|
224
|
+
|
|
225
|
+
```css
|
|
226
|
+
.blyrics--word {
|
|
227
|
+
color: var(--blyrics-lyric-inactive-color);
|
|
228
|
+
transition: color 180ms ease;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.blyrics--word[data-word-state="active"],
|
|
232
|
+
.blyrics--word[data-word-state="past"] {
|
|
233
|
+
color: var(--blyrics-lyric-active-color);
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
It is written only when a word's state changes rather than every frame, so selecting on it costs a
|
|
238
|
+
theme nothing per tick. A line synced word, which has no duration of its own, is `active` from its
|
|
239
|
+
start until the next word begins.
|
|
240
|
+
|
|
216
241
|
### Class names
|
|
217
242
|
|
|
218
243
|
These are published API rather than implementation. Renaming one costs a migration rather than a
|
package/dist/constants.d.ts
CHANGED
|
@@ -9,6 +9,11 @@ export declare const ANIMATING_CLASS: "blyrics--animating";
|
|
|
9
9
|
export declare const PAUSED_CLASS: "blyrics--paused";
|
|
10
10
|
export declare const ZERO_DURATION_ANIMATION_CLASS: "blyrics-zero-dur-animate";
|
|
11
11
|
export declare const USER_SCROLLING_CLASS: "blyrics-user-scrolling";
|
|
12
|
+
export declare const WORD_STATE_ATTR: "data-word-state";
|
|
13
|
+
export declare const WORD_STATE_UPCOMING: "upcoming";
|
|
14
|
+
export declare const WORD_STATE_ACTIVE: "active";
|
|
15
|
+
export declare const WORD_STATE_PAST: "past";
|
|
16
|
+
export type WordState = typeof WORD_STATE_UPCOMING | typeof WORD_STATE_ACTIVE | typeof WORD_STATE_PAST;
|
|
12
17
|
export declare const BACKGROUND_LYRIC_CLASS: "blyrics-background-lyric";
|
|
13
18
|
export declare const EXPLICIT_WORD_CLASS: "blyrics-explicit";
|
|
14
19
|
export declare const RTL_CLASS: "blyrics-rtl";
|
package/dist/constants.js
CHANGED
|
@@ -13,6 +13,10 @@ export const ANIMATING_CLASS = "blyrics--animating";
|
|
|
13
13
|
export const PAUSED_CLASS = "blyrics--paused";
|
|
14
14
|
export const ZERO_DURATION_ANIMATION_CLASS = "blyrics-zero-dur-animate";
|
|
15
15
|
export const USER_SCROLLING_CLASS = "blyrics-user-scrolling";
|
|
16
|
+
export const WORD_STATE_ATTR = "data-word-state";
|
|
17
|
+
export const WORD_STATE_UPCOMING = "upcoming";
|
|
18
|
+
export const WORD_STATE_ACTIVE = "active";
|
|
19
|
+
export const WORD_STATE_PAST = "past";
|
|
16
20
|
// -- Line and word variants --------------------------------------------
|
|
17
21
|
export const BACKGROUND_LYRIC_CLASS = "blyrics-background-lyric";
|
|
18
22
|
export const EXPLICIT_WORD_CLASS = "blyrics-explicit";
|
package/dist/engine.d.ts
CHANGED
|
@@ -127,6 +127,7 @@ export declare function getRenderedSyncType(engine: AnimationEngineInstance): Ly
|
|
|
127
127
|
* hold are released along with it.
|
|
128
128
|
*/
|
|
129
129
|
export declare function clearLyrics(engine: AnimationEngineInstance): void;
|
|
130
|
+
export declare function updateWordStates(lineData: LineData, currentTime: number): void;
|
|
130
131
|
type LineScrollSide = "above" | "active" | "below";
|
|
131
132
|
interface LineScrollItem {
|
|
132
133
|
lyricElement: HTMLElement;
|
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, } from "./constants.js";
|
|
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";
|
|
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";
|
|
@@ -345,6 +345,26 @@ function clearLineStateClasses(lineData) {
|
|
|
345
345
|
togglePartClass(part, PAUSED_CLASS, false);
|
|
346
346
|
}
|
|
347
347
|
}
|
|
348
|
+
export function updateWordStates(lineData, currentTime) {
|
|
349
|
+
const parts = lineData.parts;
|
|
350
|
+
const lineEndTime = lineData.time + lineData.duration;
|
|
351
|
+
for (let i = 0; i < parts.length; i++) {
|
|
352
|
+
const part = parts[i];
|
|
353
|
+
const endTime = part.duration > 0 ? part.time + part.duration : (parts[i + 1]?.time ?? lineEndTime);
|
|
354
|
+
let state = WORD_STATE_PAST;
|
|
355
|
+
if (currentTime < part.time) {
|
|
356
|
+
state = WORD_STATE_UPCOMING;
|
|
357
|
+
}
|
|
358
|
+
else if (currentTime < endTime) {
|
|
359
|
+
state = WORD_STATE_ACTIVE;
|
|
360
|
+
}
|
|
361
|
+
if (part.wordState === state)
|
|
362
|
+
continue;
|
|
363
|
+
part.wordState = state;
|
|
364
|
+
part.lyricElement.setAttribute(WORD_STATE_ATTR, state);
|
|
365
|
+
part.highlightElement.setAttribute(WORD_STATE_ATTR, state);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
348
368
|
const LINE_SYNCED_WORD_CLASS = "blyrics-line-synced-word";
|
|
349
369
|
const INSTRUMENTAL_FILL_SELECTOR = ".blyrics--instrumental-fill";
|
|
350
370
|
const INSTRUMENTAL_WAVE_CLIP_SELECTOR = ".blyrics--wave-clip";
|
|
@@ -1936,6 +1956,7 @@ export function tickView(engine, currentTime, options) {
|
|
|
1936
1956
|
if (isPlaying)
|
|
1937
1957
|
lineData.isAnimating = false; // reset the animation against current media time
|
|
1938
1958
|
}
|
|
1959
|
+
updateWordStates(lineData, currentTime);
|
|
1939
1960
|
const nativeTimingSample = lineNativeTimingSample(lineData, currentTime);
|
|
1940
1961
|
let usedNativeTimingSampleForDrift = false;
|
|
1941
1962
|
lineData.accumulatedOffsetMs = lineData.accumulatedOffsetMs / ANIMATION_TIMING_ACCUMULATION_DECAY;
|
|
@@ -1984,11 +2005,15 @@ export function tickView(engine, currentTime, options) {
|
|
|
1984
2005
|
}
|
|
1985
2006
|
lineData.isSelected = false;
|
|
1986
2007
|
clearLineStateClasses(lineData);
|
|
2008
|
+
updateWordStates(lineData, currentTime);
|
|
1987
2009
|
}
|
|
1988
2010
|
else if (hasLineAnimations(lineData) && (timeJumped || currentTime > staleAnimationEndTime)) {
|
|
1989
2011
|
logAnimationCleanup(engine, timeJumped ? "time-jump-reset" : "stale-reset", lineData, index, currentTime, staleAnimationEndTime);
|
|
1990
2012
|
resetLineAnimationState(lineData);
|
|
1991
2013
|
}
|
|
2014
|
+
if (timeJumped) {
|
|
2015
|
+
updateWordStates(lineData, currentTime);
|
|
2016
|
+
}
|
|
1992
2017
|
}
|
|
1993
2018
|
return true;
|
|
1994
2019
|
});
|
package/dist/inject.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type WordState } from "./constants.js";
|
|
1
2
|
import type { Lyric, LyricPart, LyricSyncType } from "./types.js";
|
|
2
3
|
export declare let disableRichsync: import("./themeSettings.js").Setting;
|
|
3
4
|
export declare function findNearestAgent(lyrics: Lyric[], fromIndex: number): string | undefined;
|
|
@@ -20,6 +21,7 @@ export interface PartData extends AnimationData {
|
|
|
20
21
|
letterElements?: HTMLElement[];
|
|
21
22
|
highlightLetterElements?: HTMLElement[];
|
|
22
23
|
glowSuppressed?: boolean;
|
|
24
|
+
wordState: WordState;
|
|
23
25
|
}
|
|
24
26
|
export type LineData = {
|
|
25
27
|
parts: PartData[];
|
package/dist/inject.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// animates is one word, so every part is split on whitespace with its timing pro-rated across the
|
|
12
12
|
// split by character count. A line that arrives with no timed parts at all is rebuilt the same way
|
|
13
13
|
// into zero duration words, so line synced lyrics reach the DOM the sweep already knows.
|
|
14
|
-
import { BACKGROUND_LINE_CLASS, BACKGROUND_LYRIC_CLASS, BIDI_RUN_CLASS, BIDI_SENSITIVE_CLASS, CONTENT_LINE_CLASS, EXPLICIT_WORD_CLASS, HIGHLIGHT_RUN_CLASS, LETTER_CLASS, LINE_MAIN_CLASS, LINE_SYNCED_WORD_CLASS, LONG_WORD_GROUP_CLASS, ROMANIZED_LYRICS_CLASS, RTL_CLASS, TRANSLATED_LYRICS_CLASS, WORD_CLASS, WORD_GROUP_CLASS, WORD_HIGHLIGHT_CLASS, WORD_HIGHLIGHT_LETTERED_CLASS, ZERO_DURATION_ANIMATION_CLASS, } from "./constants.js";
|
|
14
|
+
import { BACKGROUND_LINE_CLASS, BACKGROUND_LYRIC_CLASS, BIDI_RUN_CLASS, BIDI_SENSITIVE_CLASS, CONTENT_LINE_CLASS, EXPLICIT_WORD_CLASS, HIGHLIGHT_RUN_CLASS, LETTER_CLASS, LINE_MAIN_CLASS, LINE_SYNCED_WORD_CLASS, LONG_WORD_GROUP_CLASS, ROMANIZED_LYRICS_CLASS, RTL_CLASS, TRANSLATED_LYRICS_CLASS, WORD_CLASS, WORD_GROUP_CLASS, WORD_HIGHLIGHT_CLASS, WORD_HIGHLIGHT_LETTERED_CLASS, WORD_STATE_ATTR, WORD_STATE_UPCOMING, ZERO_DURATION_ANIMATION_CLASS, } from "./constants.js";
|
|
15
15
|
import { getSeekTimeFromClick } from "./seek.js";
|
|
16
16
|
import { testRtl } from "./text.js";
|
|
17
17
|
import { registerThemeSetting } from "./themeSettings.js";
|
|
@@ -20,6 +20,7 @@ let lineSyncedAnimationDelay = registerThemeSetting("blyrics-line-synced-animati
|
|
|
20
20
|
let longWordThreshold = registerThemeSetting("blyrics-long-word-threshold", 1500, true);
|
|
21
21
|
let longWordWrapThreshold = registerThemeSetting("blyrics-long-word-wrap-threshold", 10, true);
|
|
22
22
|
let letterWave = registerThemeSetting("blyrics-letter-wave", true, true);
|
|
23
|
+
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
23
24
|
const RTL_SCRIPT_REGEX = /[\p{Script=Arabic}\p{Script=Hebrew}\p{Script=Syriac}\p{Script=Thaana}]/u;
|
|
24
25
|
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;
|
|
25
26
|
const SPACE_REGEX = /^\s+$/u;
|
|
@@ -70,6 +71,7 @@ function newPartData(part, span, highlight, letterElements, highlightLetterEleme
|
|
|
70
71
|
letterElements,
|
|
71
72
|
highlightLetterElements,
|
|
72
73
|
animations: [],
|
|
74
|
+
wordState: WORD_STATE_UPCOMING,
|
|
73
75
|
};
|
|
74
76
|
}
|
|
75
77
|
export function newLineData(lyricElement, startTimeMs, durationMs) {
|
|
@@ -199,7 +201,7 @@ function appendLongWordBreaks(doc, span, text, threshold) {
|
|
|
199
201
|
return true;
|
|
200
202
|
}
|
|
201
203
|
function appendLetters(doc, wordElement, text) {
|
|
202
|
-
const chars = [...text];
|
|
204
|
+
const chars = [...graphemeSegmenter.segment(text)].map(segment => segment.segment);
|
|
203
205
|
wordElement.style.setProperty("--letters", String(chars.length));
|
|
204
206
|
// Width of the reveal mask's soft edge as a percentage of its (n+2)-letter-wide box, so the fade
|
|
205
207
|
// spans the same 0.1*letters of a letter that the old gradient did. See the mask rule in lyrics.css.
|
|
@@ -249,6 +251,7 @@ function createTimedWordSpan(doc, part, wrapThreshold, perLetter) {
|
|
|
249
251
|
wordElement.dataset.time = String(part.startTimeMs / 1000);
|
|
250
252
|
wordElement.dataset.duration = String(part.durationMs / 1000);
|
|
251
253
|
wordElement.dataset.content = part.words;
|
|
254
|
+
wordElement.setAttribute(WORD_STATE_ATTR, WORD_STATE_UPCOMING);
|
|
252
255
|
wordElement.style.setProperty("--blyrics-duration", part.durationMs + "ms");
|
|
253
256
|
}
|
|
254
257
|
return { span, highlight, letters, highlightLetters };
|
|
@@ -298,6 +301,15 @@ function createHighlightRun(doc, text) {
|
|
|
298
301
|
run.setAttribute("aria-hidden", "true");
|
|
299
302
|
return run;
|
|
300
303
|
}
|
|
304
|
+
// A background gap must carry the background size too, or it renders full-size and the gap looks oversized.
|
|
305
|
+
function createSpaceNode(doc, text, isBackground) {
|
|
306
|
+
if (!isBackground)
|
|
307
|
+
return doc.createTextNode(text);
|
|
308
|
+
const span = doc.createElement("span");
|
|
309
|
+
span.classList.add(BACKGROUND_LYRIC_CLASS);
|
|
310
|
+
span.textContent = text;
|
|
311
|
+
return span;
|
|
312
|
+
}
|
|
301
313
|
export function createLyricsLine(doc, parts, line, lyricElement, options = { splitBackgroundLine: true }) {
|
|
302
314
|
const lineText = parts.map(part => part.words).join("");
|
|
303
315
|
const mainText = options.splitBackgroundLine
|
|
@@ -336,8 +348,8 @@ export function createLyricsLine(doc, parts, line, lyricElement, options = { spl
|
|
|
336
348
|
const highlightRun = shouldUseBackgroundLine ? backgroundHighlightRun : mainHighlightRun;
|
|
337
349
|
const pendingSpace = shouldUseBackgroundLine ? pendingBackgroundSpace : pendingForegroundSpace;
|
|
338
350
|
if (target.childNodes.length > 0 && pendingSpace.length > 0) {
|
|
339
|
-
target.appendChild(doc
|
|
340
|
-
highlightRun.appendChild(doc
|
|
351
|
+
target.appendChild(createSpaceNode(doc, pendingSpace, shouldUseBackgroundLine));
|
|
352
|
+
highlightRun.appendChild(createSpaceNode(doc, pendingSpace, shouldUseBackgroundLine));
|
|
341
353
|
}
|
|
342
354
|
const { lyricGroup, highlightGroup } = createWordGroup(doc, item, line);
|
|
343
355
|
target.appendChild(lyricGroup);
|
|
@@ -400,7 +412,8 @@ export function injectRomanization(doc, lyricElement, lineData, text, timedRoman
|
|
|
400
412
|
else {
|
|
401
413
|
romanizedLine.textContent = text;
|
|
402
414
|
}
|
|
403
|
-
lyricElement.
|
|
415
|
+
const translation = lyricElement.querySelector(`.${TRANSLATED_LYRICS_CLASS}`);
|
|
416
|
+
lyricElement.insertBefore(romanizedLine, translation);
|
|
404
417
|
}
|
|
405
418
|
export function injectTranslation(doc, lyricElement, text) {
|
|
406
419
|
if (lyricElement.querySelector(`.${TRANSLATED_LYRICS_CLASS}`))
|