@braccato/core 1.2.0 → 1.3.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.
@@ -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.js CHANGED
@@ -302,10 +302,16 @@ function resetLineAnimationState(lineData) {
302
302
  resetLineAnimations(lineData);
303
303
  markLineAnimationsStopped(lineData);
304
304
  }
305
+ function togglePartClass(part, className, force) {
306
+ part.lyricElement.classList.toggle(className, force);
307
+ if ("highlightElement" in part) {
308
+ part.highlightElement.classList.toggle(className, force);
309
+ }
310
+ }
305
311
  function setAnimationsPlayState(lineData, isPlaying) {
306
312
  const children = [lineData, ...lineData.parts];
307
313
  for (const part of children) {
308
- part.lyricElement.classList.toggle(PAUSED_CLASS, !isPlaying);
314
+ togglePartClass(part, PAUSED_CLASS, !isPlaying);
309
315
  for (const animation of part.animations) {
310
316
  if (isPlaying) {
311
317
  animation.play();
@@ -319,11 +325,10 @@ function setAnimationsPlayState(lineData, isPlaying) {
319
325
  function clearLineStateClasses(lineData) {
320
326
  lineData.lyricElement.classList.remove(ANIMATING_CLASS);
321
327
  for (const part of [lineData, ...lineData.parts]) {
322
- part.lyricElement.classList.remove(PAUSED_CLASS);
328
+ togglePartClass(part, PAUSED_CLASS, false);
323
329
  }
324
330
  }
325
331
  const LINE_SYNCED_WORD_CLASS = "blyrics-line-synced-word";
326
- const WORD_HIGHLIGHT_SELECTOR = ".blyrics-word-highlight";
327
332
  const INSTRUMENTAL_FILL_SELECTOR = ".blyrics--instrumental-fill";
328
333
  const INSTRUMENTAL_WAVE_CLIP_SELECTOR = ".blyrics--wave-clip";
329
334
  const INSTRUMENTAL_WAVE_PATH_SELECTOR = ".blyrics--wave-path";
@@ -610,13 +615,6 @@ function activeTextInstantKeyframes(config) {
610
615
  },
611
616
  ];
612
617
  }
613
- function highlightTarget(part) {
614
- const highlight = part.lyricElement.querySelector(WORD_HIGHLIGHT_SELECTOR);
615
- if (highlight) {
616
- return { element: highlight, options: {} };
617
- }
618
- return { element: part.lyricElement, options: { pseudoElement: "::after" } };
619
- }
620
618
  function lineSyncedTextKeyframes(config) {
621
619
  return [
622
620
  {
@@ -649,33 +647,30 @@ function fadeOutTextKeyframes(config) {
649
647
  }
650
648
  function startRichSyncedHighlightAnimations(engine, part, config, swipeTimeMs, wordTimeMs, swipeDurationMs, glowDurationMs, appliedTimingOffsetMs) {
651
649
  const animations = [];
652
- const target = highlightTarget(part);
650
+ const highlight = part.highlightElement;
653
651
  let swipeAnimation;
654
652
  if (config.enabled.highlightSwipe) {
655
- swipeAnimation = trackLyricAnimationTiming(engine, target.element.animate(activeTextGradientKeyframes(config), {
653
+ swipeAnimation = trackLyricAnimationTiming(engine, highlight.animate(activeTextGradientKeyframes(config), {
656
654
  duration: swipeDurationMs,
657
655
  easing: config.highlight.swipeEasing,
658
656
  fill: "forwards",
659
- ...target.options,
660
657
  }), { appliedTimingOffsetMs, offsetMs: swipeTimeMs - wordTimeMs });
661
658
  swipeAnimation.currentTime = correctedAnimationTimeMs(swipeTimeMs, appliedTimingOffsetMs, swipeDurationMs);
662
659
  animations.push(swipeAnimation);
663
660
  }
664
- const opacityAnimation = trackLyricAnimationTiming(engine, target.element.animate(config.enabled.highlightSwipe ? activeTextVisibleKeyframes() : activeTextInstantKeyframes(config), {
661
+ const opacityAnimation = trackLyricAnimationTiming(engine, highlight.animate(config.enabled.highlightSwipe ? activeTextVisibleKeyframes() : activeTextInstantKeyframes(config), {
665
662
  duration: 1,
666
663
  easing: "linear",
667
664
  fill: "forwards",
668
- ...target.options,
669
665
  }), { appliedTimingOffsetMs, offsetMs: 0 });
670
666
  opacityAnimation.currentTime = correctedAnimationTimeMs(wordTimeMs, appliedTimingOffsetMs, 1);
671
667
  animations.push(opacityAnimation);
672
668
  let glowAnimation;
673
669
  if (config.enabled.highlightGlow) {
674
- glowAnimation = trackLyricAnimationTiming(engine, target.element.animate(activeTextGlowKeyframes(config), {
670
+ glowAnimation = trackLyricAnimationTiming(engine, highlight.animate(activeTextGlowKeyframes(config), {
675
671
  duration: glowDurationMs,
676
672
  easing: config.highlight.glowEasing,
677
673
  fill: "forwards",
678
- ...target.options,
679
674
  }), { appliedTimingOffsetMs, offsetMs: 0 });
680
675
  glowAnimation.currentTime = correctedAnimationTimeMs(wordTimeMs, appliedTimingOffsetMs, glowDurationMs);
681
676
  animations.push(glowAnimation);
@@ -685,22 +680,20 @@ function startRichSyncedHighlightAnimations(engine, part, config, swipeTimeMs, w
685
680
  function startLineSyncedHighlightAnimations(engine, part, config, wordTimeMs, glowDurationMs, appliedTimingOffsetMs) {
686
681
  const animations = [];
687
682
  const fadeInDuration = config.enabled.highlightFade ? config.highlight.fadeInDurationMs : 1;
688
- const target = highlightTarget(part);
689
- const opacityAnimation = trackLyricAnimationTiming(engine, target.element.animate(lineSyncedTextKeyframes(config), {
683
+ const highlight = part.highlightElement;
684
+ const opacityAnimation = trackLyricAnimationTiming(engine, highlight.animate(lineSyncedTextKeyframes(config), {
690
685
  duration: fadeInDuration,
691
686
  easing: config.enabled.highlightFade ? config.highlight.fadeInEasing : "linear",
692
687
  fill: "forwards",
693
- ...target.options,
694
688
  }), { appliedTimingOffsetMs, offsetMs: 0 });
695
689
  opacityAnimation.currentTime = correctedAnimationTimeMs(wordTimeMs, appliedTimingOffsetMs, fadeInDuration);
696
690
  animations.push(opacityAnimation);
697
691
  let glowAnimation;
698
692
  if (config.enabled.highlightGlow) {
699
- glowAnimation = trackLyricAnimationTiming(engine, target.element.animate(activeTextGlowKeyframes(config), {
693
+ glowAnimation = trackLyricAnimationTiming(engine, highlight.animate(activeTextGlowKeyframes(config), {
700
694
  duration: glowDurationMs,
701
695
  easing: config.highlight.glowEasing,
702
696
  fill: "forwards",
703
- ...target.options,
704
697
  }), { appliedTimingOffsetMs, offsetMs: 0 });
705
698
  glowAnimation.currentTime = correctedAnimationTimeMs(wordTimeMs, appliedTimingOffsetMs, glowDurationMs);
706
699
  animations.push(glowAnimation);
@@ -753,8 +746,9 @@ function startWordAnimations(engine, part, config, currentTime, appliedTimingOff
753
746
  const highlightAnimations = isLineSyncedWord
754
747
  ? startLineSyncedHighlightAnimations(engine, part, config, wordTimeMs, config.highlight.glowMinDurationMs, appliedTimingOffsetMs)
755
748
  : startRichSyncedHighlightAnimations(engine, part, config, swipeTimeMs, wordTimeMs, swipeDurationMs, glowDurationMs, appliedTimingOffsetMs);
756
- const wobbleAnimation = config.enabled.wordWobble
757
- ? trackLyricAnimationTiming(engine, part.lyricElement.animate([
749
+ const wobbleAnimations = [];
750
+ if (config.enabled.wordWobble) {
751
+ const wobbleKeyframes = [
758
752
  { transform: config.word.wobbleFrom },
759
753
  {
760
754
  transform: config.word.wobblePeak,
@@ -769,18 +763,25 @@ function startWordAnimations(engine, part, config, currentTime, appliedTimingOff
769
763
  offset: Math.max(config.word.wobblePeakOffset, config.word.wobbleSettleOffset),
770
764
  },
771
765
  { transform: config.word.wobbleTo, easing: config.word.wobbleEndEasing },
772
- ], {
766
+ ];
767
+ const wobbleOptions = {
773
768
  duration: config.word.wobbleDurationMs,
774
769
  easing: config.word.wobbleEasing,
775
770
  fill: "forwards",
776
- }), { appliedTimingOffsetMs, offsetMs: 0 })
777
- : null;
778
- if (wobbleAnimation) {
779
- wobbleAnimation.currentTime = correctedAnimationTimeMs(wordTimeMs, appliedTimingOffsetMs, config.word.wobbleDurationMs);
771
+ };
772
+ const wobbleStartMs = correctedAnimationTimeMs(wordTimeMs, appliedTimingOffsetMs, config.word.wobbleDurationMs);
773
+ // The wobble is a paint transform, so the highlight copy must carry it too or the active
774
+ // sweep drifts off the word.
775
+ for (const wordElement of [part.lyricElement, part.highlightElement]) {
776
+ const animation = trackLyricAnimationTiming(engine, wordElement.animate(wobbleKeyframes, wobbleOptions), {
777
+ appliedTimingOffsetMs,
778
+ offsetMs: 0,
779
+ });
780
+ animation.currentTime = wobbleStartMs;
781
+ wobbleAnimations.push(animation);
782
+ }
780
783
  }
781
- part.animations = wobbleAnimation
782
- ? [...highlightAnimations.animations, wobbleAnimation]
783
- : highlightAnimations.animations;
784
+ part.animations = [...highlightAnimations.animations, ...wobbleAnimations];
784
785
  }
785
786
  function startLineAnimations(engine, lineData, config, currentTime) {
786
787
  const appliedTimingOffsetMs = engine.learnedAnimationTimingOffsetMs;
@@ -796,12 +797,10 @@ function startLineAnimations(engine, lineData, config, currentTime) {
796
797
  function startWordExitAnimation(part, config) {
797
798
  resetPartAnimations(part);
798
799
  const fadeDuration = config.enabled.highlightFade ? config.highlight.fadeOutDurationMs : 1;
799
- const target = highlightTarget(part);
800
- const animation = target.element.animate(fadeOutTextKeyframes(config), {
800
+ const animation = part.highlightElement.animate(fadeOutTextKeyframes(config), {
801
801
  duration: fadeDuration,
802
802
  easing: config.enabled.highlightFade ? config.highlight.fadeOutEasing : "linear",
803
803
  fill: "none",
804
- ...target.options,
805
804
  });
806
805
  part.animations = [animation];
807
806
  animation.addEventListener("finish", () => {
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
  */
@@ -15,6 +15,9 @@ export interface PartData {
15
15
  lyricElement: HTMLElement;
16
16
  animations: Animation[];
17
17
  }
18
+ export interface PartData extends AnimationData {
19
+ highlightElement: HTMLElement;
20
+ }
18
21
  export type LineData = {
19
22
  parts: PartData[];
20
23
  isScrolled: boolean;
@@ -25,7 +28,7 @@ export type LineData = {
25
28
  isSelected: boolean;
26
29
  height: number;
27
30
  position: number;
28
- } & PartData;
31
+ } & AnimationData;
29
32
  export declare function newLineData(lyricElement: HTMLElement, startTimeMs: number, durationMs: number): LineData;
30
33
  export declare function applyDirection(element: HTMLElement, text: string): void;
31
34
  export declare function createLyricsLine(doc: Document, parts: LyricPart[], line: LineData, lyricElement: HTMLElement, options?: {
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
- span.classList.add(WORD_CLASS);
206
- span.dir = "auto";
207
- if (part.durationMs === 0) {
208
- span.classList.add(ZERO_DURATION_ANIMATION_CLASS);
209
- span.classList.add(LINE_SYNCED_WORD_CLASS);
210
- }
211
- if (testRtl(part.words)) {
212
- span.classList.add(RTL_CLASS);
213
- }
214
- if (part.durationMs > longWordThreshold.getNumberValue()) {
215
- span.dataset.longWord = "true";
216
- }
217
- if (part.isBackground) {
218
- span.classList.add(BACKGROUND_LYRIC_CLASS);
219
- }
220
- if (part.explicit) {
221
- span.classList.add(EXPLICIT_WORD_CLASS);
222
- }
223
- const hasBreaks = appendLongWordBreaks(doc, span, part.words, wrapThreshold);
224
- if (hasBreaks) {
225
- const highlight = doc.createElement("span");
226
- highlight.classList.add(WORD_HIGHLIGHT_CLASS);
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
- span.dataset.time = String(part.startTimeMs / 1000);
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 groupElement = doc.createElement("span");
240
- groupElement.classList.add(WORD_GROUP_CLASS);
241
- groupElement.dir = "auto";
242
- groupElement.dataset.content = group.text;
243
- if (group.text.length > wrapThreshold * 2) {
244
- groupElement.classList.add(LONG_WORD_GROUP_CLASS);
245
- }
246
- if (group.isBackground) {
247
- groupElement.classList.add(BACKGROUND_LYRIC_CLASS);
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
- groupElement.appendChild(span);
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 groupElement;
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
- target.appendChild(createWordGroup(doc, item, line));
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 = "";
@@ -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: inline;
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::after,
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--word::after {
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@braccato/core",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Synchronized lyrics renderer with word-by-word animations",
5
5
  "type": "module",
6
6
  "license": "MIT",