@braccato/core 1.1.0 → 1.2.1
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 +7 -0
- package/dist/README.md +7 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/engine.d.ts +2 -0
- package/dist/engine.js +47 -42
- package/dist/inject.d.ts +1 -0
- package/dist/inject.js +58 -53
- package/dist/instrumental.d.ts +2 -0
- package/dist/instrumental.js +3 -2
- package/dist/renderer.js +4 -0
- package/dist/styles/instrumental.css +2 -11
- package/dist/styles/lyrics.css +19 -43
- package/dist/styles/variables.css +2 -0
- package/package.json +1 -1
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/constants.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export declare const BACKGROUND_LINE_CLASS: "blyrics-background-line";
|
|
|
19
19
|
export declare const WORD_GROUP_CLASS: "blyrics-word-group";
|
|
20
20
|
export declare const LONG_WORD_GROUP_CLASS: "blyrics-word-group-long";
|
|
21
21
|
export declare const WORD_HIGHLIGHT_CLASS: "blyrics-word-highlight";
|
|
22
|
+
export declare const HIGHLIGHT_RUN_CLASS: "blyrics-highlight-run";
|
|
22
23
|
export declare const LINE_SYNCED_WORD_CLASS: "blyrics-line-synced-word";
|
|
23
24
|
export declare const BIDI_RUN_CLASS: "blyrics-bidi-run";
|
|
24
25
|
export declare const BIDI_SENSITIVE_CLASS: "blyrics-bidi-sensitive";
|
package/dist/constants.js
CHANGED
|
@@ -25,6 +25,7 @@ export const BACKGROUND_LINE_CLASS = "blyrics-background-line";
|
|
|
25
25
|
export const WORD_GROUP_CLASS = "blyrics-word-group";
|
|
26
26
|
export const LONG_WORD_GROUP_CLASS = "blyrics-word-group-long";
|
|
27
27
|
export const WORD_HIGHLIGHT_CLASS = "blyrics-word-highlight";
|
|
28
|
+
export const HIGHLIGHT_RUN_CLASS = "blyrics-highlight-run";
|
|
28
29
|
export const LINE_SYNCED_WORD_CLASS = "blyrics-line-synced-word";
|
|
29
30
|
export const BIDI_RUN_CLASS = "blyrics-bidi-run";
|
|
30
31
|
export const BIDI_SENSITIVE_CLASS = "blyrics-bidi-sensitive";
|
package/dist/engine.d.ts
CHANGED
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";
|
|
@@ -301,10 +302,14 @@ function resetLineAnimationState(lineData) {
|
|
|
301
302
|
resetLineAnimations(lineData);
|
|
302
303
|
markLineAnimationsStopped(lineData);
|
|
303
304
|
}
|
|
305
|
+
function togglePartClass(part, className, force) {
|
|
306
|
+
part.lyricElement.classList.toggle(className, force);
|
|
307
|
+
part.highlightElement?.classList.toggle(className, force);
|
|
308
|
+
}
|
|
304
309
|
function setAnimationsPlayState(lineData, isPlaying) {
|
|
305
310
|
const children = [lineData, ...lineData.parts];
|
|
306
311
|
for (const part of children) {
|
|
307
|
-
part
|
|
312
|
+
togglePartClass(part, PAUSED_CLASS, !isPlaying);
|
|
308
313
|
for (const animation of part.animations) {
|
|
309
314
|
if (isPlaying) {
|
|
310
315
|
animation.play();
|
|
@@ -318,16 +323,15 @@ function setAnimationsPlayState(lineData, isPlaying) {
|
|
|
318
323
|
function clearLineStateClasses(lineData) {
|
|
319
324
|
lineData.lyricElement.classList.remove(ANIMATING_CLASS);
|
|
320
325
|
for (const part of [lineData, ...lineData.parts]) {
|
|
321
|
-
part
|
|
326
|
+
togglePartClass(part, PAUSED_CLASS, false);
|
|
322
327
|
}
|
|
323
328
|
}
|
|
324
329
|
const LINE_SYNCED_WORD_CLASS = "blyrics-line-synced-word";
|
|
325
|
-
const WORD_HIGHLIGHT_SELECTOR = ".blyrics-word-highlight";
|
|
326
330
|
const INSTRUMENTAL_FILL_SELECTOR = ".blyrics--instrumental-fill";
|
|
327
331
|
const INSTRUMENTAL_WAVE_CLIP_SELECTOR = ".blyrics--wave-clip";
|
|
328
332
|
const INSTRUMENTAL_WAVE_PATH_SELECTOR = ".blyrics--wave-path";
|
|
329
|
-
const
|
|
330
|
-
const
|
|
333
|
+
const INSTRUMENTAL_WAVE_PATH_HIGH_FALLBACK = `path("${INSTRUMENTAL_WAVE_PATH_HIGH}")`;
|
|
334
|
+
const INSTRUMENTAL_WAVE_PATH_LOW_FALLBACK = `path("${INSTRUMENTAL_WAVE_PATH_LOW}")`;
|
|
331
335
|
const LINE_SCROLL_INDEX_PROPERTY = "--blyrics-line-scroll-index";
|
|
332
336
|
const LINE_SCROLL_ACTIVE_INDEX_PROPERTY = "--blyrics-line-scroll-active-index";
|
|
333
337
|
const LINE_SCROLL_RELATIVE_INDEX_PROPERTY = "--blyrics-line-scroll-relative-index";
|
|
@@ -609,13 +613,6 @@ function activeTextInstantKeyframes(config) {
|
|
|
609
613
|
},
|
|
610
614
|
];
|
|
611
615
|
}
|
|
612
|
-
function highlightTarget(part) {
|
|
613
|
-
const highlight = part.lyricElement.querySelector(WORD_HIGHLIGHT_SELECTOR);
|
|
614
|
-
if (highlight) {
|
|
615
|
-
return { element: highlight, options: {} };
|
|
616
|
-
}
|
|
617
|
-
return { element: part.lyricElement, options: { pseudoElement: "::after" } };
|
|
618
|
-
}
|
|
619
616
|
function lineSyncedTextKeyframes(config) {
|
|
620
617
|
return [
|
|
621
618
|
{
|
|
@@ -648,33 +645,30 @@ function fadeOutTextKeyframes(config) {
|
|
|
648
645
|
}
|
|
649
646
|
function startRichSyncedHighlightAnimations(engine, part, config, swipeTimeMs, wordTimeMs, swipeDurationMs, glowDurationMs, appliedTimingOffsetMs) {
|
|
650
647
|
const animations = [];
|
|
651
|
-
const
|
|
648
|
+
const highlight = part.highlightElement;
|
|
652
649
|
let swipeAnimation;
|
|
653
650
|
if (config.enabled.highlightSwipe) {
|
|
654
|
-
swipeAnimation = trackLyricAnimationTiming(engine,
|
|
651
|
+
swipeAnimation = trackLyricAnimationTiming(engine, highlight.animate(activeTextGradientKeyframes(config), {
|
|
655
652
|
duration: swipeDurationMs,
|
|
656
653
|
easing: config.highlight.swipeEasing,
|
|
657
654
|
fill: "forwards",
|
|
658
|
-
...target.options,
|
|
659
655
|
}), { appliedTimingOffsetMs, offsetMs: swipeTimeMs - wordTimeMs });
|
|
660
656
|
swipeAnimation.currentTime = correctedAnimationTimeMs(swipeTimeMs, appliedTimingOffsetMs, swipeDurationMs);
|
|
661
657
|
animations.push(swipeAnimation);
|
|
662
658
|
}
|
|
663
|
-
const opacityAnimation = trackLyricAnimationTiming(engine,
|
|
659
|
+
const opacityAnimation = trackLyricAnimationTiming(engine, highlight.animate(config.enabled.highlightSwipe ? activeTextVisibleKeyframes() : activeTextInstantKeyframes(config), {
|
|
664
660
|
duration: 1,
|
|
665
661
|
easing: "linear",
|
|
666
662
|
fill: "forwards",
|
|
667
|
-
...target.options,
|
|
668
663
|
}), { appliedTimingOffsetMs, offsetMs: 0 });
|
|
669
664
|
opacityAnimation.currentTime = correctedAnimationTimeMs(wordTimeMs, appliedTimingOffsetMs, 1);
|
|
670
665
|
animations.push(opacityAnimation);
|
|
671
666
|
let glowAnimation;
|
|
672
667
|
if (config.enabled.highlightGlow) {
|
|
673
|
-
glowAnimation = trackLyricAnimationTiming(engine,
|
|
668
|
+
glowAnimation = trackLyricAnimationTiming(engine, highlight.animate(activeTextGlowKeyframes(config), {
|
|
674
669
|
duration: glowDurationMs,
|
|
675
670
|
easing: config.highlight.glowEasing,
|
|
676
671
|
fill: "forwards",
|
|
677
|
-
...target.options,
|
|
678
672
|
}), { appliedTimingOffsetMs, offsetMs: 0 });
|
|
679
673
|
glowAnimation.currentTime = correctedAnimationTimeMs(wordTimeMs, appliedTimingOffsetMs, glowDurationMs);
|
|
680
674
|
animations.push(glowAnimation);
|
|
@@ -684,22 +678,20 @@ function startRichSyncedHighlightAnimations(engine, part, config, swipeTimeMs, w
|
|
|
684
678
|
function startLineSyncedHighlightAnimations(engine, part, config, wordTimeMs, glowDurationMs, appliedTimingOffsetMs) {
|
|
685
679
|
const animations = [];
|
|
686
680
|
const fadeInDuration = config.enabled.highlightFade ? config.highlight.fadeInDurationMs : 1;
|
|
687
|
-
const
|
|
688
|
-
const opacityAnimation = trackLyricAnimationTiming(engine,
|
|
681
|
+
const highlight = part.highlightElement;
|
|
682
|
+
const opacityAnimation = trackLyricAnimationTiming(engine, highlight.animate(lineSyncedTextKeyframes(config), {
|
|
689
683
|
duration: fadeInDuration,
|
|
690
684
|
easing: config.enabled.highlightFade ? config.highlight.fadeInEasing : "linear",
|
|
691
685
|
fill: "forwards",
|
|
692
|
-
...target.options,
|
|
693
686
|
}), { appliedTimingOffsetMs, offsetMs: 0 });
|
|
694
687
|
opacityAnimation.currentTime = correctedAnimationTimeMs(wordTimeMs, appliedTimingOffsetMs, fadeInDuration);
|
|
695
688
|
animations.push(opacityAnimation);
|
|
696
689
|
let glowAnimation;
|
|
697
690
|
if (config.enabled.highlightGlow) {
|
|
698
|
-
glowAnimation = trackLyricAnimationTiming(engine,
|
|
691
|
+
glowAnimation = trackLyricAnimationTiming(engine, highlight.animate(activeTextGlowKeyframes(config), {
|
|
699
692
|
duration: glowDurationMs,
|
|
700
693
|
easing: config.highlight.glowEasing,
|
|
701
694
|
fill: "forwards",
|
|
702
|
-
...target.options,
|
|
703
695
|
}), { appliedTimingOffsetMs, offsetMs: 0 });
|
|
704
696
|
glowAnimation.currentTime = correctedAnimationTimeMs(wordTimeMs, appliedTimingOffsetMs, glowDurationMs);
|
|
705
697
|
animations.push(glowAnimation);
|
|
@@ -752,8 +744,9 @@ function startWordAnimations(engine, part, config, currentTime, appliedTimingOff
|
|
|
752
744
|
const highlightAnimations = isLineSyncedWord
|
|
753
745
|
? startLineSyncedHighlightAnimations(engine, part, config, wordTimeMs, config.highlight.glowMinDurationMs, appliedTimingOffsetMs)
|
|
754
746
|
: startRichSyncedHighlightAnimations(engine, part, config, swipeTimeMs, wordTimeMs, swipeDurationMs, glowDurationMs, appliedTimingOffsetMs);
|
|
755
|
-
const
|
|
756
|
-
|
|
747
|
+
const wobbleAnimations = [];
|
|
748
|
+
if (config.enabled.wordWobble) {
|
|
749
|
+
const wobbleKeyframes = [
|
|
757
750
|
{ transform: config.word.wobbleFrom },
|
|
758
751
|
{
|
|
759
752
|
transform: config.word.wobblePeak,
|
|
@@ -768,18 +761,25 @@ function startWordAnimations(engine, part, config, currentTime, appliedTimingOff
|
|
|
768
761
|
offset: Math.max(config.word.wobblePeakOffset, config.word.wobbleSettleOffset),
|
|
769
762
|
},
|
|
770
763
|
{ transform: config.word.wobbleTo, easing: config.word.wobbleEndEasing },
|
|
771
|
-
]
|
|
764
|
+
];
|
|
765
|
+
const wobbleOptions = {
|
|
772
766
|
duration: config.word.wobbleDurationMs,
|
|
773
767
|
easing: config.word.wobbleEasing,
|
|
774
768
|
fill: "forwards",
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
769
|
+
};
|
|
770
|
+
const wobbleStartMs = correctedAnimationTimeMs(wordTimeMs, appliedTimingOffsetMs, config.word.wobbleDurationMs);
|
|
771
|
+
// The wobble is a paint transform, so the highlight copy must carry it too or the active
|
|
772
|
+
// sweep drifts off the word.
|
|
773
|
+
for (const wordElement of [part.lyricElement, part.highlightElement]) {
|
|
774
|
+
const animation = trackLyricAnimationTiming(engine, wordElement.animate(wobbleKeyframes, wobbleOptions), {
|
|
775
|
+
appliedTimingOffsetMs,
|
|
776
|
+
offsetMs: 0,
|
|
777
|
+
});
|
|
778
|
+
animation.currentTime = wobbleStartMs;
|
|
779
|
+
wobbleAnimations.push(animation);
|
|
780
|
+
}
|
|
779
781
|
}
|
|
780
|
-
part.animations =
|
|
781
|
-
? [...highlightAnimations.animations, wobbleAnimation]
|
|
782
|
-
: highlightAnimations.animations;
|
|
782
|
+
part.animations = [...highlightAnimations.animations, ...wobbleAnimations];
|
|
783
783
|
}
|
|
784
784
|
function startLineAnimations(engine, lineData, config, currentTime) {
|
|
785
785
|
const appliedTimingOffsetMs = engine.learnedAnimationTimingOffsetMs;
|
|
@@ -795,12 +795,10 @@ function startLineAnimations(engine, lineData, config, currentTime) {
|
|
|
795
795
|
function startWordExitAnimation(part, config) {
|
|
796
796
|
resetPartAnimations(part);
|
|
797
797
|
const fadeDuration = config.enabled.highlightFade ? config.highlight.fadeOutDurationMs : 1;
|
|
798
|
-
const
|
|
799
|
-
const animation = target.element.animate(fadeOutTextKeyframes(config), {
|
|
798
|
+
const animation = part.highlightElement.animate(fadeOutTextKeyframes(config), {
|
|
800
799
|
duration: fadeDuration,
|
|
801
800
|
easing: config.enabled.highlightFade ? config.highlight.fadeOutEasing : "linear",
|
|
802
801
|
fill: "none",
|
|
803
|
-
...target.options,
|
|
804
802
|
});
|
|
805
803
|
part.animations = [animation];
|
|
806
804
|
animation.addEventListener("finish", () => {
|
|
@@ -856,9 +854,9 @@ function startInstrumentalAnimations(engine, lineData, config, currentTime, appl
|
|
|
856
854
|
fill: "both",
|
|
857
855
|
}, { appliedTimingOffsetMs, offsetMs: 0 });
|
|
858
856
|
waveOscillationAnimation = animateInstrumentalChild(engine, lineData, INSTRUMENTAL_WAVE_PATH_SELECTOR, [
|
|
859
|
-
{ d:
|
|
860
|
-
{ d:
|
|
861
|
-
{ d:
|
|
857
|
+
{ d: config.instrumental.wavePathHigh },
|
|
858
|
+
{ d: config.instrumental.wavePathLow, offset: 0.5 },
|
|
859
|
+
{ d: config.instrumental.wavePathHigh },
|
|
862
860
|
], {
|
|
863
861
|
duration: config.instrumental.waveOscillationDurationMs,
|
|
864
862
|
easing: config.instrumental.waveOscillationEasing,
|
|
@@ -1016,6 +1014,8 @@ function readAnimationConfig(engine, lyricsElement) {
|
|
|
1016
1014
|
waveFrom: getCSSValue(engine, lyricsElement, "--blyrics-instrumental-wave-transform-from", "scaleY(1.2)"),
|
|
1017
1015
|
waveTo: getCSSValue(engine, lyricsElement, "--blyrics-instrumental-wave-transform-to", "scaleY(0.0001)"),
|
|
1018
1016
|
waveEasing: getCSSValue(engine, lyricsElement, "--blyrics-instrumental-wave-easing", "ease-in"),
|
|
1017
|
+
wavePathHigh: getCSSValue(engine, lyricsElement, "--blyrics-instrumental-wave-path-high", INSTRUMENTAL_WAVE_PATH_HIGH_FALLBACK),
|
|
1018
|
+
wavePathLow: getCSSValue(engine, lyricsElement, "--blyrics-instrumental-wave-path-low", INSTRUMENTAL_WAVE_PATH_LOW_FALLBACK),
|
|
1019
1019
|
waveOscillationDurationMs: getCSSDurationWithFallback(engine, lyricsElement, "--blyrics-instrumental-wave-oscillation-duration", "1.25s"),
|
|
1020
1020
|
waveOscillationEasing: getCSSValue(engine, lyricsElement, "--blyrics-instrumental-wave-oscillation-easing", "ease-in-out"),
|
|
1021
1021
|
},
|
|
@@ -1560,6 +1560,7 @@ export function tickView(engine, currentTime, options) {
|
|
|
1560
1560
|
}
|
|
1561
1561
|
const tabRendererHeight = engine.cachedTabRendererHeight ?? tabRenderer.getBoundingClientRect().height;
|
|
1562
1562
|
let scrollTop = tabRenderer.scrollTop;
|
|
1563
|
+
const maxScrollTop = Math.max(0, tabRenderer.scrollHeight - tabRenderer.clientHeight);
|
|
1563
1564
|
if (animationConfig.enabled.scroll) {
|
|
1564
1565
|
updateVisibleLyricWillChange(engine, lines, scrollTop, engine.pendingLineScroll?.toScrollTop ?? scrollTop, tabRendererHeight);
|
|
1565
1566
|
}
|
|
@@ -1713,8 +1714,9 @@ export function tickView(engine, currentTime, options) {
|
|
|
1713
1714
|
scrollPos = Math.max(scrollPos, lastActiveLyric.position - tabRendererHeight + lastActiveLyric.height);
|
|
1714
1715
|
// Make sure top of last active lyric is visible.
|
|
1715
1716
|
scrollPos = Math.min(scrollPos, lastActiveLyric.position);
|
|
1716
|
-
//
|
|
1717
|
-
|
|
1717
|
+
// Past either end the browser clamps the write and reports nothing, leaving the view aiming
|
|
1718
|
+
// at a position it never reached and re-aiming once per remaining line.
|
|
1719
|
+
scrollPos = clamp(scrollPos, 0, maxScrollTop);
|
|
1718
1720
|
if (ENABLE_DEBUG_RENDER.getBooleanValue()) {
|
|
1719
1721
|
let transform = engine.window.getComputedStyle(lyricsElement).transform;
|
|
1720
1722
|
const matrix = new engine.window.DOMMatrix(transform);
|
|
@@ -1899,6 +1901,9 @@ function applyScrollPadding(engine) {
|
|
|
1899
1901
|
});
|
|
1900
1902
|
engine.document.documentElement.style.setProperty("--blyrics-padding-top", top + "px");
|
|
1901
1903
|
engine.document.documentElement.style.setProperty("--blyrics-padding-bottom", bottom + "px");
|
|
1904
|
+
// Inline, because a theme's own `.blyrics-container { padding }` is appended after the package's
|
|
1905
|
+
// stylesheet at the same weight and would otherwise take this room away.
|
|
1906
|
+
lyricsElement.style.setProperty("padding-bottom", bottom + "px");
|
|
1902
1907
|
}
|
|
1903
1908
|
/**
|
|
1904
1909
|
* Re-reads the view's layout: the scroll padding first, then the line positions the padding moved.
|
package/dist/inject.d.ts
CHANGED
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, 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, 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, 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, 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";
|
|
@@ -60,11 +60,12 @@ export function deriveSyncType(lyrics) {
|
|
|
60
60
|
return "richsync";
|
|
61
61
|
return lyrics.every(item => item.startTimeMs === 0) ? "none" : "synced";
|
|
62
62
|
}
|
|
63
|
-
function newPartData(part, span) {
|
|
63
|
+
function newPartData(part, span, highlight) {
|
|
64
64
|
return {
|
|
65
65
|
time: part.startTimeMs / 1000,
|
|
66
66
|
duration: part.durationMs / 1000,
|
|
67
67
|
lyricElement: span,
|
|
68
|
+
highlightElement: highlight,
|
|
68
69
|
animations: [],
|
|
69
70
|
};
|
|
70
71
|
}
|
|
@@ -193,67 +194,57 @@ function appendLongWordBreaks(doc, span, text, threshold) {
|
|
|
193
194
|
}
|
|
194
195
|
return true;
|
|
195
196
|
}
|
|
196
|
-
function cloneTextWithBreaks(doc, source) {
|
|
197
|
-
const fragment = doc.createDocumentFragment();
|
|
198
|
-
for (const node of source.childNodes) {
|
|
199
|
-
fragment.appendChild(node.cloneNode(true));
|
|
200
|
-
}
|
|
201
|
-
return fragment;
|
|
202
|
-
}
|
|
203
197
|
function createTimedWordSpan(doc, part, wrapThreshold) {
|
|
204
198
|
const span = doc.createElement("span");
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
highlight.setAttribute("aria-hidden", "true");
|
|
228
|
-
highlight.appendChild(cloneTextWithBreaks(doc, span));
|
|
229
|
-
span.appendChild(highlight);
|
|
199
|
+
const highlight = doc.createElement("span");
|
|
200
|
+
highlight.classList.add(WORD_HIGHLIGHT_CLASS);
|
|
201
|
+
for (const wordElement of [span, highlight]) {
|
|
202
|
+
wordElement.classList.add(WORD_CLASS);
|
|
203
|
+
wordElement.dir = "auto";
|
|
204
|
+
if (part.durationMs === 0) {
|
|
205
|
+
wordElement.classList.add(ZERO_DURATION_ANIMATION_CLASS);
|
|
206
|
+
wordElement.classList.add(LINE_SYNCED_WORD_CLASS);
|
|
207
|
+
}
|
|
208
|
+
if (testRtl(part.words))
|
|
209
|
+
wordElement.classList.add(RTL_CLASS);
|
|
210
|
+
if (part.durationMs > longWordThreshold.getNumberValue())
|
|
211
|
+
wordElement.dataset.longWord = "true";
|
|
212
|
+
if (part.isBackground)
|
|
213
|
+
wordElement.classList.add(BACKGROUND_LYRIC_CLASS);
|
|
214
|
+
if (part.explicit)
|
|
215
|
+
wordElement.classList.add(EXPLICIT_WORD_CLASS);
|
|
216
|
+
appendLongWordBreaks(doc, wordElement, part.words, wrapThreshold);
|
|
217
|
+
wordElement.dataset.time = String(part.startTimeMs / 1000);
|
|
218
|
+
wordElement.dataset.duration = String(part.durationMs / 1000);
|
|
219
|
+
wordElement.dataset.content = part.words;
|
|
220
|
+
wordElement.style.setProperty("--blyrics-duration", part.durationMs + "ms");
|
|
230
221
|
}
|
|
231
|
-
|
|
232
|
-
span.dataset.duration = String(part.durationMs / 1000);
|
|
233
|
-
span.dataset.content = part.words;
|
|
234
|
-
span.style.setProperty("--blyrics-duration", part.durationMs + "ms");
|
|
235
|
-
return span;
|
|
222
|
+
return { span, highlight };
|
|
236
223
|
}
|
|
237
224
|
function createWordGroup(doc, group, lineData) {
|
|
238
225
|
const wrapThreshold = Math.max(1, longWordWrapThreshold.getNumberValue());
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
groupElement
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
groupElement.
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
226
|
+
const lyricGroup = doc.createElement("span");
|
|
227
|
+
const highlightGroup = doc.createElement("span");
|
|
228
|
+
for (const groupElement of [lyricGroup, highlightGroup]) {
|
|
229
|
+
groupElement.classList.add(WORD_GROUP_CLASS);
|
|
230
|
+
groupElement.dir = "auto";
|
|
231
|
+
groupElement.dataset.content = group.text;
|
|
232
|
+
if (group.text.length > wrapThreshold * 2) {
|
|
233
|
+
groupElement.classList.add(LONG_WORD_GROUP_CLASS);
|
|
234
|
+
}
|
|
235
|
+
if (group.isBackground) {
|
|
236
|
+
groupElement.classList.add(BACKGROUND_LYRIC_CLASS);
|
|
237
|
+
}
|
|
248
238
|
}
|
|
249
239
|
for (const token of group.tokens) {
|
|
250
240
|
if (token.kind === "space")
|
|
251
241
|
continue;
|
|
252
|
-
const span = createTimedWordSpan(doc, token.part, wrapThreshold);
|
|
253
|
-
lineData.parts.push(newPartData(token.part, span));
|
|
254
|
-
|
|
242
|
+
const { span, highlight } = createTimedWordSpan(doc, token.part, wrapThreshold);
|
|
243
|
+
lineData.parts.push(newPartData(token.part, span, highlight));
|
|
244
|
+
lyricGroup.appendChild(span);
|
|
245
|
+
highlightGroup.appendChild(highlight);
|
|
255
246
|
}
|
|
256
|
-
return
|
|
247
|
+
return { lyricGroup, highlightGroup };
|
|
257
248
|
}
|
|
258
249
|
function createContentLine(doc, className, text) {
|
|
259
250
|
const line = doc.createElement("div");
|
|
@@ -268,6 +259,12 @@ function createBidiRun(doc, text) {
|
|
|
268
259
|
applyDirection(run, text);
|
|
269
260
|
return run;
|
|
270
261
|
}
|
|
262
|
+
function createHighlightRun(doc, text) {
|
|
263
|
+
const run = createBidiRun(doc, text);
|
|
264
|
+
run.classList.add(HIGHLIGHT_RUN_CLASS);
|
|
265
|
+
run.setAttribute("aria-hidden", "true");
|
|
266
|
+
return run;
|
|
267
|
+
}
|
|
271
268
|
export function createLyricsLine(doc, parts, line, lyricElement, options = { splitBackgroundLine: true }) {
|
|
272
269
|
const lineText = parts.map(part => part.words).join("");
|
|
273
270
|
const mainText = options.splitBackgroundLine
|
|
@@ -282,13 +279,17 @@ export function createLyricsLine(doc, parts, line, lyricElement, options = { spl
|
|
|
282
279
|
.join("");
|
|
283
280
|
const main = createContentLine(doc, LINE_MAIN_CLASS, mainText);
|
|
284
281
|
const mainRun = createBidiRun(doc, mainText);
|
|
282
|
+
const mainHighlightRun = createHighlightRun(doc, mainText);
|
|
285
283
|
const groupedTokens = groupTokensByWord(normalizeParts(parts));
|
|
286
284
|
const backgroundLine = createContentLine(doc, BACKGROUND_LINE_CLASS, backgroundText);
|
|
287
285
|
const backgroundRun = createBidiRun(doc, backgroundText);
|
|
286
|
+
const backgroundHighlightRun = createHighlightRun(doc, backgroundText);
|
|
288
287
|
let hasBackground = false;
|
|
289
288
|
let pendingForegroundSpace = "";
|
|
290
289
|
let pendingBackgroundSpace = "";
|
|
290
|
+
main.appendChild(mainHighlightRun);
|
|
291
291
|
main.appendChild(mainRun);
|
|
292
|
+
backgroundLine.appendChild(backgroundHighlightRun);
|
|
292
293
|
backgroundLine.appendChild(backgroundRun);
|
|
293
294
|
for (const item of groupedTokens) {
|
|
294
295
|
if ("kind" in item) {
|
|
@@ -299,11 +300,15 @@ export function createLyricsLine(doc, parts, line, lyricElement, options = { spl
|
|
|
299
300
|
else {
|
|
300
301
|
const shouldUseBackgroundLine = options.splitBackgroundLine && item.isBackground;
|
|
301
302
|
const target = shouldUseBackgroundLine ? backgroundRun : mainRun;
|
|
303
|
+
const highlightRun = shouldUseBackgroundLine ? backgroundHighlightRun : mainHighlightRun;
|
|
302
304
|
const pendingSpace = shouldUseBackgroundLine ? pendingBackgroundSpace : pendingForegroundSpace;
|
|
303
305
|
if (target.childNodes.length > 0 && pendingSpace.length > 0) {
|
|
304
306
|
target.appendChild(doc.createTextNode(pendingSpace));
|
|
307
|
+
highlightRun.appendChild(doc.createTextNode(pendingSpace));
|
|
305
308
|
}
|
|
306
|
-
|
|
309
|
+
const { lyricGroup, highlightGroup } = createWordGroup(doc, item, line);
|
|
310
|
+
target.appendChild(lyricGroup);
|
|
311
|
+
highlightRun.appendChild(highlightGroup);
|
|
307
312
|
if (shouldUseBackgroundLine) {
|
|
308
313
|
hasBackground = true;
|
|
309
314
|
pendingBackgroundSpace = "";
|
package/dist/instrumental.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/instrumental.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
}
|
package/dist/styles/lyrics.css
CHANGED
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
display: block;
|
|
39
39
|
max-width: 100%;
|
|
40
40
|
overflow-wrap: normal;
|
|
41
|
+
position: relative;
|
|
41
42
|
text-align: inherit;
|
|
42
43
|
unicode-bidi: plaintext;
|
|
43
44
|
white-space: normal;
|
|
@@ -47,18 +48,28 @@
|
|
|
47
48
|
display: block;
|
|
48
49
|
margin-top: 0.1em;
|
|
49
50
|
max-width: 100%;
|
|
51
|
+
position: relative;
|
|
50
52
|
text-align: inherit;
|
|
51
53
|
unicode-bidi: plaintext;
|
|
52
54
|
white-space: normal;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
.blyrics-bidi-run {
|
|
56
|
-
display:
|
|
58
|
+
display: block;
|
|
57
59
|
max-width: 100%;
|
|
58
60
|
unicode-bidi: normal;
|
|
59
61
|
white-space: inherit;
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
.blyrics-highlight-run {
|
|
65
|
+
inset: 0 0 auto;
|
|
66
|
+
pointer-events: none;
|
|
67
|
+
position: absolute;
|
|
68
|
+
user-select: none;
|
|
69
|
+
width: auto;
|
|
70
|
+
z-index: 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
62
73
|
.blyrics-word-group {
|
|
63
74
|
display: inline-block;
|
|
64
75
|
max-width: 100%;
|
|
@@ -116,68 +127,33 @@
|
|
|
116
127
|
initial-value: -0.1;
|
|
117
128
|
}
|
|
118
129
|
|
|
119
|
-
.blyrics--word
|
|
120
|
-
.blyrics-word-highlight {
|
|
130
|
+
.blyrics--word.blyrics-word-highlight {
|
|
121
131
|
color: transparent;
|
|
122
132
|
background-image: linear-gradient(
|
|
123
133
|
90deg,
|
|
124
134
|
var(--blyrics-lyric-active-color)
|
|
125
|
-
calc(
|
|
126
|
-
100% * var(--lyric-transition-amount-start) - 4rem *
|
|
127
|
-
var(--lyric-transition-amount-start) + 2rem
|
|
128
|
-
),
|
|
135
|
+
calc(100% * var(--lyric-transition-amount-start)),
|
|
129
136
|
#00000000
|
|
130
|
-
calc(
|
|
131
|
-
100% * var(--lyric-transition-amount-end) - 4rem *
|
|
132
|
-
var(--lyric-transition-amount-end) + 2rem + 1px
|
|
133
|
-
)
|
|
137
|
+
calc(100% * var(--lyric-transition-amount-end) + 1px)
|
|
134
138
|
);
|
|
135
139
|
background-clip: text;
|
|
136
|
-
box-sizing: content-box;
|
|
137
140
|
filter: drop-shadow(0 0 0 var(--blyrics-glow-color));
|
|
138
|
-
left: -2rem;
|
|
139
141
|
opacity: 0;
|
|
140
|
-
padding: 2rem;
|
|
141
142
|
pointer-events: none;
|
|
142
|
-
position: absolute;
|
|
143
|
-
top: -2rem;
|
|
144
|
-
white-space: inherit;
|
|
145
|
-
width: 100%;
|
|
146
143
|
--lyric-transition-amount-start: var(
|
|
147
144
|
--blyrics-highlight-swipe-start-from,
|
|
148
145
|
-0.2
|
|
149
146
|
);
|
|
150
147
|
--lyric-transition-amount-end: var(--blyrics-highlight-swipe-end-from, -0.1);
|
|
151
148
|
}
|
|
152
|
-
|
|
153
|
-
.blyrics
|
|
154
|
-
content: attr(data-content);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
.blyrics-word-highlight {
|
|
158
|
-
user-select: none;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
.blyrics--word:has(.blyrics-word-highlight)::after {
|
|
162
|
-
content: none;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
.blyrics-rtl.blyrics--word::after,
|
|
166
|
-
.blyrics--word.blyrics-rtl::after,
|
|
167
|
-
.blyrics-rtl .blyrics-word-highlight,
|
|
168
|
-
.blyrics--word.blyrics-rtl .blyrics-word-highlight {
|
|
149
|
+
.blyrics-word-highlight.blyrics-rtl,
|
|
150
|
+
.blyrics-rtl .blyrics-word-highlight {
|
|
169
151
|
background-image: linear-gradient(
|
|
170
152
|
270deg,
|
|
171
153
|
var(--blyrics-lyric-active-color)
|
|
172
|
-
calc(
|
|
173
|
-
100% * var(--lyric-transition-amount-start) - 4rem *
|
|
174
|
-
var(--lyric-transition-amount-start) + 2rem
|
|
175
|
-
),
|
|
154
|
+
calc(100% * var(--lyric-transition-amount-start)),
|
|
176
155
|
#00000000
|
|
177
|
-
calc(
|
|
178
|
-
100% * var(--lyric-transition-amount-end) - 4rem *
|
|
179
|
-
var(--lyric-transition-amount-end) + 2rem + 1px
|
|
180
|
-
)
|
|
156
|
+
calc(100% * var(--lyric-transition-amount-end) + 1px)
|
|
181
157
|
);
|
|
182
158
|
}
|
|
183
159
|
|
|
@@ -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
|
|