@uimaxbai/am-lyrics 1.1.7 → 1.2.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 +4 -0
- package/demo.webp +0 -0
- package/dist/src/AmLyrics.d.ts +8 -0
- package/dist/src/AmLyrics.d.ts.map +1 -1
- package/dist/src/GoogleService.d.ts +16 -3
- package/dist/src/GoogleService.d.ts.map +1 -1
- package/dist/src/am-lyrics.js +502 -376
- package/dist/src/am-lyrics.js.map +1 -1
- package/dist/src/react.js +502 -376
- package/dist/src/react.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/AmLyrics.ts +617 -432
- package/src/GoogleService.ts +49 -19
package/src/AmLyrics.ts
CHANGED
|
@@ -2,8 +2,34 @@ import { css, html, LitElement, svg } from 'lit';
|
|
|
2
2
|
import { property, query, state } from 'lit/decorators.js';
|
|
3
3
|
import { GoogleService } from './GoogleService.js';
|
|
4
4
|
|
|
5
|
-
const VERSION = '1.
|
|
5
|
+
const VERSION = '1.2.0';
|
|
6
6
|
const INSTRUMENTAL_THRESHOLD_MS = 7000; // Show dots for gaps >= 7s
|
|
7
|
+
const FETCH_TIMEOUT_MS = 8000; // Timeout for all lyrics fetch requests
|
|
8
|
+
const SEEK_THRESHOLD_MS = 500;
|
|
9
|
+
const PRE_SCROLL_LEAD_MS = 500;
|
|
10
|
+
const SCROLL_ANIMATION_DURATION_MS = 280;
|
|
11
|
+
const SCROLL_DELAY_INCREMENT_MS = 24;
|
|
12
|
+
const GAP_PULSE_DURATION_MS = 4000;
|
|
13
|
+
const GAP_PULSE_CYCLE_MS = GAP_PULSE_DURATION_MS * 2;
|
|
14
|
+
const GAP_EXIT_LEAD_MS = 360;
|
|
15
|
+
const GAP_MIN_SCALE = 0.85;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Fetch with an automatic timeout via AbortSignal.
|
|
19
|
+
* Rejects if the request takes longer than `timeoutMs`.
|
|
20
|
+
*/
|
|
21
|
+
function fetchWithTimeout(
|
|
22
|
+
url: string,
|
|
23
|
+
options: Parameters<typeof fetch>[1] = {},
|
|
24
|
+
timeoutMs = FETCH_TIMEOUT_MS,
|
|
25
|
+
): Promise<Response> {
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
28
|
+
|
|
29
|
+
return fetch(url, { ...options, signal: controller.signal }).finally(() =>
|
|
30
|
+
clearTimeout(timeoutId),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
7
33
|
|
|
8
34
|
const KPOE_SERVERS = [
|
|
9
35
|
'https://lyricsplus.binimum.org',
|
|
@@ -172,7 +198,7 @@ export class AmLyrics extends LitElement {
|
|
|
172
198
|
.lyrics-line.scroll-animate {
|
|
173
199
|
transition: none !important; /* Prevent conflict with scroll animation */
|
|
174
200
|
animation-name: lyrics-scroll;
|
|
175
|
-
animation-duration:
|
|
201
|
+
animation-duration: var(--scroll-duration, 280ms);
|
|
176
202
|
animation-timing-function: cubic-bezier(0.41, 0, 0.12, 0.99);
|
|
177
203
|
animation-fill-mode: both;
|
|
178
204
|
animation-delay: var(--lyrics-line-delay, 0ms);
|
|
@@ -244,19 +270,20 @@ export class AmLyrics extends LitElement {
|
|
|
244
270
|
opacity: 0;
|
|
245
271
|
font-size: var(--lyplus-font-size-subtext);
|
|
246
272
|
transition:
|
|
247
|
-
max-height 0.
|
|
248
|
-
opacity
|
|
249
|
-
padding 0.
|
|
273
|
+
max-height 350ms cubic-bezier(0.33, 1, 0.68, 1),
|
|
274
|
+
opacity 300ms ease-out,
|
|
275
|
+
padding 350ms cubic-bezier(0.33, 1, 0.68, 1);
|
|
250
276
|
margin: 0;
|
|
251
277
|
}
|
|
252
278
|
|
|
253
|
-
.lyrics-line.active .background-vocal-container
|
|
279
|
+
.lyrics-line.active .background-vocal-container,
|
|
280
|
+
.lyrics-line.pre-active .background-vocal-container {
|
|
254
281
|
max-height: 4em;
|
|
255
282
|
opacity: 1;
|
|
256
283
|
transition:
|
|
257
|
-
max-height 0.
|
|
258
|
-
opacity
|
|
259
|
-
padding 0.
|
|
284
|
+
max-height 350ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
285
|
+
opacity 300ms ease-out,
|
|
286
|
+
padding 350ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
260
287
|
will-change: max-height, opacity, padding;
|
|
261
288
|
}
|
|
262
289
|
|
|
@@ -267,6 +294,11 @@ export class AmLyrics extends LitElement {
|
|
|
267
294
|
will-change: transform, opacity;
|
|
268
295
|
}
|
|
269
296
|
|
|
297
|
+
.lyrics-line.pre-active {
|
|
298
|
+
opacity: 1;
|
|
299
|
+
will-change: transform, opacity;
|
|
300
|
+
}
|
|
301
|
+
|
|
270
302
|
.lyrics-line.singer-right {
|
|
271
303
|
text-align: end;
|
|
272
304
|
}
|
|
@@ -346,7 +378,7 @@ export class AmLyrics extends LitElement {
|
|
|
346
378
|
/* Unblur early for pre-active lines */
|
|
347
379
|
.lyrics-container.blur-inactive-enabled .lyrics-line.pre-active {
|
|
348
380
|
filter: blur(0px) !important;
|
|
349
|
-
opacity:
|
|
381
|
+
opacity: 1;
|
|
350
382
|
}
|
|
351
383
|
|
|
352
384
|
/* ==========================================================================
|
|
@@ -584,43 +616,45 @@ export class AmLyrics extends LitElement {
|
|
|
584
616
|
INSTRUMENTAL GAP STYLES
|
|
585
617
|
========================================================================== */
|
|
586
618
|
.lyrics-gap {
|
|
587
|
-
height: 0;
|
|
619
|
+
max-height: 0;
|
|
588
620
|
padding: 0 var(--lyplus-padding-gap);
|
|
589
621
|
overflow: hidden;
|
|
590
622
|
opacity: 0;
|
|
591
623
|
box-sizing: content-box;
|
|
592
624
|
background-clip: unset;
|
|
625
|
+
transform-origin: top;
|
|
593
626
|
transition:
|
|
594
|
-
padding 0.
|
|
595
|
-
height 0.
|
|
596
|
-
opacity
|
|
597
|
-
transform
|
|
627
|
+
padding 220ms cubic-bezier(0.33, 1, 0.68, 1),
|
|
628
|
+
max-height 220ms cubic-bezier(0.33, 1, 0.68, 1),
|
|
629
|
+
opacity 160ms ease-out,
|
|
630
|
+
transform var(--scroll-duration, 280ms) var(--lyrics-line-delay, 0ms);
|
|
598
631
|
}
|
|
599
632
|
|
|
600
633
|
.lyrics-gap.active {
|
|
601
|
-
height: 1.
|
|
634
|
+
max-height: 1.6em;
|
|
602
635
|
padding: var(--lyplus-padding-gap);
|
|
603
636
|
opacity: 1;
|
|
604
637
|
overflow: visible;
|
|
605
638
|
transition:
|
|
606
|
-
padding 0.
|
|
607
|
-
height 0.
|
|
608
|
-
opacity
|
|
609
|
-
transform
|
|
610
|
-
will-change: height, opacity, padding;
|
|
639
|
+
padding 220ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
640
|
+
max-height 220ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
641
|
+
opacity 160ms ease-out,
|
|
642
|
+
transform var(--scroll-duration, 280ms);
|
|
643
|
+
will-change: max-height, opacity, padding;
|
|
611
644
|
}
|
|
612
645
|
|
|
613
|
-
/* Exiting state:
|
|
646
|
+
/* Exiting state: quickly collapse width and height so dots don't distort page, or remove max-height transition */
|
|
614
647
|
.lyrics-gap.gap-exiting {
|
|
615
|
-
height:
|
|
616
|
-
padding: var(--lyplus-padding-gap);
|
|
617
|
-
opacity:
|
|
648
|
+
max-height: 0;
|
|
649
|
+
padding: 0 var(--lyplus-padding-gap);
|
|
650
|
+
opacity: 0;
|
|
618
651
|
overflow: visible;
|
|
619
652
|
transition:
|
|
620
|
-
padding 0.
|
|
621
|
-
height
|
|
622
|
-
|
|
623
|
-
|
|
653
|
+
padding var(--gap-exit-duration, 360ms) cubic-bezier(0.33, 1, 0.68, 1),
|
|
654
|
+
max-height var(--gap-exit-duration, 360ms)
|
|
655
|
+
cubic-bezier(0.33, 1, 0.68, 1),
|
|
656
|
+
opacity 160ms ease-out,
|
|
657
|
+
transform var(--scroll-duration, 280ms);
|
|
624
658
|
}
|
|
625
659
|
|
|
626
660
|
.lyrics-gap .main-vocal-container {
|
|
@@ -629,7 +663,8 @@ export class AmLyrics extends LitElement {
|
|
|
629
663
|
|
|
630
664
|
/* Jump animation plays during exit */
|
|
631
665
|
.lyrics-gap.gap-exiting .main-vocal-container {
|
|
632
|
-
animation: gap-ended
|
|
666
|
+
animation: gap-ended var(--gap-exit-duration, 360ms)
|
|
667
|
+
cubic-bezier(0.33, 1, 0.68, 1) forwards;
|
|
633
668
|
}
|
|
634
669
|
|
|
635
670
|
.lyrics-gap:not(.active):not(.gap-exiting) .main-vocal-container {
|
|
@@ -643,7 +678,9 @@ export class AmLyrics extends LitElement {
|
|
|
643
678
|
}
|
|
644
679
|
|
|
645
680
|
.lyrics-gap.active .main-vocal-container .lyrics-word {
|
|
646
|
-
animation: gap-loop
|
|
681
|
+
animation: gap-loop var(--gap-pulse-duration, 4000ms) ease-in-out infinite
|
|
682
|
+
alternate;
|
|
683
|
+
animation-delay: var(--gap-loop-delay, 0ms);
|
|
647
684
|
will-change: transform;
|
|
648
685
|
}
|
|
649
686
|
|
|
@@ -667,6 +704,18 @@ export class AmLyrics extends LitElement {
|
|
|
667
704
|
color: var(--lyplus-text-primary) !important;
|
|
668
705
|
}
|
|
669
706
|
|
|
707
|
+
.lyrics-line.pre-active .lyrics-syllable.line-synced {
|
|
708
|
+
animation: fade-in-line 0.14s ease-out forwards !important;
|
|
709
|
+
color: var(--lyplus-text-primary) !important;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
.lyrics-line.active .lyrics-syllable.line-synced span.char,
|
|
713
|
+
.lyrics-line.pre-active .lyrics-syllable.line-synced span.char {
|
|
714
|
+
background-image: none !important;
|
|
715
|
+
background-color: var(--lyplus-text-primary) !important;
|
|
716
|
+
transition: background-color 120ms ease-out !important;
|
|
717
|
+
}
|
|
718
|
+
|
|
670
719
|
@keyframes fade-in-line {
|
|
671
720
|
from {
|
|
672
721
|
opacity: 0.5;
|
|
@@ -1080,19 +1129,20 @@ export class AmLyrics extends LitElement {
|
|
|
1080
1129
|
/* Gap dot animations */
|
|
1081
1130
|
@keyframes gap-loop {
|
|
1082
1131
|
from {
|
|
1083
|
-
transform: scale(1.
|
|
1132
|
+
transform: scale(1.12);
|
|
1084
1133
|
}
|
|
1085
1134
|
to {
|
|
1086
|
-
transform: scale(0.85);
|
|
1135
|
+
transform: scale(var(--gap-exit-scale, 0.85));
|
|
1087
1136
|
}
|
|
1088
1137
|
}
|
|
1089
1138
|
|
|
1090
1139
|
@keyframes gap-ended {
|
|
1091
1140
|
0% {
|
|
1092
|
-
transform: translateY(-25%) scale(
|
|
1141
|
+
transform: translateY(-25%) scale(var(--gap-exit-scale, 0.85))
|
|
1142
|
+
translateZ(0);
|
|
1093
1143
|
}
|
|
1094
1144
|
35% {
|
|
1095
|
-
transform: translateY(-
|
|
1145
|
+
transform: translateY(-5%) scale(1.08) translateZ(0);
|
|
1096
1146
|
}
|
|
1097
1147
|
100% {
|
|
1098
1148
|
transform: translateY(-25%) scale(0) translateZ(0);
|
|
@@ -1491,11 +1541,19 @@ export class AmLyrics extends LitElement {
|
|
|
1491
1541
|
|
|
1492
1542
|
private scrollAnimationTimeout?: ReturnType<typeof setTimeout>;
|
|
1493
1543
|
|
|
1544
|
+
// AbortController for cancelling in-flight lyrics fetches
|
|
1545
|
+
private fetchAbortController?: AbortController;
|
|
1546
|
+
|
|
1494
1547
|
// Syllable animation tracking
|
|
1495
1548
|
private lastActiveIndex = 0;
|
|
1496
1549
|
|
|
1497
1550
|
private visibleLineIds: Set<string> = new Set();
|
|
1498
1551
|
|
|
1552
|
+
// Bound handler references for proper event listener removal
|
|
1553
|
+
private _boundHandleUserScroll = this.handleUserScroll.bind(this);
|
|
1554
|
+
|
|
1555
|
+
private _boundAnimateProgress = this.animateProgress.bind(this);
|
|
1556
|
+
|
|
1499
1557
|
connectedCallback() {
|
|
1500
1558
|
super.connectedCallback();
|
|
1501
1559
|
this.fetchLyrics();
|
|
@@ -1505,13 +1563,46 @@ export class AmLyrics extends LitElement {
|
|
|
1505
1563
|
super.disconnectedCallback();
|
|
1506
1564
|
if (this.animationFrameId) {
|
|
1507
1565
|
cancelAnimationFrame(this.animationFrameId);
|
|
1566
|
+
this.animationFrameId = undefined;
|
|
1508
1567
|
}
|
|
1509
1568
|
if (this.userScrollTimeoutId) {
|
|
1510
1569
|
clearTimeout(this.userScrollTimeoutId);
|
|
1570
|
+
this.userScrollTimeoutId = undefined;
|
|
1571
|
+
}
|
|
1572
|
+
if (this.clickSeekTimeout) {
|
|
1573
|
+
clearTimeout(this.clickSeekTimeout);
|
|
1574
|
+
this.clickSeekTimeout = undefined;
|
|
1575
|
+
}
|
|
1576
|
+
if (this.scrollUnlockTimeout) {
|
|
1577
|
+
clearTimeout(this.scrollUnlockTimeout);
|
|
1578
|
+
this.scrollUnlockTimeout = undefined;
|
|
1579
|
+
}
|
|
1580
|
+
if (this.scrollAnimationTimeout) {
|
|
1581
|
+
clearTimeout(this.scrollAnimationTimeout);
|
|
1582
|
+
this.scrollAnimationTimeout = undefined;
|
|
1583
|
+
}
|
|
1584
|
+
// Cancel any in-flight fetch requests
|
|
1585
|
+
this.fetchAbortController?.abort();
|
|
1586
|
+
this.fetchAbortController = undefined;
|
|
1587
|
+
// Remove scroll event listeners
|
|
1588
|
+
if (this.lyricsContainer) {
|
|
1589
|
+
this.lyricsContainer.removeEventListener(
|
|
1590
|
+
'wheel',
|
|
1591
|
+
this._boundHandleUserScroll,
|
|
1592
|
+
);
|
|
1593
|
+
this.lyricsContainer.removeEventListener(
|
|
1594
|
+
'touchmove',
|
|
1595
|
+
this._boundHandleUserScroll,
|
|
1596
|
+
);
|
|
1511
1597
|
}
|
|
1512
1598
|
}
|
|
1513
1599
|
|
|
1514
1600
|
private async fetchLyrics() {
|
|
1601
|
+
// Cancel any in-flight fetch to prevent stale results from racing
|
|
1602
|
+
this.fetchAbortController?.abort();
|
|
1603
|
+
const controller = new AbortController();
|
|
1604
|
+
this.fetchAbortController = controller;
|
|
1605
|
+
|
|
1515
1606
|
this.isLoading = true;
|
|
1516
1607
|
this.lyrics = undefined;
|
|
1517
1608
|
this.lyricsSource = null;
|
|
@@ -1521,6 +1612,8 @@ export class AmLyrics extends LitElement {
|
|
|
1521
1612
|
this.hasFetchedAllProviders = false;
|
|
1522
1613
|
try {
|
|
1523
1614
|
const resolvedMetadata = await this.resolveSongMetadata();
|
|
1615
|
+
// If a newer fetch was triggered while we awaited, bail out
|
|
1616
|
+
if (controller.signal.aborted) return;
|
|
1524
1617
|
|
|
1525
1618
|
const isMusicIdOnlyRequest =
|
|
1526
1619
|
Boolean(this.musicId) &&
|
|
@@ -1605,7 +1698,10 @@ export class AmLyrics extends LitElement {
|
|
|
1605
1698
|
this.lyrics = undefined;
|
|
1606
1699
|
this.lyricsSource = null;
|
|
1607
1700
|
} finally {
|
|
1608
|
-
this
|
|
1701
|
+
// Only update loading state if this fetch wasn't superseded
|
|
1702
|
+
if (!controller.signal.aborted) {
|
|
1703
|
+
this.isLoading = false;
|
|
1704
|
+
}
|
|
1609
1705
|
}
|
|
1610
1706
|
}
|
|
1611
1707
|
|
|
@@ -1932,7 +2028,7 @@ export class AmLyrics extends LitElement {
|
|
|
1932
2028
|
|
|
1933
2029
|
try {
|
|
1934
2030
|
// eslint-disable-next-line no-await-in-loop
|
|
1935
|
-
const response = await
|
|
2031
|
+
const response = await fetchWithTimeout(url);
|
|
1936
2032
|
if (response.ok) {
|
|
1937
2033
|
// eslint-disable-next-line no-await-in-loop
|
|
1938
2034
|
const payload = await response.json();
|
|
@@ -1983,7 +2079,9 @@ export class AmLyrics extends LitElement {
|
|
|
1983
2079
|
);
|
|
1984
2080
|
}
|
|
1985
2081
|
|
|
1986
|
-
|
|
2082
|
+
if (!DEFAULT_KPOE_SOURCE_ORDER.includes('apple')) {
|
|
2083
|
+
params.append('source', DEFAULT_KPOE_SOURCE_ORDER);
|
|
2084
|
+
}
|
|
1987
2085
|
|
|
1988
2086
|
const getRank = (sourceLabel: string, parsedLines: any[]): number => {
|
|
1989
2087
|
const lower = sourceLabel.toLowerCase();
|
|
@@ -2036,13 +2134,13 @@ export class AmLyrics extends LitElement {
|
|
|
2036
2134
|
}
|
|
2037
2135
|
|
|
2038
2136
|
const cacheUrl = `https://lyrics-api.binimum.org/?${cacheParams.toString()}`;
|
|
2039
|
-
const cacheRes = await
|
|
2137
|
+
const cacheRes = await fetchWithTimeout(cacheUrl);
|
|
2040
2138
|
if (cacheRes.ok) {
|
|
2041
2139
|
const cacheData = await cacheRes.json();
|
|
2042
2140
|
if (cacheData.results && cacheData.results.length > 0) {
|
|
2043
2141
|
const result = cacheData.results[0];
|
|
2044
2142
|
if (result.timing_type === 'word' && result.lyricsUrl) {
|
|
2045
|
-
const ttmlRes = await
|
|
2143
|
+
const ttmlRes = await fetchWithTimeout(result.lyricsUrl);
|
|
2046
2144
|
if (ttmlRes.ok) {
|
|
2047
2145
|
const ttmlText = await ttmlRes.text();
|
|
2048
2146
|
const lines = AmLyrics.parseTTML(ttmlText);
|
|
@@ -2056,7 +2154,7 @@ export class AmLyrics extends LitElement {
|
|
|
2056
2154
|
const fallbackParams = new URLSearchParams(params);
|
|
2057
2155
|
const fallbackUrl = `https://lyricsplus.binimum.org/v2/lyrics/get?${fallbackParams.toString()}`;
|
|
2058
2156
|
try {
|
|
2059
|
-
const fallbackRes = await
|
|
2157
|
+
const fallbackRes = await fetchWithTimeout(fallbackUrl);
|
|
2060
2158
|
if (fallbackRes.ok) {
|
|
2061
2159
|
const payload = await fallbackRes.json();
|
|
2062
2160
|
const lines = AmLyrics.convertKPoeLyrics(payload);
|
|
@@ -2081,7 +2179,7 @@ export class AmLyrics extends LitElement {
|
|
|
2081
2179
|
|
|
2082
2180
|
// If fallback fails or has no word sync, fall back to bini lyrics
|
|
2083
2181
|
if (result.lyricsUrl) {
|
|
2084
|
-
const ttmlRes = await
|
|
2182
|
+
const ttmlRes = await fetchWithTimeout(result.lyricsUrl);
|
|
2085
2183
|
if (ttmlRes.ok) {
|
|
2086
2184
|
const ttmlText = await ttmlRes.text();
|
|
2087
2185
|
const lines = AmLyrics.parseTTML(ttmlText);
|
|
@@ -2103,10 +2201,10 @@ export class AmLyrics extends LitElement {
|
|
|
2103
2201
|
}
|
|
2104
2202
|
|
|
2105
2203
|
// Shuffle servers so we pick a random one first, with all others as fallback
|
|
2106
|
-
//
|
|
2204
|
+
// Try up to 3 servers to improve reliability when some have CORS or connectivity issues
|
|
2107
2205
|
const shuffledServers = [...KPOE_SERVERS]
|
|
2108
2206
|
.sort(() => Math.random() - 0.5)
|
|
2109
|
-
.slice(0,
|
|
2207
|
+
.slice(0, 3);
|
|
2110
2208
|
|
|
2111
2209
|
for (const base of shuffledServers) {
|
|
2112
2210
|
const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base;
|
|
@@ -2116,12 +2214,12 @@ export class AmLyrics extends LitElement {
|
|
|
2116
2214
|
|
|
2117
2215
|
try {
|
|
2118
2216
|
// eslint-disable-next-line no-await-in-loop
|
|
2119
|
-
const response = await
|
|
2217
|
+
const response = await fetchWithTimeout(url);
|
|
2120
2218
|
if (response.ok) {
|
|
2121
2219
|
// eslint-disable-next-line no-await-in-loop
|
|
2122
2220
|
payload = await response.json();
|
|
2123
2221
|
}
|
|
2124
|
-
} catch
|
|
2222
|
+
} catch {
|
|
2125
2223
|
payload = null;
|
|
2126
2224
|
}
|
|
2127
2225
|
|
|
@@ -2156,7 +2254,7 @@ export class AmLyrics extends LitElement {
|
|
|
2156
2254
|
try {
|
|
2157
2255
|
const fallbackParams = new URLSearchParams(params);
|
|
2158
2256
|
const url = `https://lyricsplus.binimum.org/v2/lyrics/get?${fallbackParams.toString()}`;
|
|
2159
|
-
const response = await
|
|
2257
|
+
const response = await fetchWithTimeout(url);
|
|
2160
2258
|
if (response.ok) {
|
|
2161
2259
|
const payload = await response.json();
|
|
2162
2260
|
if (payload) {
|
|
@@ -2198,7 +2296,6 @@ export class AmLyrics extends LitElement {
|
|
|
2198
2296
|
if (!match) {
|
|
2199
2297
|
// Skip non-timestamped lines (headers like [ti:], [ar:], etc.)
|
|
2200
2298
|
// eslint-disable-next-line no-continue
|
|
2201
|
-
// eslint-disable-next-line no-continue
|
|
2202
2299
|
continue;
|
|
2203
2300
|
}
|
|
2204
2301
|
const minutes = parseInt(match[1], 10);
|
|
@@ -2221,7 +2318,6 @@ export class AmLyrics extends LitElement {
|
|
|
2221
2318
|
|
|
2222
2319
|
// Skip empty lines (instrumental gaps)
|
|
2223
2320
|
if (!text.trim()) {
|
|
2224
|
-
// eslint-disable-next-line no-continue
|
|
2225
2321
|
// eslint-disable-next-line no-continue
|
|
2226
2322
|
continue;
|
|
2227
2323
|
}
|
|
@@ -2261,9 +2357,9 @@ export class AmLyrics extends LitElement {
|
|
|
2261
2357
|
|
|
2262
2358
|
if (!title || !artist) return null;
|
|
2263
2359
|
|
|
2264
|
-
// Pick
|
|
2360
|
+
// Pick 3 random unique servers for better reliability
|
|
2265
2361
|
const shuffled = [...TIDAL_SERVERS].sort(() => Math.random() - 0.5);
|
|
2266
|
-
const serversToTry = shuffled.slice(0,
|
|
2362
|
+
const serversToTry = shuffled.slice(0, 3);
|
|
2267
2363
|
|
|
2268
2364
|
for (const base of serversToTry) {
|
|
2269
2365
|
try {
|
|
@@ -2273,12 +2369,11 @@ export class AmLyrics extends LitElement {
|
|
|
2273
2369
|
const searchQuery = `${title} ${artist}`;
|
|
2274
2370
|
const searchParams = new URLSearchParams({ s: searchQuery });
|
|
2275
2371
|
// eslint-disable-next-line no-await-in-loop
|
|
2276
|
-
const searchResponse = await
|
|
2372
|
+
const searchResponse = await fetchWithTimeout(
|
|
2277
2373
|
`${normalizedBase}/search/?${searchParams.toString()}`,
|
|
2278
2374
|
);
|
|
2279
2375
|
|
|
2280
2376
|
if (!searchResponse.ok) {
|
|
2281
|
-
// eslint-disable-next-line no-continue
|
|
2282
2377
|
// eslint-disable-next-line no-continue
|
|
2283
2378
|
continue;
|
|
2284
2379
|
}
|
|
@@ -2288,7 +2383,6 @@ export class AmLyrics extends LitElement {
|
|
|
2288
2383
|
const items = searchData?.data?.items;
|
|
2289
2384
|
|
|
2290
2385
|
if (!Array.isArray(items) || items.length === 0) {
|
|
2291
|
-
// eslint-disable-next-line no-continue
|
|
2292
2386
|
// eslint-disable-next-line no-continue
|
|
2293
2387
|
continue;
|
|
2294
2388
|
}
|
|
@@ -2307,19 +2401,17 @@ export class AmLyrics extends LitElement {
|
|
|
2307
2401
|
|
|
2308
2402
|
const trackId = bestTrack?.id;
|
|
2309
2403
|
if (!trackId) {
|
|
2310
|
-
// eslint-disable-next-line no-continue
|
|
2311
2404
|
// eslint-disable-next-line no-continue
|
|
2312
2405
|
continue;
|
|
2313
2406
|
}
|
|
2314
2407
|
|
|
2315
2408
|
// Step 2: Fetch lyrics
|
|
2316
2409
|
// eslint-disable-next-line no-await-in-loop
|
|
2317
|
-
const lyricsResponse = await
|
|
2410
|
+
const lyricsResponse = await fetchWithTimeout(
|
|
2318
2411
|
`${normalizedBase}/lyrics/?id=${trackId}`,
|
|
2319
2412
|
);
|
|
2320
2413
|
|
|
2321
2414
|
if (!lyricsResponse.ok) {
|
|
2322
|
-
// eslint-disable-next-line no-continue
|
|
2323
2415
|
// eslint-disable-next-line no-continue
|
|
2324
2416
|
continue;
|
|
2325
2417
|
}
|
|
@@ -2361,7 +2453,7 @@ export class AmLyrics extends LitElement {
|
|
|
2361
2453
|
try {
|
|
2362
2454
|
const searchQuery = `${artist} ${title}`;
|
|
2363
2455
|
const params = new URLSearchParams({ q: searchQuery });
|
|
2364
|
-
const response = await
|
|
2456
|
+
const response = await fetchWithTimeout(
|
|
2365
2457
|
`https://lrclib.net/api/search?${params.toString()}`,
|
|
2366
2458
|
{
|
|
2367
2459
|
headers: {
|
|
@@ -2433,7 +2525,9 @@ export class AmLyrics extends LitElement {
|
|
|
2433
2525
|
|
|
2434
2526
|
try {
|
|
2435
2527
|
const params = new URLSearchParams({ title, artist });
|
|
2436
|
-
const response = await
|
|
2528
|
+
const response = await fetchWithTimeout(
|
|
2529
|
+
`${GENIUS_WORKER_URL}?${params.toString()}`,
|
|
2530
|
+
);
|
|
2437
2531
|
|
|
2438
2532
|
if (!response.ok) return null;
|
|
2439
2533
|
const data = await response.json();
|
|
@@ -2467,8 +2561,7 @@ export class AmLyrics extends LitElement {
|
|
|
2467
2561
|
}
|
|
2468
2562
|
}
|
|
2469
2563
|
} catch {
|
|
2470
|
-
//
|
|
2471
|
-
console.error('No Genius lyrics found');
|
|
2564
|
+
// Genius fetch failed, will fall through to return null
|
|
2472
2565
|
}
|
|
2473
2566
|
|
|
2474
2567
|
return null;
|
|
@@ -2546,7 +2639,7 @@ export class AmLyrics extends LitElement {
|
|
|
2546
2639
|
const doc = parser.parseFromString(ttmlString, 'text/xml');
|
|
2547
2640
|
|
|
2548
2641
|
const translations: Record<string, string> = {};
|
|
2549
|
-
const transliterations: Record<string,
|
|
2642
|
+
const transliterations: Record<string, any> = {};
|
|
2550
2643
|
const agentMap: Record<string, string> = {};
|
|
2551
2644
|
|
|
2552
2645
|
const agents = doc.getElementsByTagName('ttm:agent');
|
|
@@ -2571,20 +2664,6 @@ export class AmLyrics extends LitElement {
|
|
|
2571
2664
|
}
|
|
2572
2665
|
}
|
|
2573
2666
|
|
|
2574
|
-
const transliterationNodes = doc.getElementsByTagName('transliteration');
|
|
2575
|
-
for (let i = 0; i < transliterationNodes.length; i += 1) {
|
|
2576
|
-
const texts = transliterationNodes[i].getElementsByTagName('text');
|
|
2577
|
-
for (let j = 0; j < texts.length; j += 1) {
|
|
2578
|
-
const textNode = texts[j];
|
|
2579
|
-
const key = textNode.getAttribute('for');
|
|
2580
|
-
if (key && textNode.textContent) {
|
|
2581
|
-
transliterations[key] = textNode.textContent
|
|
2582
|
-
.trim()
|
|
2583
|
-
.replace(/\s+/g, ' ');
|
|
2584
|
-
}
|
|
2585
|
-
}
|
|
2586
|
-
}
|
|
2587
|
-
|
|
2588
2667
|
const timeToMs = (timeStr: string | null): number => {
|
|
2589
2668
|
if (!timeStr) return 0;
|
|
2590
2669
|
const parts = timeStr.split(':');
|
|
@@ -2602,6 +2681,59 @@ export class AmLyrics extends LitElement {
|
|
|
2602
2681
|
return Math.round(seconds * 1000);
|
|
2603
2682
|
};
|
|
2604
2683
|
|
|
2684
|
+
const transliterationNodes = doc.getElementsByTagName('transliteration');
|
|
2685
|
+
for (let i = 0; i < transliterationNodes.length; i += 1) {
|
|
2686
|
+
const texts = transliterationNodes[i].getElementsByTagName('text');
|
|
2687
|
+
for (let j = 0; j < texts.length; j += 1) {
|
|
2688
|
+
const textNode = texts[j];
|
|
2689
|
+
const key = textNode.getAttribute('for');
|
|
2690
|
+
if (!key) {
|
|
2691
|
+
// eslint-disable-next-line no-continue
|
|
2692
|
+
continue;
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
const spans = Array.from(
|
|
2696
|
+
textNode.getElementsByTagName('span'),
|
|
2697
|
+
).filter(span => span.getAttribute('begin'));
|
|
2698
|
+
|
|
2699
|
+
if (spans.length > 0) {
|
|
2700
|
+
const syllabus: any[] = [];
|
|
2701
|
+
let fullText = '';
|
|
2702
|
+
for (let k = 0; k < spans.length; k += 1) {
|
|
2703
|
+
const span = spans[k];
|
|
2704
|
+
const begin = span.getAttribute('begin');
|
|
2705
|
+
const end = span.getAttribute('end');
|
|
2706
|
+
let spanText = span.textContent || '';
|
|
2707
|
+
const nextNode = span.nextSibling;
|
|
2708
|
+
if (
|
|
2709
|
+
nextNode &&
|
|
2710
|
+
nextNode.nodeType === 3 &&
|
|
2711
|
+
/^\s/.test(nextNode.textContent || '') &&
|
|
2712
|
+
!spanText.endsWith(' ')
|
|
2713
|
+
) {
|
|
2714
|
+
spanText += ' ';
|
|
2715
|
+
}
|
|
2716
|
+
if (spanText.trim() === '') {
|
|
2717
|
+
// eslint-disable-next-line no-continue
|
|
2718
|
+
continue;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
syllabus.push({
|
|
2722
|
+
time: timeToMs(begin),
|
|
2723
|
+
duration: timeToMs(end) - timeToMs(begin),
|
|
2724
|
+
text: spanText,
|
|
2725
|
+
});
|
|
2726
|
+
fullText += spanText;
|
|
2727
|
+
}
|
|
2728
|
+
transliterations[key] = { text: fullText.trim(), syllabus };
|
|
2729
|
+
} else if (textNode.textContent) {
|
|
2730
|
+
transliterations[key] = {
|
|
2731
|
+
text: textNode.textContent.trim().replace(/\s+/g, ' '),
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2605
2737
|
const lines: LyricsLine[] = [];
|
|
2606
2738
|
const pNodes = doc.getElementsByTagName('p');
|
|
2607
2739
|
|
|
@@ -2697,6 +2829,74 @@ export class AmLyrics extends LitElement {
|
|
|
2697
2829
|
|
|
2698
2830
|
const alignment = alignments[i];
|
|
2699
2831
|
|
|
2832
|
+
// Distribute line-level transliteration to individual syllables
|
|
2833
|
+
// so that per-syllable animated romanisation works (like KPoe lyrics)
|
|
2834
|
+
const lineTransliterationItem = key ? transliterations[key] : undefined;
|
|
2835
|
+
if (
|
|
2836
|
+
lineTransliterationItem &&
|
|
2837
|
+
mainSyllables.length > 1 &&
|
|
2838
|
+
spans.length > 0
|
|
2839
|
+
) {
|
|
2840
|
+
if (
|
|
2841
|
+
lineTransliterationItem.syllabus &&
|
|
2842
|
+
lineTransliterationItem.syllabus.length === mainSyllables.length
|
|
2843
|
+
) {
|
|
2844
|
+
mainSyllables.forEach((syl, mapIdx) => {
|
|
2845
|
+
// eslint-disable-next-line no-param-reassign
|
|
2846
|
+
syl.romanizedText = lineTransliterationItem.syllabus[mapIdx].text;
|
|
2847
|
+
});
|
|
2848
|
+
} else {
|
|
2849
|
+
const lineTransliteration = lineTransliterationItem.text;
|
|
2850
|
+
const romanWords = lineTransliteration.split(/\s+/).filter(Boolean);
|
|
2851
|
+
|
|
2852
|
+
const syllableGroups: number[][] = [];
|
|
2853
|
+
for (let si = 0; si < mainSyllables.length; si += 1) {
|
|
2854
|
+
if (mainSyllables[si].part && syllableGroups.length > 0) {
|
|
2855
|
+
syllableGroups[syllableGroups.length - 1].push(si);
|
|
2856
|
+
} else {
|
|
2857
|
+
syllableGroups.push([si]);
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
const isCJK =
|
|
2862
|
+
/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(
|
|
2863
|
+
mainSyllables.map(s => s.text).join(''),
|
|
2864
|
+
);
|
|
2865
|
+
|
|
2866
|
+
if (romanWords.length === syllableGroups.length) {
|
|
2867
|
+
syllableGroups.forEach((group, gi) => {
|
|
2868
|
+
// eslint-disable-next-line no-param-reassign
|
|
2869
|
+
mainSyllables[group[0]].romanizedText = romanWords[gi];
|
|
2870
|
+
});
|
|
2871
|
+
} else if (romanWords.length === mainSyllables.length) {
|
|
2872
|
+
mainSyllables.forEach((syl, mapIdx) => {
|
|
2873
|
+
// eslint-disable-next-line no-param-reassign
|
|
2874
|
+
syl.romanizedText = romanWords[mapIdx];
|
|
2875
|
+
});
|
|
2876
|
+
} else if (isCJK) {
|
|
2877
|
+
let romanIdx = 0;
|
|
2878
|
+
for (const group of syllableGroups) {
|
|
2879
|
+
const syl = mainSyllables[group[0]];
|
|
2880
|
+
const sylText = group
|
|
2881
|
+
.map(gIndex => mainSyllables[gIndex].text)
|
|
2882
|
+
.join('');
|
|
2883
|
+
const validChars =
|
|
2884
|
+
sylText.match(
|
|
2885
|
+
/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7afA-Za-z0-9]/g,
|
|
2886
|
+
) || [];
|
|
2887
|
+
const needed = validChars.length;
|
|
2888
|
+
if (needed > 0 && romanIdx < romanWords.length) {
|
|
2889
|
+
// eslint-disable-next-line no-param-reassign
|
|
2890
|
+
syl.romanizedText = romanWords
|
|
2891
|
+
.slice(romanIdx, romanIdx + needed)
|
|
2892
|
+
.join(' ');
|
|
2893
|
+
romanIdx += needed;
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2700
2900
|
lines.push({
|
|
2701
2901
|
text: mainSyllables,
|
|
2702
2902
|
background: bgSyllables.length > 0,
|
|
@@ -2707,7 +2907,7 @@ export class AmLyrics extends LitElement {
|
|
|
2707
2907
|
alignment,
|
|
2708
2908
|
songPart,
|
|
2709
2909
|
translation: key ? translations[key] : undefined,
|
|
2710
|
-
romanizedText:
|
|
2910
|
+
romanizedText: lineTransliterationItem?.text,
|
|
2711
2911
|
oppositeTurn: alignment === 'end',
|
|
2712
2912
|
});
|
|
2713
2913
|
}
|
|
@@ -2889,12 +3089,12 @@ export class AmLyrics extends LitElement {
|
|
|
2889
3089
|
if (this.lyricsContainer) {
|
|
2890
3090
|
this.lyricsContainer.addEventListener(
|
|
2891
3091
|
'wheel',
|
|
2892
|
-
this.
|
|
3092
|
+
this._boundHandleUserScroll,
|
|
2893
3093
|
{ passive: true },
|
|
2894
3094
|
);
|
|
2895
3095
|
this.lyricsContainer.addEventListener(
|
|
2896
3096
|
'touchmove',
|
|
2897
|
-
this.
|
|
3097
|
+
this._boundHandleUserScroll,
|
|
2898
3098
|
{ passive: true },
|
|
2899
3099
|
);
|
|
2900
3100
|
}
|
|
@@ -2908,15 +3108,15 @@ export class AmLyrics extends LitElement {
|
|
|
2908
3108
|
*/
|
|
2909
3109
|
private _onTimeChanged(oldTime: number, newTime: number): void {
|
|
2910
3110
|
const timeDiff = Math.abs(newTime - oldTime);
|
|
3111
|
+
const isSeek = timeDiff > SEEK_THRESHOLD_MS;
|
|
2911
3112
|
|
|
2912
3113
|
const newActiveLines = this.findActiveLineIndices(newTime);
|
|
2913
3114
|
const oldActiveLines = this.activeLineIndices;
|
|
2914
3115
|
|
|
2915
3116
|
// Reset animation if active lines change or if we skip time.
|
|
2916
|
-
// A threshold of 0.5s (500ms) is used to detect a "skip".
|
|
2917
3117
|
const linesChanged = !AmLyrics.arraysEqual(newActiveLines, oldActiveLines);
|
|
2918
3118
|
|
|
2919
|
-
if (linesChanged ||
|
|
3119
|
+
if (linesChanged || isSeek) {
|
|
2920
3120
|
// Imperatively manage 'active' class so that scroll-animate and other
|
|
2921
3121
|
// imperative classes are never clobbered.
|
|
2922
3122
|
if (this.lyricsContainer) {
|
|
@@ -2944,25 +3144,16 @@ export class AmLyrics extends LitElement {
|
|
|
2944
3144
|
}
|
|
2945
3145
|
}
|
|
2946
3146
|
}
|
|
2947
|
-
}
|
|
2948
|
-
this.startAnimationFromTime(newTime);
|
|
2949
|
-
|
|
2950
|
-
// Update position classes BEFORE scrolling so currentPrimaryActiveLine is current
|
|
2951
|
-
if (this.lyricsContainer && this.activeLineIndices.length > 0) {
|
|
2952
|
-
const primaryLineIndex = this.activeLineIndices[0];
|
|
2953
|
-
const primaryLine = this.lyricsContainer.querySelector(
|
|
2954
|
-
`#lyrics-line-${primaryLineIndex}`,
|
|
2955
|
-
) as HTMLElement;
|
|
2956
3147
|
|
|
2957
|
-
if (
|
|
2958
|
-
this.
|
|
2959
|
-
this.currentPrimaryActiveLine = primaryLine;
|
|
2960
|
-
this.updatePositionClasses(primaryLine);
|
|
3148
|
+
if (newActiveLines.length > 0) {
|
|
3149
|
+
this.clearPreActiveClasses();
|
|
2961
3150
|
}
|
|
2962
3151
|
}
|
|
2963
3152
|
|
|
3153
|
+
this.startAnimationFromTime(newTime);
|
|
3154
|
+
|
|
2964
3155
|
// Trigger scroll imperatively (was previously in updated() via @state)
|
|
2965
|
-
this._handleActiveLineScroll(oldActiveLines);
|
|
3156
|
+
this._handleActiveLineScroll(oldActiveLines, isSeek);
|
|
2966
3157
|
}
|
|
2967
3158
|
|
|
2968
3159
|
// YouLyPlus-style syllable animation updates
|
|
@@ -2995,7 +3186,7 @@ export class AmLyrics extends LitElement {
|
|
|
2995
3186
|
const isActive = gap.classList.contains('active');
|
|
2996
3187
|
const isExiting = gap.classList.contains('gap-exiting');
|
|
2997
3188
|
// Start exit animation early so it completes before the next lyric
|
|
2998
|
-
const exitLeadMs =
|
|
3189
|
+
const exitLeadMs = GAP_EXIT_LEAD_MS;
|
|
2999
3190
|
const shouldStartExiting =
|
|
3000
3191
|
isActive && !isExiting && newTime >= gapEndTime - exitLeadMs;
|
|
3001
3192
|
|
|
@@ -3023,16 +3214,6 @@ export class AmLyrics extends LitElement {
|
|
|
3023
3214
|
AmLyrics.updateSyllableAnimation(dot as HTMLElement);
|
|
3024
3215
|
}
|
|
3025
3216
|
});
|
|
3026
|
-
|
|
3027
|
-
// Scroll to the gap element so dots animate in with
|
|
3028
|
-
// the staggered scroll rather than popping in.
|
|
3029
|
-
if (
|
|
3030
|
-
this.autoScroll &&
|
|
3031
|
-
!this.isUserScrolling &&
|
|
3032
|
-
!this.isClickSeeking
|
|
3033
|
-
) {
|
|
3034
|
-
this.scrollToActiveLineYouLy(gap as HTMLElement);
|
|
3035
|
-
}
|
|
3036
3217
|
} else if (shouldStartExiting) {
|
|
3037
3218
|
// Exiting gap: keep visible while dots animate out
|
|
3038
3219
|
gap.classList.add('gap-exiting');
|
|
@@ -3040,7 +3221,7 @@ export class AmLyrics extends LitElement {
|
|
|
3040
3221
|
// After exit animation completes, remove gap-exiting to collapse
|
|
3041
3222
|
setTimeout(() => {
|
|
3042
3223
|
gap.classList.remove('gap-exiting');
|
|
3043
|
-
},
|
|
3224
|
+
}, GAP_EXIT_LEAD_MS);
|
|
3044
3225
|
} else if (isActive && !shouldBeActive) {
|
|
3045
3226
|
// NEW: Immediate cleanup if we seeked out of valid range
|
|
3046
3227
|
gap.classList.remove('active');
|
|
@@ -3059,22 +3240,6 @@ export class AmLyrics extends LitElement {
|
|
|
3059
3240
|
this.lastInstrumentalIndex = null;
|
|
3060
3241
|
}
|
|
3061
3242
|
|
|
3062
|
-
// Update position classes for YouLyPlus blur/opacity effect
|
|
3063
|
-
// (only needed when lines didn't change — when they DID change,
|
|
3064
|
-
// position classes are already updated above before scrolling)
|
|
3065
|
-
if (!linesChanged && this.activeLineIndices.length > 0) {
|
|
3066
|
-
const primaryLineIndex = this.activeLineIndices[0];
|
|
3067
|
-
const primaryLine = this.lyricsContainer.querySelector(
|
|
3068
|
-
`#lyrics-line-${primaryLineIndex}`,
|
|
3069
|
-
) as HTMLElement;
|
|
3070
|
-
|
|
3071
|
-
if (primaryLine && primaryLine !== this.currentPrimaryActiveLine) {
|
|
3072
|
-
this.lastPrimaryActiveLine = this.currentPrimaryActiveLine;
|
|
3073
|
-
this.currentPrimaryActiveLine = primaryLine;
|
|
3074
|
-
this.updatePositionClasses(primaryLine);
|
|
3075
|
-
}
|
|
3076
|
-
}
|
|
3077
|
-
|
|
3078
3243
|
// Pre-scroll: scroll to upcoming line ~0.5s before it starts
|
|
3079
3244
|
if (
|
|
3080
3245
|
this.autoScroll &&
|
|
@@ -3082,11 +3247,11 @@ export class AmLyrics extends LitElement {
|
|
|
3082
3247
|
!this.isClickSeeking &&
|
|
3083
3248
|
this.lyrics
|
|
3084
3249
|
) {
|
|
3085
|
-
const preScrollLeadMs = 500; // 500ms lead time
|
|
3086
|
-
|
|
3087
3250
|
// Condition: ONLY pre-scroll if no other lyric is currently playing.
|
|
3088
3251
|
// If a lyric is playing, we must wait for it to finish (handled by updated()).
|
|
3089
3252
|
if (this.activeLineIndices.length === 0) {
|
|
3253
|
+
let preActiveLineIndex: number | null = null;
|
|
3254
|
+
|
|
3090
3255
|
for (let i = 0; i < this.lyrics.length; i += 1) {
|
|
3091
3256
|
const line = this.lyrics[i];
|
|
3092
3257
|
const timeUntilStart = line.timestamp - newTime;
|
|
@@ -3095,23 +3260,20 @@ export class AmLyrics extends LitElement {
|
|
|
3095
3260
|
`#lyrics-line-${i}`,
|
|
3096
3261
|
) as HTMLElement;
|
|
3097
3262
|
|
|
3098
|
-
if (timeUntilStart > 0 && timeUntilStart <=
|
|
3263
|
+
if (timeUntilStart > 0 && timeUntilStart <= PRE_SCROLL_LEAD_MS) {
|
|
3099
3264
|
// Time to pre-scroll and pre-activate!
|
|
3100
3265
|
if (nextLineEl) {
|
|
3101
3266
|
// Apply unblur & zoom effect ahead of lyric start
|
|
3267
|
+
preActiveLineIndex = i;
|
|
3102
3268
|
nextLineEl.classList.add('pre-active');
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
if (nextLineEl !== this.currentPrimaryActiveLine) {
|
|
3106
|
-
this.scrollToActiveLineYouLy(nextLineEl);
|
|
3107
|
-
}
|
|
3269
|
+
this.clearPreActiveClasses(i);
|
|
3270
|
+
this.focusLine(nextLineEl);
|
|
3108
3271
|
}
|
|
3109
3272
|
break;
|
|
3110
|
-
} else if (nextLineEl) {
|
|
3111
|
-
// Ensure lines outside the pre-scroll window don't stay pre-active
|
|
3112
|
-
nextLineEl.classList.remove('pre-active');
|
|
3113
3273
|
}
|
|
3114
3274
|
}
|
|
3275
|
+
|
|
3276
|
+
this.clearPreActiveClasses(preActiveLineIndex);
|
|
3115
3277
|
}
|
|
3116
3278
|
}
|
|
3117
3279
|
}
|
|
@@ -3192,40 +3354,25 @@ export class AmLyrics extends LitElement {
|
|
|
3192
3354
|
* Handle scrolling when active line indices change.
|
|
3193
3355
|
* Called imperatively from _onTimeChanged instead of from updated().
|
|
3194
3356
|
*/
|
|
3195
|
-
private _handleActiveLineScroll(
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
this.activeLineIndices.length === 0
|
|
3201
|
-
) {
|
|
3357
|
+
private _handleActiveLineScroll(
|
|
3358
|
+
_oldActiveIndices: number[],
|
|
3359
|
+
forceScroll = false,
|
|
3360
|
+
): void {
|
|
3361
|
+
if (this.activeLineIndices.length === 0 || !this.lyricsContainer) {
|
|
3202
3362
|
return;
|
|
3203
3363
|
}
|
|
3204
3364
|
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
idx => !oldActiveIndices.includes(idx),
|
|
3365
|
+
const targetLineIndex = this.getPrimaryActiveLineIndex(
|
|
3366
|
+
this.activeLineIndices,
|
|
3208
3367
|
);
|
|
3368
|
+
if (targetLineIndex === null) return;
|
|
3209
3369
|
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
return;
|
|
3213
|
-
}
|
|
3214
|
-
|
|
3215
|
-
// New lines were added — scroll to the latest newly-added line.
|
|
3216
|
-
// Previous overlap logic skipped every other line for songs with tiny
|
|
3217
|
-
// timing overlaps between consecutive lines, causing a visible glitch.
|
|
3218
|
-
const latestNewIndex = newlyAdded[newlyAdded.length - 1];
|
|
3219
|
-
const targetLine = this.lyricsContainer?.querySelector(
|
|
3220
|
-
`#lyrics-line-${latestNewIndex}`,
|
|
3370
|
+
const targetLine = this.lyricsContainer.querySelector(
|
|
3371
|
+
`#lyrics-line-${targetLineIndex}`,
|
|
3221
3372
|
) as HTMLElement;
|
|
3222
3373
|
|
|
3223
3374
|
if (targetLine) {
|
|
3224
|
-
this.
|
|
3225
|
-
} else if (this.currentPrimaryActiveLine) {
|
|
3226
|
-
this.scrollToActiveLineYouLy(this.currentPrimaryActiveLine);
|
|
3227
|
-
} else {
|
|
3228
|
-
this.scrollToActiveLine();
|
|
3375
|
+
this.focusLine(targetLine, forceScroll);
|
|
3229
3376
|
}
|
|
3230
3377
|
}
|
|
3231
3378
|
|
|
@@ -3321,6 +3468,83 @@ export class AmLyrics extends LitElement {
|
|
|
3321
3468
|
return a.length === b.length && a.every((val, i) => val === b[i]);
|
|
3322
3469
|
}
|
|
3323
3470
|
|
|
3471
|
+
private static getLineIndexFromElement(
|
|
3472
|
+
lineElement: HTMLElement | null,
|
|
3473
|
+
): number | null {
|
|
3474
|
+
if (!lineElement) return null;
|
|
3475
|
+
const match = lineElement.id.match(/^lyrics-line-(\d+)$/);
|
|
3476
|
+
return match ? parseInt(match[1], 10) : null;
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
private static getGapLoopDelay(gapDuration: number): number {
|
|
3480
|
+
const desiredPhase = GAP_PULSE_DURATION_MS;
|
|
3481
|
+
const targetTime = gapDuration - GAP_EXIT_LEAD_MS;
|
|
3482
|
+
const normalizedTarget =
|
|
3483
|
+
((targetTime % GAP_PULSE_CYCLE_MS) + GAP_PULSE_CYCLE_MS) %
|
|
3484
|
+
GAP_PULSE_CYCLE_MS;
|
|
3485
|
+
|
|
3486
|
+
return (
|
|
3487
|
+
(((desiredPhase - normalizedTarget) % GAP_PULSE_CYCLE_MS) +
|
|
3488
|
+
GAP_PULSE_CYCLE_MS) %
|
|
3489
|
+
GAP_PULSE_CYCLE_MS
|
|
3490
|
+
);
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
private clearPreActiveClasses(exceptLineIndex: number | null = null): void {
|
|
3494
|
+
if (!this.lyricsContainer) return;
|
|
3495
|
+
|
|
3496
|
+
this.lyricsContainer
|
|
3497
|
+
.querySelectorAll('.lyrics-line.pre-active')
|
|
3498
|
+
.forEach(element => {
|
|
3499
|
+
const lineElement = element as HTMLElement;
|
|
3500
|
+
const lineIndex = AmLyrics.getLineIndexFromElement(lineElement);
|
|
3501
|
+
if (lineIndex !== exceptLineIndex) {
|
|
3502
|
+
lineElement.classList.remove('pre-active');
|
|
3503
|
+
}
|
|
3504
|
+
});
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
private getPrimaryActiveLineIndex(activeIndices: number[]): number | null {
|
|
3508
|
+
if (activeIndices.length === 0) return null;
|
|
3509
|
+
|
|
3510
|
+
const groupStart = activeIndices[0];
|
|
3511
|
+
const groupEnd = activeIndices[activeIndices.length - 1];
|
|
3512
|
+
let candidateIndex = Math.max(groupStart, groupEnd - 2);
|
|
3513
|
+
|
|
3514
|
+
const currentPrimaryIndex = AmLyrics.getLineIndexFromElement(
|
|
3515
|
+
this.currentPrimaryActiveLine,
|
|
3516
|
+
);
|
|
3517
|
+
if (
|
|
3518
|
+
currentPrimaryIndex !== null &&
|
|
3519
|
+
activeIndices.includes(currentPrimaryIndex) &&
|
|
3520
|
+
candidateIndex < currentPrimaryIndex
|
|
3521
|
+
) {
|
|
3522
|
+
candidateIndex = currentPrimaryIndex;
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
return candidateIndex;
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3528
|
+
private focusLine(lineElement: HTMLElement, forceScroll = false): void {
|
|
3529
|
+
const primaryChanged = lineElement !== this.currentPrimaryActiveLine;
|
|
3530
|
+
|
|
3531
|
+
if (primaryChanged) {
|
|
3532
|
+
this.lastPrimaryActiveLine = this.currentPrimaryActiveLine;
|
|
3533
|
+
this.currentPrimaryActiveLine = lineElement;
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
this.updatePositionClasses(lineElement);
|
|
3537
|
+
|
|
3538
|
+
if (
|
|
3539
|
+
(forceScroll || primaryChanged) &&
|
|
3540
|
+
this.autoScroll &&
|
|
3541
|
+
!this.isUserScrolling &&
|
|
3542
|
+
!this.isClickSeeking
|
|
3543
|
+
) {
|
|
3544
|
+
this.scrollToActiveLineYouLy(lineElement, forceScroll);
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3324
3548
|
private handleUserScroll() {
|
|
3325
3549
|
// Ignore programmatic scrolls and click-seek scrolls
|
|
3326
3550
|
if (this.isProgrammaticScroll || this.isClickSeeking) {
|
|
@@ -3641,8 +3865,9 @@ export class AmLyrics extends LitElement {
|
|
|
3641
3865
|
this.animatingLines = [];
|
|
3642
3866
|
|
|
3643
3867
|
// Find the clicked line element and scroll to it with forceScroll (like YouLyPlus)
|
|
3868
|
+
// Timestamps are already in milliseconds — match the data-start-time attribute directly
|
|
3644
3869
|
const clickedLineElement = this.lyricsContainer?.querySelector(
|
|
3645
|
-
`.lyrics-line[data-start-time="${line.timestamp
|
|
3870
|
+
`.lyrics-line[data-start-time="${line.text[0]?.timestamp || 0}"]`,
|
|
3646
3871
|
) as HTMLElement | null;
|
|
3647
3872
|
|
|
3648
3873
|
if (clickedLineElement && this.lyricsContainer) {
|
|
@@ -3809,11 +4034,10 @@ export class AmLyrics extends LitElement {
|
|
|
3809
4034
|
const { animatingLines } = this;
|
|
3810
4035
|
|
|
3811
4036
|
const targetTop = Math.max(0, -newTranslateY);
|
|
3812
|
-
|
|
3813
|
-
// The || operator treats 0 as falsy, which caused bounce when scrollTop was 0
|
|
4037
|
+
const appliedTranslateY = -targetTop;
|
|
3814
4038
|
const prevOffset = -parent.scrollTop;
|
|
3815
|
-
const delta = prevOffset -
|
|
3816
|
-
this.currentScrollOffset =
|
|
4039
|
+
const delta = prevOffset - appliedTranslateY;
|
|
4040
|
+
this.currentScrollOffset = appliedTranslateY;
|
|
3817
4041
|
|
|
3818
4042
|
// Skip animation if already at the target position (e.g., first lines at top)
|
|
3819
4043
|
if (Math.abs(parent.scrollTop - targetTop) < 1 && Math.abs(delta) < 1) {
|
|
@@ -3828,6 +4052,7 @@ export class AmLyrics extends LitElement {
|
|
|
3828
4052
|
line.classList.remove('scroll-animate');
|
|
3829
4053
|
line.style.removeProperty('--scroll-delta');
|
|
3830
4054
|
line.style.removeProperty('--lyrics-line-delay');
|
|
4055
|
+
line.style.removeProperty('--scroll-duration');
|
|
3831
4056
|
}
|
|
3832
4057
|
animatingLines.length = 0;
|
|
3833
4058
|
parent.scrollTo({ top: targetTop, behavior: 'smooth' });
|
|
@@ -3856,7 +4081,7 @@ export class AmLyrics extends LitElement {
|
|
|
3856
4081
|
const referenceIndex = lineArray.indexOf(referenceLine);
|
|
3857
4082
|
if (referenceIndex === -1) return;
|
|
3858
4083
|
|
|
3859
|
-
const delayIncrement =
|
|
4084
|
+
const delayIncrement = SCROLL_DELAY_INCREMENT_MS;
|
|
3860
4085
|
const lookBehind = 10;
|
|
3861
4086
|
const lookAhead = 15;
|
|
3862
4087
|
const len = lineArray.length;
|
|
@@ -3878,10 +4103,14 @@ export class AmLyrics extends LitElement {
|
|
|
3878
4103
|
|
|
3879
4104
|
line.style.setProperty('--scroll-delta', `${delta}px`);
|
|
3880
4105
|
line.style.setProperty('--lyrics-line-delay', `${delay}ms`);
|
|
4106
|
+
line.style.setProperty(
|
|
4107
|
+
'--scroll-duration',
|
|
4108
|
+
`${SCROLL_ANIMATION_DURATION_MS}ms`,
|
|
4109
|
+
);
|
|
3881
4110
|
|
|
3882
4111
|
newAnimatingLines.push(line);
|
|
3883
4112
|
|
|
3884
|
-
const lineDuration =
|
|
4113
|
+
const lineDuration = SCROLL_ANIMATION_DURATION_MS + delay;
|
|
3885
4114
|
if (lineDuration > maxAnimationDuration) {
|
|
3886
4115
|
maxAnimationDuration = lineDuration;
|
|
3887
4116
|
}
|
|
@@ -3899,7 +4128,7 @@ export class AmLyrics extends LitElement {
|
|
|
3899
4128
|
}
|
|
3900
4129
|
|
|
3901
4130
|
animState.isAnimating = true;
|
|
3902
|
-
const BASE_DURATION =
|
|
4131
|
+
const BASE_DURATION = SCROLL_ANIMATION_DURATION_MS;
|
|
3903
4132
|
|
|
3904
4133
|
this.scrollUnlockTimeout = setTimeout(() => {
|
|
3905
4134
|
animState.isAnimating = false;
|
|
@@ -3917,6 +4146,7 @@ export class AmLyrics extends LitElement {
|
|
|
3917
4146
|
line.classList.remove('scroll-animate');
|
|
3918
4147
|
line.style.removeProperty('--scroll-delta');
|
|
3919
4148
|
line.style.removeProperty('--lyrics-line-delay');
|
|
4149
|
+
line.style.removeProperty('--scroll-duration');
|
|
3920
4150
|
}
|
|
3921
4151
|
animatingLines.length = 0;
|
|
3922
4152
|
this.scrollAnimationTimeout = undefined;
|
|
@@ -4022,7 +4252,7 @@ export class AmLyrics extends LitElement {
|
|
|
4022
4252
|
|
|
4023
4253
|
setTimeout(() => {
|
|
4024
4254
|
this.isProgrammaticScroll = false;
|
|
4025
|
-
},
|
|
4255
|
+
}, SCROLL_ANIMATION_DURATION_MS + 160);
|
|
4026
4256
|
|
|
4027
4257
|
this.animateScrollYouLy(targetTranslateY, forceScroll);
|
|
4028
4258
|
}
|
|
@@ -4067,35 +4297,76 @@ export class AmLyrics extends LitElement {
|
|
|
4067
4297
|
value: string;
|
|
4068
4298
|
}> = [];
|
|
4069
4299
|
|
|
4070
|
-
// Step 1
|
|
4300
|
+
// Step 1 & 2: Apply animations
|
|
4071
4301
|
if (isGrowable && isFirstSyllable && allWordCharSpans.length > 0) {
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
const growDurationMs = finalDuration * 1.5;
|
|
4302
|
+
// Glow AND wipe applied to ALL characters simultaneously from the first syllable
|
|
4303
|
+
// This prevents CSS animation restarts because the `animation` property is set once.
|
|
4075
4304
|
|
|
4076
|
-
|
|
4305
|
+
const firstSyllableStartTime = parseFloat(
|
|
4306
|
+
syllable.getAttribute('data-start-time') || '0',
|
|
4307
|
+
);
|
|
4308
|
+
|
|
4309
|
+
allWordCharSpans.forEach((span, charIndexInWord) => {
|
|
4077
4310
|
const horizontalOffset = parseFloat(
|
|
4078
4311
|
span.dataset.horizontalOffset || '0',
|
|
4079
4312
|
);
|
|
4080
|
-
// Use syllableCharIndex like YouLyPlus, not loop index
|
|
4081
|
-
const charIndex = parseFloat(span.dataset.syllableCharIndex || '0');
|
|
4082
|
-
const growDelay = baseDelayPerChar * charIndex;
|
|
4083
4313
|
|
|
4084
|
-
// READ DATA ATTRIBUTES for style values
|
|
4085
4314
|
const maxScale = span.dataset.maxScale || '1.1';
|
|
4086
4315
|
const shadowIntensity = span.dataset.shadowIntensity || '0.6';
|
|
4087
4316
|
const translateYPeak = span.dataset.translateYPeak || '-2';
|
|
4088
4317
|
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
)
|
|
4318
|
+
const animationParts: string[] = [];
|
|
4319
|
+
|
|
4320
|
+
const parentSyllable = span.closest('.lyrics-syllable');
|
|
4321
|
+
if (parentSyllable) {
|
|
4322
|
+
const parentDuration = parseFloat(
|
|
4323
|
+
parentSyllable.getAttribute('data-duration') || '0',
|
|
4324
|
+
);
|
|
4325
|
+
const parentStartTime = parseFloat(
|
|
4326
|
+
parentSyllable.getAttribute('data-start-time') || '0',
|
|
4327
|
+
);
|
|
4328
|
+
|
|
4329
|
+
const startPct = parseFloat(span.dataset.wipeStart || '0');
|
|
4330
|
+
const durationPct = parseFloat(span.dataset.wipeDuration || '0');
|
|
4331
|
+
|
|
4332
|
+
const relativeStartOffset = Math.max(
|
|
4333
|
+
0,
|
|
4334
|
+
parentStartTime - firstSyllableStartTime,
|
|
4335
|
+
);
|
|
4336
|
+
const wipeDelay = relativeStartOffset + parentDuration * startPct;
|
|
4337
|
+
const wipeDuration = parentDuration * durationPct;
|
|
4338
|
+
|
|
4339
|
+
const useStartAnimation = isFirstInContainer && charIndexInWord === 0;
|
|
4340
|
+
let charWipeAnimation = 'wipe';
|
|
4341
|
+
if (useStartAnimation)
|
|
4342
|
+
charWipeAnimation = isRTL ? 'start-wipe-rtl' : 'start-wipe';
|
|
4343
|
+
else charWipeAnimation = isRTL ? 'wipe-rtl' : 'wipe';
|
|
4344
|
+
|
|
4345
|
+
// Blend word and syllable durations to let the gradient flow smoothly
|
|
4346
|
+
// while still responding to syllable pacing (no strict exactness, just natural flow)
|
|
4347
|
+
const growDelay = wipeDelay;
|
|
4348
|
+
const growDurationMs = Math.max(
|
|
4349
|
+
600,
|
|
4350
|
+
wordDurationMs * 0.8 + parentDuration * 1.5,
|
|
4351
|
+
);
|
|
4352
|
+
|
|
4353
|
+
animationParts.push(
|
|
4354
|
+
`grow-dynamic ${growDurationMs}ms ease-in-out ${growDelay}ms forwards`,
|
|
4355
|
+
);
|
|
4356
|
+
|
|
4357
|
+
if (wipeDuration > 0) {
|
|
4358
|
+
animationParts.push(
|
|
4359
|
+
`${charWipeAnimation} ${wipeDuration}ms linear ${wipeDelay}ms forwards`,
|
|
4360
|
+
);
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
|
|
4364
|
+
charAnimationsMap.set(span, animationParts.join(', '));
|
|
4093
4365
|
|
|
4094
|
-
// Push style updates to be applied imperatively
|
|
4095
4366
|
styleUpdates.push({
|
|
4096
4367
|
element: span,
|
|
4097
4368
|
property: '--char-offset-x',
|
|
4098
|
-
value: `${horizontalOffset}`,
|
|
4369
|
+
value: `${horizontalOffset}`,
|
|
4099
4370
|
});
|
|
4100
4371
|
styleUpdates.push({
|
|
4101
4372
|
element: span,
|
|
@@ -4110,14 +4381,34 @@ export class AmLyrics extends LitElement {
|
|
|
4110
4381
|
styleUpdates.push({
|
|
4111
4382
|
element: span,
|
|
4112
4383
|
property: '--translate-y-peak',
|
|
4113
|
-
value: `${translateYPeak}`,
|
|
4384
|
+
value: `${translateYPeak}`,
|
|
4114
4385
|
});
|
|
4115
4386
|
});
|
|
4116
|
-
}
|
|
4387
|
+
} else if (isGrowable && !isFirstSyllable && charSpans.length > 0) {
|
|
4388
|
+
// For subsequent syllables of a growable word:
|
|
4389
|
+
// If they already have `grow-dynamic`, it means the first syllable correctly took care of BOTH grow and wipe!
|
|
4390
|
+
// Otherwise, they scrubbed directly into this syllable, so let's at least do the wipe.
|
|
4391
|
+
charSpans.forEach(span => {
|
|
4392
|
+
const existingAnimation =
|
|
4393
|
+
charAnimationsMap.get(span) || span.style.animation || '';
|
|
4394
|
+
if (existingAnimation.includes('grow-dynamic')) return;
|
|
4395
|
+
|
|
4396
|
+
const startPct = parseFloat(span.dataset.wipeStart || '0');
|
|
4397
|
+
const durationPct = parseFloat(span.dataset.wipeDuration || '0');
|
|
4398
|
+
const wipeDelay = syllableDurationMs * startPct;
|
|
4399
|
+
const wipeDuration = syllableDurationMs * durationPct;
|
|
4400
|
+
|
|
4401
|
+
const charWipeAnimation = isRTL ? 'wipe-rtl' : 'wipe';
|
|
4117
4402
|
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4403
|
+
if (wipeDuration > 0) {
|
|
4404
|
+
charAnimationsMap.set(
|
|
4405
|
+
span,
|
|
4406
|
+
`${charWipeAnimation} ${wipeDuration}ms linear ${wipeDelay}ms forwards`,
|
|
4407
|
+
);
|
|
4408
|
+
}
|
|
4409
|
+
});
|
|
4410
|
+
} else if (charSpans.length > 0) {
|
|
4411
|
+
// Per-character wipe for non-growable words (matching YouLyPlus)
|
|
4121
4412
|
charSpans.forEach((span, charIndex) => {
|
|
4122
4413
|
const startPct = parseFloat(span.dataset.wipeStart || '0');
|
|
4123
4414
|
const durationPct = parseFloat(span.dataset.wipeDuration || '0');
|
|
@@ -4126,54 +4417,39 @@ export class AmLyrics extends LitElement {
|
|
|
4126
4417
|
const wipeDuration = syllableDurationMs * durationPct;
|
|
4127
4418
|
|
|
4128
4419
|
const useStartAnimation = isFirstInContainer && charIndex === 0;
|
|
4129
|
-
let charWipeAnimation
|
|
4420
|
+
let charWipeAnimation = 'wipe';
|
|
4130
4421
|
if (useStartAnimation) {
|
|
4131
4422
|
charWipeAnimation = isRTL ? 'start-wipe-rtl' : 'start-wipe';
|
|
4132
4423
|
} else {
|
|
4133
4424
|
charWipeAnimation = isRTL ? 'wipe-rtl' : 'wipe';
|
|
4134
4425
|
}
|
|
4135
4426
|
|
|
4136
|
-
// Get existing animation from map (grow-dynamic) and combine with wipe
|
|
4137
|
-
const existingAnimation =
|
|
4138
|
-
charAnimationsMap.get(span) || span.style.animation || '';
|
|
4139
|
-
const animationParts: string[] = [];
|
|
4140
|
-
|
|
4141
|
-
if (existingAnimation && existingAnimation.includes('grow-dynamic')) {
|
|
4142
|
-
animationParts.push(existingAnimation.split(',')[0].trim());
|
|
4143
|
-
}
|
|
4144
|
-
|
|
4145
4427
|
if (wipeDuration > 0) {
|
|
4146
|
-
|
|
4428
|
+
charAnimationsMap.set(
|
|
4429
|
+
span,
|
|
4147
4430
|
`${charWipeAnimation} ${wipeDuration}ms linear ${wipeDelay}ms forwards`,
|
|
4148
4431
|
);
|
|
4149
4432
|
}
|
|
4150
|
-
|
|
4151
|
-
charAnimationsMap.set(span, animationParts.join(', '));
|
|
4152
4433
|
});
|
|
4153
4434
|
} else {
|
|
4154
|
-
// Syllable-level wipe for regular (non-growable) words
|
|
4435
|
+
// Syllable-level wipe for regular (non-growable) words without chars
|
|
4155
4436
|
const wipeRatio = parseFloat(
|
|
4156
4437
|
syllable.getAttribute('data-wipe-ratio') || '1',
|
|
4157
4438
|
);
|
|
4158
4439
|
const visualDuration = syllableDurationMs * wipeRatio;
|
|
4159
4440
|
|
|
4160
|
-
let wipeAnimation
|
|
4441
|
+
let wipeAnimation = 'wipe';
|
|
4161
4442
|
if (isFirstInContainer) {
|
|
4162
4443
|
wipeAnimation = isRTL ? 'start-wipe-rtl' : 'start-wipe';
|
|
4163
4444
|
} else {
|
|
4164
4445
|
wipeAnimation = isRTL ? 'wipe-rtl' : 'wipe';
|
|
4165
4446
|
}
|
|
4166
4447
|
|
|
4167
|
-
if (syllable.classList.contains('line-synced'))
|
|
4168
|
-
// If line-synced, just add the class for CSS animation, or ensure valid state
|
|
4169
|
-
// The CSS rule .lyrics-syllable.line-synced handles the fade
|
|
4170
|
-
return;
|
|
4171
|
-
}
|
|
4448
|
+
if (syllable.classList.contains('line-synced')) return;
|
|
4172
4449
|
|
|
4173
4450
|
const currentWipeAnimation = isGap ? 'fade-gap' : wipeAnimation;
|
|
4174
|
-
const syllableAnimation = `${currentWipeAnimation} ${visualDuration}ms ${isGap ? 'ease-out' : 'linear'} forwards`;
|
|
4175
4451
|
// eslint-disable-next-line no-param-reassign
|
|
4176
|
-
syllable.style.animation =
|
|
4452
|
+
syllable.style.animation = `${currentWipeAnimation} ${visualDuration}ms ${isGap ? 'ease-out' : 'linear'} forwards`;
|
|
4177
4453
|
}
|
|
4178
4454
|
|
|
4179
4455
|
// --- WRITE PHASE ---
|
|
@@ -4434,9 +4710,7 @@ export class AmLyrics extends LitElement {
|
|
|
4434
4710
|
}
|
|
4435
4711
|
|
|
4436
4712
|
if (running) {
|
|
4437
|
-
this.animationFrameId = requestAnimationFrame(
|
|
4438
|
-
this.animateProgress.bind(this),
|
|
4439
|
-
);
|
|
4713
|
+
this.animationFrameId = requestAnimationFrame(this._boundAnimateProgress);
|
|
4440
4714
|
} else if (this.animationFrameId) {
|
|
4441
4715
|
// Stop animation if no words are running
|
|
4442
4716
|
cancelAnimationFrame(this.animationFrameId);
|
|
@@ -4713,6 +4987,9 @@ export class AmLyrics extends LitElement {
|
|
|
4713
4987
|
const groupGrowable: boolean[] = new Array(wordGroups.length).fill(
|
|
4714
4988
|
false,
|
|
4715
4989
|
);
|
|
4990
|
+
const groupGlowing: boolean[] = new Array(wordGroups.length).fill(
|
|
4991
|
+
false,
|
|
4992
|
+
);
|
|
4716
4993
|
// Visual word info for growable char-level glow:
|
|
4717
4994
|
// Each group stores the combined visual word's text, duration, and
|
|
4718
4995
|
// the char offset of this group within the visual word.
|
|
@@ -4752,20 +5029,30 @@ export class AmLyrics extends LitElement {
|
|
|
4752
5029
|
combinedText,
|
|
4753
5030
|
);
|
|
4754
5031
|
const hasHyphen = combinedText.includes('-');
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
!hasHyphen &&
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
5032
|
+
|
|
5033
|
+
const wordLen = combinedText.length;
|
|
5034
|
+
let isGrowableVW =
|
|
5035
|
+
!isCJK && !isRTL && !hasHyphen && wordLen > 0 && wordLen <= 12;
|
|
5036
|
+
if (isGrowableVW) {
|
|
5037
|
+
if (wordLen < 3) {
|
|
5038
|
+
isGrowableVW =
|
|
5039
|
+
combinedDuration >= 1110 && combinedDuration >= wordLen * 550;
|
|
5040
|
+
} else {
|
|
5041
|
+
isGrowableVW =
|
|
5042
|
+
combinedDuration >= 800 && combinedDuration >= wordLen * 180;
|
|
5043
|
+
}
|
|
5044
|
+
}
|
|
5045
|
+
|
|
5046
|
+
// Glow requirement (more strict)
|
|
5047
|
+
const isGlowingVW =
|
|
5048
|
+
isGrowableVW &&
|
|
5049
|
+
combinedDuration >= 1200 &&
|
|
5050
|
+
combinedDuration >= combinedText.length * 300;
|
|
4765
5051
|
|
|
4766
5052
|
let charOff = 0;
|
|
4767
5053
|
for (let gi = vwStart; gi <= vwEnd; gi += 1) {
|
|
4768
5054
|
groupGrowable[gi] = isGrowableVW;
|
|
5055
|
+
groupGlowing[gi] = isGlowingVW;
|
|
4769
5056
|
vwFullText[gi] = combinedText;
|
|
4770
5057
|
vwFullDuration[gi] = combinedDuration;
|
|
4771
5058
|
vwCharOffset[gi] = charOff;
|
|
@@ -4783,264 +5070,159 @@ export class AmLyrics extends LitElement {
|
|
|
4783
5070
|
const mainVocalElement = html`<p class="main-vocal-container">
|
|
4784
5071
|
${wordGroups.map((group, groupIdx) => {
|
|
4785
5072
|
const isGrowable = groupGrowable[groupIdx];
|
|
5073
|
+
const isGlowing = groupGlowing[groupIdx];
|
|
5074
|
+
const groupLineSynced = group.some(s => s.lineSynced);
|
|
4786
5075
|
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
}
|
|
5076
|
+
const wordText = isGrowable ? vwFullText[groupIdx] : '';
|
|
5077
|
+
const wordDuration = isGrowable ? vwFullDuration[groupIdx] : 0;
|
|
5078
|
+
const wordNumChars = wordText.length;
|
|
5079
|
+
const groupCharOffset = isGrowable ? vwCharOffset[groupIdx] : 0;
|
|
4792
5080
|
|
|
4793
|
-
|
|
4794
|
-
const groupLineSynced = group.some(s => s.lineSynced);
|
|
5081
|
+
let sylCharAccumulator = 0;
|
|
4795
5082
|
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
// by scanning forward while vwCharOffset is consecutive
|
|
4807
|
-
let combinedRawText = '';
|
|
4808
|
-
for (let gi = groupIdx; gi < wordGroups.length; gi += 1) {
|
|
4809
|
-
if (gi > groupIdx && vwCharOffset[gi] === 0) break;
|
|
4810
|
-
if (gi > groupIdx && !groupGrowable[gi]) break;
|
|
4811
|
-
combinedRawText += wordGroups[gi].map(s => s.text).join('');
|
|
4812
|
-
}
|
|
5083
|
+
return html`<span
|
|
5084
|
+
class="lyrics-word ${isGrowable ? 'growable' : ''} ${isGlowing
|
|
5085
|
+
? 'glowing'
|
|
5086
|
+
: ''} ${group.length > 1 ? 'allow-break' : ''}"
|
|
5087
|
+
>
|
|
5088
|
+
${group.map((syllable, sylIdx) => {
|
|
5089
|
+
const startTimeMs = syllable.timestamp;
|
|
5090
|
+
const endTimeMs = syllable.endtime;
|
|
5091
|
+
const durationMs = endTimeMs - startTimeMs;
|
|
5092
|
+
const text = syllable.text || '';
|
|
4813
5093
|
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
(maxDuration - minDuration),
|
|
4831
|
-
),
|
|
4832
|
-
);
|
|
4833
|
-
const easedProgress = progress ** easingPower;
|
|
4834
|
-
|
|
4835
|
-
const isLongWord = numChars > 5;
|
|
4836
|
-
const isShortDuration = wordDuration < 1500;
|
|
4837
|
-
let maxDecayRate = 0;
|
|
4838
|
-
if (isLongWord || isShortDuration) {
|
|
4839
|
-
let decayStrength = 0;
|
|
4840
|
-
if (isLongWord)
|
|
4841
|
-
decayStrength += Math.min((numChars - 5) / 3, 1.0) * 0.4;
|
|
4842
|
-
if (isShortDuration)
|
|
4843
|
-
decayStrength +=
|
|
4844
|
-
Math.max(0, 1.0 - (wordDuration - 1000) / 500) * 0.4;
|
|
4845
|
-
maxDecayRate = Math.min(decayStrength, 0.85);
|
|
4846
|
-
}
|
|
4847
|
-
|
|
4848
|
-
const positionInWord =
|
|
4849
|
-
numChars > 1 ? charIndex / (numChars - 1) : 0;
|
|
4850
|
-
const decayFactor = 1.0 - positionInWord * maxDecayRate;
|
|
4851
|
-
const charProgress = easedProgress * decayFactor;
|
|
4852
|
-
|
|
4853
|
-
const baseGrowth = numChars <= 3 ? 0.07 : 0.05;
|
|
4854
|
-
const charMaxScale = 1.0 + baseGrowth + charProgress * 0.1;
|
|
4855
|
-
const charShadowIntensity = 0.4 + charProgress * 0.4;
|
|
4856
|
-
const normalizedGrowth = (charMaxScale - 1.0) / 0.13;
|
|
4857
|
-
const charTranslateYPeak = -normalizedGrowth * 6;
|
|
4858
|
-
|
|
4859
|
-
const position = (charIndex + 0.5) / numChars;
|
|
4860
|
-
const horizontalOffset =
|
|
4861
|
-
(position - 0.5) * 2 * ((charMaxScale - 1.0) * 25);
|
|
4862
|
-
|
|
4863
|
-
return html`<span
|
|
4864
|
-
class="char"
|
|
4865
|
-
data-char-index="${charIndex}"
|
|
4866
|
-
data-syllable-char-index="${charIndex}"
|
|
4867
|
-
data-wipe-start="${charStartPercent.toFixed(4)}"
|
|
4868
|
-
data-wipe-duration="${(1 / numChars).toFixed(4)}"
|
|
4869
|
-
data-horizontal-offset="${horizontalOffset.toFixed(2)}"
|
|
4870
|
-
data-max-scale="${charMaxScale.toFixed(3)}"
|
|
4871
|
-
data-shadow-intensity="${charShadowIntensity.toFixed(3)}"
|
|
4872
|
-
data-translate-y-peak="${charTranslateYPeak.toFixed(3)}"
|
|
4873
|
-
>${char}</span
|
|
4874
|
-
>`;
|
|
4875
|
-
})}`;
|
|
4876
|
-
|
|
4877
|
-
return html`<span class="lyrics-word growable">
|
|
4878
|
-
<span class="lyrics-syllable-wrap">
|
|
4879
|
-
<span
|
|
4880
|
-
class="lyrics-syllable ${groupLineSynced
|
|
4881
|
-
? 'line-synced'
|
|
4882
|
-
: ''}"
|
|
4883
|
-
data-start-time="${startTimeMs}"
|
|
4884
|
-
data-end-time="${endTimeMs}"
|
|
4885
|
-
data-duration="${wordDuration}"
|
|
4886
|
-
data-syllable-index="0"
|
|
4887
|
-
data-wipe-ratio="1"
|
|
4888
|
-
>${syllableContent}</span
|
|
4889
|
-
>
|
|
4890
|
-
</span>
|
|
4891
|
-
</span>`;
|
|
4892
|
-
}
|
|
5094
|
+
const romanizedText =
|
|
5095
|
+
this.showRomanization &&
|
|
5096
|
+
syllable.romanizedText &&
|
|
5097
|
+
syllable.romanizedText.trim() !== syllable.text.trim()
|
|
5098
|
+
? html`<span
|
|
5099
|
+
class="lyrics-syllable transliteration ${groupLineSynced
|
|
5100
|
+
? 'line-synced'
|
|
5101
|
+
: ''}"
|
|
5102
|
+
data-start-time="${startTimeMs}"
|
|
5103
|
+
data-end-time="${endTimeMs}"
|
|
5104
|
+
data-duration="${durationMs}"
|
|
5105
|
+
data-syllable-index="0"
|
|
5106
|
+
data-wipe-ratio="1"
|
|
5107
|
+
>${syllable.romanizedText}</span
|
|
5108
|
+
>`
|
|
5109
|
+
: '';
|
|
4893
5110
|
|
|
4894
|
-
|
|
4895
|
-
if (group.length === 1) {
|
|
4896
|
-
const syllable = group[0];
|
|
4897
|
-
const startTimeMs = syllable.timestamp;
|
|
4898
|
-
const endTimeMs = syllable.endtime;
|
|
4899
|
-
const durationMs = endTimeMs - startTimeMs;
|
|
4900
|
-
const text = syllable.text || '';
|
|
4901
|
-
const trimmedText = text.trim();
|
|
4902
|
-
|
|
4903
|
-
// Optional romanization per syllable (hide if same as the original text)
|
|
4904
|
-
const romanizedText =
|
|
4905
|
-
this.showRomanization &&
|
|
4906
|
-
syllable.romanizedText &&
|
|
4907
|
-
syllable.romanizedText.trim() !== syllable.text.trim()
|
|
4908
|
-
? html`<span
|
|
4909
|
-
class="lyrics-syllable transliteration ${syllable.lineSynced
|
|
4910
|
-
? 'line-synced'
|
|
4911
|
-
: ''}"
|
|
4912
|
-
data-start-time="${startTimeMs}"
|
|
4913
|
-
data-end-time="${endTimeMs}"
|
|
4914
|
-
data-duration="${durationMs}"
|
|
4915
|
-
data-syllable-index="0"
|
|
4916
|
-
data-wipe-ratio="1"
|
|
4917
|
-
>${syllable.romanizedText}</span
|
|
4918
|
-
>`
|
|
4919
|
-
: '';
|
|
4920
|
-
|
|
4921
|
-
// For growable words (single-group visual word), use char glow
|
|
4922
|
-
const syllableContent = isGrowable
|
|
4923
|
-
? html`${text.split('').map((char, charIndex) => {
|
|
4924
|
-
if (char === ' ') {
|
|
4925
|
-
return ' ';
|
|
4926
|
-
}
|
|
4927
|
-
const numChars = trimmedText.length;
|
|
4928
|
-
const charStartPercent = charIndex / text.length;
|
|
5111
|
+
let syllableContent: any = text;
|
|
4929
5112
|
|
|
4930
|
-
|
|
4931
|
-
|
|
5113
|
+
if (isGrowable) {
|
|
5114
|
+
let charIndexInsideSyllable = 0;
|
|
5115
|
+
const numCharsInSyllable =
|
|
5116
|
+
text.replace(/\s/g, '').length || 1;
|
|
5117
|
+
|
|
5118
|
+
syllableContent = html`${text.split('').map(char => {
|
|
5119
|
+
if (char === ' ') return ' ';
|
|
5120
|
+
|
|
5121
|
+
const charIndexInsideWord =
|
|
5122
|
+
groupCharOffset + sylCharAccumulator;
|
|
5123
|
+
const charStartPercentVal =
|
|
5124
|
+
charIndexInsideSyllable / numCharsInSyllable;
|
|
5125
|
+
|
|
5126
|
+
sylCharAccumulator += 1;
|
|
5127
|
+
charIndexInsideSyllable += 1;
|
|
5128
|
+
|
|
5129
|
+
const minDuration = 400;
|
|
5130
|
+
const maxDuration = 3000;
|
|
4932
5131
|
const easingPower = 3;
|
|
4933
5132
|
const progress = Math.min(
|
|
4934
5133
|
1,
|
|
4935
5134
|
Math.max(
|
|
4936
5135
|
0,
|
|
4937
|
-
(
|
|
5136
|
+
(wordDuration - minDuration) /
|
|
4938
5137
|
(maxDuration - minDuration),
|
|
4939
5138
|
),
|
|
4940
5139
|
);
|
|
4941
5140
|
const easedProgress = progress ** easingPower;
|
|
4942
5141
|
|
|
4943
|
-
const isLongWord =
|
|
4944
|
-
const isShortDuration =
|
|
5142
|
+
const isLongWord = wordNumChars > 5;
|
|
5143
|
+
const isShortDuration = wordDuration < 1200;
|
|
4945
5144
|
let maxDecayRate = 0;
|
|
4946
5145
|
if (isLongWord || isShortDuration) {
|
|
4947
5146
|
let decayStrength = 0;
|
|
4948
5147
|
if (isLongWord)
|
|
4949
5148
|
decayStrength +=
|
|
4950
|
-
Math.min((
|
|
4951
|
-
if (isShortDuration)
|
|
5149
|
+
Math.min((wordNumChars - 5) / 5, 1.0) * 0.4;
|
|
5150
|
+
if (isShortDuration && wordNumChars > 3)
|
|
5151
|
+
decayStrength +=
|
|
5152
|
+
Math.max(0, 1.0 - (wordDuration - 800) / 400) * 0.3;
|
|
5153
|
+
else if (isShortDuration && wordNumChars <= 3)
|
|
4952
5154
|
decayStrength +=
|
|
4953
|
-
Math.max(0, 1.0 - (
|
|
4954
|
-
maxDecayRate = Math.min(decayStrength, 0.
|
|
5155
|
+
Math.max(0, 1.0 - (wordDuration - 800) / 400) * 0.1;
|
|
5156
|
+
maxDecayRate = Math.min(decayStrength, 0.7);
|
|
4955
5157
|
}
|
|
4956
5158
|
|
|
4957
5159
|
const positionInWord =
|
|
4958
|
-
|
|
5160
|
+
wordNumChars > 1
|
|
5161
|
+
? charIndexInsideWord / (wordNumChars - 1)
|
|
5162
|
+
: 0;
|
|
4959
5163
|
const decayFactor = 1.0 - positionInWord * maxDecayRate;
|
|
4960
5164
|
const charProgress = easedProgress * decayFactor;
|
|
4961
5165
|
|
|
4962
|
-
const baseGrowth =
|
|
4963
|
-
const charMaxScale = 1.0 + baseGrowth + charProgress * 0.
|
|
4964
|
-
const
|
|
4965
|
-
|
|
4966
|
-
|
|
5166
|
+
const baseGrowth = wordNumChars <= 3 ? 0.05 : 0.04;
|
|
5167
|
+
const charMaxScale = 1.0 + baseGrowth + charProgress * 0.08;
|
|
5168
|
+
const glowDurFactor = Math.min(1.1, wordDuration / 1500);
|
|
5169
|
+
let glowLenFactor = 1.0;
|
|
5170
|
+
if (wordNumChars <= 3) {
|
|
5171
|
+
glowLenFactor = 0.85;
|
|
5172
|
+
} else if (wordNumChars >= 6) {
|
|
5173
|
+
glowLenFactor = 1.1;
|
|
5174
|
+
}
|
|
5175
|
+
const glowIntensityScale = glowDurFactor * glowLenFactor;
|
|
5176
|
+
const charShadowIntensity = isGlowing
|
|
5177
|
+
? (0.35 + charProgress * 0.45) * glowIntensityScale
|
|
5178
|
+
: 0;
|
|
5179
|
+
const normalizedGrowth = (charMaxScale - 1.0) / 0.1;
|
|
5180
|
+
const effectiveDuration =
|
|
5181
|
+
(wordDuration + durationMs * 2) / 3;
|
|
5182
|
+
const peakMultiplier = Math.min(
|
|
5183
|
+
1,
|
|
5184
|
+
Math.max(0.3, effectiveDuration / 2000),
|
|
5185
|
+
);
|
|
5186
|
+
const charTranslateYPeak =
|
|
5187
|
+
-normalizedGrowth * (2 * peakMultiplier); // Further dampened lift peak
|
|
4967
5188
|
|
|
4968
|
-
const position = (
|
|
5189
|
+
const position = (charIndexInsideWord + 0.5) / wordNumChars;
|
|
4969
5190
|
const horizontalOffset =
|
|
4970
5191
|
(position - 0.5) * 2 * ((charMaxScale - 1.0) * 25);
|
|
4971
5192
|
|
|
4972
5193
|
return html`<span
|
|
4973
5194
|
class="char"
|
|
4974
|
-
data-char-index="${
|
|
4975
|
-
data-syllable-char-index="${
|
|
4976
|
-
data-wipe-start="${
|
|
4977
|
-
data-wipe-duration="${(1 /
|
|
5195
|
+
data-char-index="${charIndexInsideWord}"
|
|
5196
|
+
data-syllable-char-index="${charIndexInsideWord}"
|
|
5197
|
+
data-wipe-start="${charStartPercentVal.toFixed(4)}"
|
|
5198
|
+
data-wipe-duration="${(1 / numCharsInSyllable).toFixed(
|
|
5199
|
+
4,
|
|
5200
|
+
)}"
|
|
4978
5201
|
data-horizontal-offset="${horizontalOffset.toFixed(2)}"
|
|
4979
5202
|
data-max-scale="${charMaxScale.toFixed(3)}"
|
|
4980
5203
|
data-shadow-intensity="${charShadowIntensity.toFixed(3)}"
|
|
4981
5204
|
data-translate-y-peak="${charTranslateYPeak.toFixed(3)}"
|
|
4982
5205
|
>${char}</span
|
|
4983
5206
|
>`;
|
|
4984
|
-
})}
|
|
4985
|
-
|
|
5207
|
+
})}`;
|
|
5208
|
+
}
|
|
4986
5209
|
|
|
4987
|
-
|
|
4988
|
-
class="lyrics-word ${isGrowable ? 'growable' : ''}"
|
|
4989
|
-
>
|
|
4990
|
-
<span class="lyrics-syllable-wrap">
|
|
5210
|
+
return html`<span class="lyrics-syllable-wrap">
|
|
4991
5211
|
<span
|
|
4992
|
-
class="lyrics-syllable ${
|
|
5212
|
+
class="lyrics-syllable ${groupLineSynced
|
|
4993
5213
|
? 'line-synced'
|
|
4994
5214
|
: ''}"
|
|
4995
5215
|
data-start-time="${startTimeMs}"
|
|
4996
5216
|
data-end-time="${endTimeMs}"
|
|
4997
5217
|
data-duration="${durationMs}"
|
|
4998
|
-
data-
|
|
5218
|
+
data-word-duration="${wordDuration}"
|
|
5219
|
+
data-syllable-index="${sylIdx}"
|
|
4999
5220
|
data-wipe-ratio="1"
|
|
5000
5221
|
>${syllableContent}</span
|
|
5001
5222
|
>
|
|
5002
5223
|
${romanizedText}
|
|
5003
|
-
</span
|
|
5004
|
-
|
|
5005
|
-
}
|
|
5006
|
-
|
|
5007
|
-
// Multi-syllable group (part=true): render all syllables inside one lyrics-word
|
|
5008
|
-
return html`<span
|
|
5009
|
-
class="lyrics-word ${isGrowable ? 'growable' : ''} allow-break"
|
|
5010
|
-
>
|
|
5011
|
-
${group.map(
|
|
5012
|
-
(syllable, sylIdx) => html`
|
|
5013
|
-
<span class="lyrics-syllable-wrap">
|
|
5014
|
-
<span
|
|
5015
|
-
class="lyrics-syllable ${groupLineSynced
|
|
5016
|
-
? 'line-synced'
|
|
5017
|
-
: ''}"
|
|
5018
|
-
data-start-time="${syllable.timestamp}"
|
|
5019
|
-
data-end-time="${syllable.endtime}"
|
|
5020
|
-
data-duration="${syllable.endtime - syllable.timestamp}"
|
|
5021
|
-
data-syllable-index="${sylIdx}"
|
|
5022
|
-
data-wipe-ratio="1"
|
|
5023
|
-
>${syllable.text}</span
|
|
5024
|
-
>
|
|
5025
|
-
${this.showRomanization &&
|
|
5026
|
-
syllable.romanizedText &&
|
|
5027
|
-
syllable.romanizedText.trim() !== syllable.text.trim()
|
|
5028
|
-
? html`<span
|
|
5029
|
-
class="lyrics-syllable transliteration ${groupLineSynced
|
|
5030
|
-
? 'line-synced'
|
|
5031
|
-
: ''}"
|
|
5032
|
-
data-start-time="${syllable.timestamp}"
|
|
5033
|
-
data-end-time="${syllable.endtime}"
|
|
5034
|
-
data-duration="${syllable.endtime -
|
|
5035
|
-
syllable.timestamp}"
|
|
5036
|
-
data-syllable-index="0"
|
|
5037
|
-
data-wipe-ratio="1"
|
|
5038
|
-
>${syllable.romanizedText}</span
|
|
5039
|
-
>`
|
|
5040
|
-
: ''}
|
|
5041
|
-
</span>
|
|
5042
|
-
`,
|
|
5043
|
-
)}
|
|
5224
|
+
</span>`;
|
|
5225
|
+
})}
|
|
5044
5226
|
</span>`;
|
|
5045
5227
|
})}
|
|
5046
5228
|
</p>`;
|
|
@@ -5076,8 +5258,10 @@ export class AmLyrics extends LitElement {
|
|
|
5076
5258
|
let maybeInstrumentalBlock: unknown = null;
|
|
5077
5259
|
const gapForLine = gapByIndex.get(lineIndex);
|
|
5078
5260
|
if (gapForLine) {
|
|
5261
|
+
const gapDuration = gapForLine.gapEnd - gapForLine.gapStart;
|
|
5079
5262
|
// Calculate dot timing for fill-up animation (3 dots)
|
|
5080
|
-
const dotDuration =
|
|
5263
|
+
const dotDuration = gapDuration / 3;
|
|
5264
|
+
const gapLoopDelay = AmLyrics.getGapLoopDelay(gapDuration);
|
|
5081
5265
|
|
|
5082
5266
|
// Gap starts without 'active' — _onTimeChanged toggles it imperatively
|
|
5083
5267
|
maybeInstrumentalBlock = html`<div
|
|
@@ -5085,6 +5269,7 @@ export class AmLyrics extends LitElement {
|
|
|
5085
5269
|
class="lyrics-line lyrics-gap"
|
|
5086
5270
|
data-start-time="${gapForLine.gapStart}"
|
|
5087
5271
|
data-end-time="${gapForLine.gapEnd}"
|
|
5272
|
+
style="--gap-pulse-duration: ${GAP_PULSE_DURATION_MS}ms; --gap-loop-delay: -${gapLoopDelay}ms; --gap-exit-duration: ${GAP_EXIT_LEAD_MS}ms; --gap-exit-scale: ${GAP_MIN_SCALE};"
|
|
5088
5273
|
>
|
|
5089
5274
|
<div class="lyrics-line-container">
|
|
5090
5275
|
<p class="main-vocal-container">
|