@uimaxbai/am-lyrics 1.4.1 → 1.5.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/dist/src/AmLyrics.d.ts +27 -2
- package/dist/src/AmLyrics.d.ts.map +1 -1
- package/dist/src/am-lyrics.js +963 -351
- package/dist/src/am-lyrics.js.map +1 -1
- package/dist/src/react.js +963 -351
- package/dist/src/react.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/AmLyrics.ts +1130 -435
package/src/AmLyrics.ts
CHANGED
|
@@ -2,14 +2,11 @@ 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.5.0';
|
|
6
6
|
const INSTRUMENTAL_THRESHOLD_MS = 7000; // Show dots for gaps >= 7s
|
|
7
7
|
const FETCH_TIMEOUT_MS = 8000; // Timeout for all lyrics fetch requests
|
|
8
8
|
const SEEK_THRESHOLD_MS = 500;
|
|
9
|
-
const
|
|
10
|
-
const PRE_SCROLL_LEAD_SHORT_MS = 350;
|
|
11
|
-
const SCROLL_ANIMATION_DURATION_MS = 280;
|
|
12
|
-
const SCROLL_DELAY_INCREMENT_MS = 24;
|
|
9
|
+
const SCROLL_ANIMATION_DURATION_MS = 350;
|
|
13
10
|
const GAP_PULSE_DURATION_MS = 4000;
|
|
14
11
|
const GAP_PULSE_CYCLE_MS = GAP_PULSE_DURATION_MS * 2;
|
|
15
12
|
const GAP_EXIT_LEAD_MS = 600;
|
|
@@ -168,6 +165,7 @@ export class AmLyrics extends LitElement {
|
|
|
168
165
|
-webkit-overflow-scrolling: touch;
|
|
169
166
|
box-sizing: border-box;
|
|
170
167
|
scrollbar-width: none;
|
|
168
|
+
overflow-anchor: none;
|
|
171
169
|
}
|
|
172
170
|
|
|
173
171
|
.lyrics-container::-webkit-scrollbar {
|
|
@@ -188,9 +186,16 @@ export class AmLyrics extends LitElement {
|
|
|
188
186
|
}
|
|
189
187
|
|
|
190
188
|
.lyrics-line.scroll-animate {
|
|
191
|
-
|
|
189
|
+
/* Preserve the graceful fade duration; the keyframe handles the
|
|
190
|
+
transform, so we only need to keep opacity/filter transitions
|
|
191
|
+
alive without !important overriding the base rule. */
|
|
192
|
+
transition:
|
|
193
|
+
opacity 0.7s ease,
|
|
194
|
+
filter 0.7s ease,
|
|
195
|
+
transform 0.4s cubic-bezier(0.41, 0, 0.12, 0.99)
|
|
196
|
+
var(--lyrics-line-delay, 0ms);
|
|
192
197
|
animation-name: lyrics-scroll;
|
|
193
|
-
animation-duration: var(--scroll-duration,
|
|
198
|
+
animation-duration: var(--scroll-duration, 400ms);
|
|
194
199
|
animation-timing-function: cubic-bezier(0.41, 0, 0.12, 0.99);
|
|
195
200
|
animation-fill-mode: both;
|
|
196
201
|
animation-delay: var(--lyrics-line-delay, 0ms);
|
|
@@ -211,12 +216,15 @@ export class AmLyrics extends LitElement {
|
|
|
211
216
|
font-size: var(--lyplus-font-size-base);
|
|
212
217
|
cursor: pointer;
|
|
213
218
|
transform-origin: left;
|
|
219
|
+
/* Graceful 0.7 s fade so the line stays mostly bright while the
|
|
220
|
+
0.4 s scroll animation runs, then settles into the inactive state. */
|
|
214
221
|
transition:
|
|
215
|
-
opacity 0.
|
|
222
|
+
opacity 0.7s ease,
|
|
216
223
|
transform 0.4s cubic-bezier(0.41, 0, 0.12, 0.99)
|
|
217
224
|
var(--lyrics-line-delay, 0ms),
|
|
218
|
-
filter 0.
|
|
225
|
+
filter 0.7s ease;
|
|
219
226
|
content-visibility: auto;
|
|
227
|
+
contain: layout style;
|
|
220
228
|
text-rendering: optimizeLegibility;
|
|
221
229
|
}
|
|
222
230
|
|
|
@@ -228,7 +236,7 @@ export class AmLyrics extends LitElement {
|
|
|
228
236
|
.lyrics-line-container {
|
|
229
237
|
overflow-wrap: break-word;
|
|
230
238
|
transform-origin: left;
|
|
231
|
-
transform:
|
|
239
|
+
transform: translateZ(0);
|
|
232
240
|
transition:
|
|
233
241
|
transform 0.7s ease,
|
|
234
242
|
background-color 0.7s,
|
|
@@ -237,7 +245,7 @@ export class AmLyrics extends LitElement {
|
|
|
237
245
|
|
|
238
246
|
.lyrics-line.active .lyrics-line-container,
|
|
239
247
|
.lyrics-line.pre-active .lyrics-line-container {
|
|
240
|
-
transform:
|
|
248
|
+
transform: translateZ(0);
|
|
241
249
|
transition:
|
|
242
250
|
transform 0.5s ease,
|
|
243
251
|
background-color 0.18s,
|
|
@@ -251,31 +259,50 @@ export class AmLyrics extends LitElement {
|
|
|
251
259
|
|
|
252
260
|
.background-vocal-container {
|
|
253
261
|
max-height: 0;
|
|
254
|
-
padding-top: 0;
|
|
255
|
-
transform: translateY(-0.5em) scale(0.95);
|
|
256
262
|
overflow: visible;
|
|
257
263
|
opacity: 0;
|
|
258
264
|
font-size: var(--lyplus-font-size-subtext);
|
|
265
|
+
line-height: 1.15;
|
|
266
|
+
color: color-mix(in srgb, var(--lyplus-text-secondary) 80%, transparent);
|
|
267
|
+
/* Fast exit (0.25 s) so bg vocals collapse quickly and feel snappy.
|
|
268
|
+
The scroll takes ~0.4 s; finishing the collapse first prevents
|
|
269
|
+
the container from trailing behind the motion. */
|
|
259
270
|
transition:
|
|
260
|
-
max-height
|
|
261
|
-
opacity
|
|
262
|
-
transform 450ms cubic-bezier(0.33, 1, 0.68, 1),
|
|
263
|
-
padding 450ms cubic-bezier(0.33, 1, 0.68, 1);
|
|
271
|
+
max-height 250ms cubic-bezier(0.41, 0, 0.12, 0.99),
|
|
272
|
+
opacity 250ms cubic-bezier(0.41, 0, 0.12, 0.99);
|
|
264
273
|
margin: 0;
|
|
274
|
+
pointer-events: none;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.background-vocal-wrap {
|
|
278
|
+
display: block;
|
|
279
|
+
padding-top: 0;
|
|
280
|
+
padding-bottom: 0;
|
|
281
|
+
transition: padding-top 250ms cubic-bezier(0.41, 0, 0.12, 0.99);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.lyrics-line.singer-right .background-vocal-container,
|
|
285
|
+
.lyrics-line.rtl-text .background-vocal-container {
|
|
286
|
+
margin-left: auto;
|
|
287
|
+
margin-right: 0;
|
|
265
288
|
}
|
|
266
289
|
|
|
267
|
-
.
|
|
268
|
-
|
|
290
|
+
/* Background vocals expand only when .bg-expanded is present.
|
|
291
|
+
This is separate from .active so bg vocals can collapse immediately
|
|
292
|
+
while .active stays to keep text white until the scroll passes. */
|
|
293
|
+
.lyrics-line.bg-expanded .background-vocal-container {
|
|
269
294
|
max-height: 4em;
|
|
270
295
|
opacity: 1;
|
|
271
|
-
|
|
272
|
-
transform: translateY(0) scale(1);
|
|
296
|
+
/* Slower entry (0.6 s) so bg vocals expand smoothly. */
|
|
273
297
|
transition:
|
|
274
|
-
max-height
|
|
275
|
-
opacity
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
298
|
+
max-height 0.6s ease,
|
|
299
|
+
opacity 0.6s ease;
|
|
300
|
+
will-change: max-height, opacity;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.lyrics-line.bg-expanded .background-vocal-wrap {
|
|
304
|
+
padding-top: 0.26em;
|
|
305
|
+
transition: padding-top 0.6s ease;
|
|
279
306
|
}
|
|
280
307
|
|
|
281
308
|
/* --- Line States & Modifiers --- */
|
|
@@ -288,6 +315,16 @@ export class AmLyrics extends LitElement {
|
|
|
288
315
|
opacity: 1;
|
|
289
316
|
}
|
|
290
317
|
|
|
318
|
+
.lyrics-line.persist-highlight {
|
|
319
|
+
filter: none !important;
|
|
320
|
+
opacity: 1;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.lyrics-line.persist-highlight .lyrics-syllable.finished,
|
|
324
|
+
.lyrics-line.persist-highlight .lyrics-syllable.finished span.char {
|
|
325
|
+
transition: none !important;
|
|
326
|
+
}
|
|
327
|
+
|
|
291
328
|
.lyrics-line.singer-right {
|
|
292
329
|
text-align: end;
|
|
293
330
|
}
|
|
@@ -352,22 +389,32 @@ export class AmLyrics extends LitElement {
|
|
|
352
389
|
|
|
353
390
|
/* --- Blur Effect for Inactive Lines --- */
|
|
354
391
|
.lyrics-container.blur-inactive-enabled:not(.not-focused)
|
|
355
|
-
.lyrics-line:not(.active):not(.pre-active):not(.lyrics-gap)
|
|
392
|
+
.lyrics-line:not(.active):not(.pre-active):not(.lyrics-gap):not(
|
|
393
|
+
.persist-highlight
|
|
394
|
+
) {
|
|
356
395
|
filter: blur(var(--lyplus-blur-amount));
|
|
357
396
|
}
|
|
358
397
|
|
|
398
|
+
/* Viewport Virtualization: Strip expensive filters and animations from
|
|
399
|
+
offscreen lines. IntersectionObserver toggles this class. */
|
|
400
|
+
.lyrics-line.far-line {
|
|
401
|
+
filter: none !important;
|
|
402
|
+
will-change: auto !important;
|
|
403
|
+
animation: none !important;
|
|
404
|
+
}
|
|
405
|
+
|
|
359
406
|
.lyrics-container.blur-inactive-enabled:not(.not-focused)
|
|
360
407
|
.lyrics-line.post-active-line:not(.lyrics-gap):not(.active):not(
|
|
361
408
|
.pre-active
|
|
362
|
-
),
|
|
409
|
+
):not(.persist-highlight),
|
|
363
410
|
.lyrics-container.blur-inactive-enabled:not(.not-focused)
|
|
364
411
|
.lyrics-line.next-active-line:not(.lyrics-gap):not(.active):not(
|
|
365
412
|
.pre-active
|
|
366
|
-
),
|
|
413
|
+
):not(.persist-highlight),
|
|
367
414
|
.lyrics-container.blur-inactive-enabled:not(.not-focused)
|
|
368
415
|
.lyrics-line.lyrics-activest:not(.active):not(.lyrics-gap):not(
|
|
369
416
|
.pre-active
|
|
370
|
-
) {
|
|
417
|
+
):not(.persist-highlight) {
|
|
371
418
|
filter: blur(var(--lyplus-blur-amount-near));
|
|
372
419
|
}
|
|
373
420
|
|
|
@@ -397,6 +444,17 @@ export class AmLyrics extends LitElement {
|
|
|
397
444
|
display: inline;
|
|
398
445
|
}
|
|
399
446
|
|
|
447
|
+
.lyrics-word.char-rise {
|
|
448
|
+
display: inline-block;
|
|
449
|
+
vertical-align: baseline;
|
|
450
|
+
white-space: nowrap;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.lyrics-word.char-rise.allow-break {
|
|
454
|
+
display: inline;
|
|
455
|
+
white-space: normal;
|
|
456
|
+
}
|
|
457
|
+
|
|
400
458
|
.lyrics-syllable-wrap {
|
|
401
459
|
display: inline;
|
|
402
460
|
}
|
|
@@ -426,17 +484,19 @@ export class AmLyrics extends LitElement {
|
|
|
426
484
|
/* --- Syllable States --- */
|
|
427
485
|
.lyrics-syllable.finished {
|
|
428
486
|
background-color: var(--lyplus-text-primary);
|
|
429
|
-
transition: transform 1s
|
|
487
|
+
/* Unified transition: transform keeps its 1s glow decay, while
|
|
488
|
+
background-color and color fade at 0.7s so everything dims
|
|
489
|
+
together when the line becomes inactive. */
|
|
490
|
+
transition:
|
|
491
|
+
transform 1s ease,
|
|
492
|
+
background-color 0.7s ease,
|
|
493
|
+
color 0.7s ease;
|
|
430
494
|
}
|
|
431
495
|
|
|
432
496
|
.lyrics-syllable.finished.has-chars {
|
|
433
497
|
background-color: transparent;
|
|
434
498
|
}
|
|
435
499
|
|
|
436
|
-
.lyrics-line:not(.active) .lyrics-syllable.finished {
|
|
437
|
-
transition: color 0.18s;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
500
|
.lyrics-line.active:not(.lyrics-gap) .lyrics-syllable {
|
|
441
501
|
transition:
|
|
442
502
|
transform 1s ease,
|
|
@@ -486,7 +546,60 @@ export class AmLyrics extends LitElement {
|
|
|
486
546
|
);
|
|
487
547
|
background-position:
|
|
488
548
|
calc(100% + 0.5em) 0%,
|
|
489
|
-
right
|
|
549
|
+
right 0%;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/* Background vocals: muted gray wipe instead of white.
|
|
553
|
+
Must match specificity of the main .active .highlight rule (0,3,1). */
|
|
554
|
+
.lyrics-line.active
|
|
555
|
+
.background-vocal-container
|
|
556
|
+
.lyrics-syllable.highlight.no-chars,
|
|
557
|
+
.lyrics-line.active
|
|
558
|
+
.background-vocal-container
|
|
559
|
+
.lyrics-syllable.pre-highlight.no-chars,
|
|
560
|
+
.lyrics-line.pre-active
|
|
561
|
+
.background-vocal-container
|
|
562
|
+
.lyrics-syllable.highlight.no-chars,
|
|
563
|
+
.lyrics-line.pre-active
|
|
564
|
+
.background-vocal-container
|
|
565
|
+
.lyrics-syllable.pre-highlight.no-chars {
|
|
566
|
+
background-image:
|
|
567
|
+
linear-gradient(
|
|
568
|
+
90deg,
|
|
569
|
+
#ffffff00 0%,
|
|
570
|
+
color-mix(in srgb, var(--lyplus-text-primary, #fff) 50%, #888888) 50%,
|
|
571
|
+
#0000 100%
|
|
572
|
+
),
|
|
573
|
+
linear-gradient(
|
|
574
|
+
90deg,
|
|
575
|
+
color-mix(in srgb, var(--lyplus-text-primary, #fff) 50%, #888888) 100%,
|
|
576
|
+
#0000 100%
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.lyrics-line.active
|
|
581
|
+
.background-vocal-container
|
|
582
|
+
.lyrics-syllable.highlight.rtl-text,
|
|
583
|
+
.lyrics-line.active
|
|
584
|
+
.background-vocal-container
|
|
585
|
+
.lyrics-syllable.pre-highlight.rtl-text,
|
|
586
|
+
.lyrics-line.pre-active
|
|
587
|
+
.background-vocal-container
|
|
588
|
+
.lyrics-syllable.highlight.rtl-text,
|
|
589
|
+
.lyrics-line.pre-active
|
|
590
|
+
.background-vocal-container
|
|
591
|
+
.lyrics-syllable.pre-highlight.rtl-text {
|
|
592
|
+
background-image:
|
|
593
|
+
linear-gradient(
|
|
594
|
+
-90deg,
|
|
595
|
+
color-mix(in srgb, var(--lyplus-text-primary) 50%, #888888) 0%,
|
|
596
|
+
transparent 100%
|
|
597
|
+
),
|
|
598
|
+
linear-gradient(
|
|
599
|
+
-90deg,
|
|
600
|
+
color-mix(in srgb, var(--lyplus-text-primary) 50%, #888888) 100%,
|
|
601
|
+
transparent 100%
|
|
602
|
+
);
|
|
490
603
|
}
|
|
491
604
|
|
|
492
605
|
/* Non-growable words float up with a gentle curve */
|
|
@@ -494,18 +607,88 @@ export class AmLyrics extends LitElement {
|
|
|
494
607
|
.lyrics-word:not(.growable)
|
|
495
608
|
.lyrics-syllable.highlight {
|
|
496
609
|
transform: translateY(-3.5%);
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.lyrics-line.persist-highlight:not(.lyrics-gap)
|
|
613
|
+
.lyrics-word:not(.growable)
|
|
614
|
+
.lyrics-syllable.finished {
|
|
615
|
+
transform: translateY(-3.5%);
|
|
501
616
|
}
|
|
502
617
|
|
|
503
618
|
.lyrics-word.growable .lyrics-syllable.cleanup .char {
|
|
504
619
|
transform: translateY(-3.5%);
|
|
505
620
|
}
|
|
506
621
|
|
|
507
|
-
.lyrics-
|
|
508
|
-
|
|
622
|
+
.lyrics-word.char-rise .lyrics-syllable.cleanup .char {
|
|
623
|
+
transform: translateY(-3.5%);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.lyrics-line.persist-highlight
|
|
627
|
+
.lyrics-word.growable
|
|
628
|
+
.lyrics-syllable.finished
|
|
629
|
+
.char,
|
|
630
|
+
.lyrics-line.persist-highlight
|
|
631
|
+
.lyrics-word.char-rise
|
|
632
|
+
.lyrics-syllable.finished
|
|
633
|
+
.char {
|
|
634
|
+
transform: translateY(-3.5%);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/* Background vocal overrides — placed AFTER main rules so they win
|
|
638
|
+
on equal specificity. */
|
|
639
|
+
.background-vocal-container .lyrics-syllable {
|
|
640
|
+
background-color: color-mix(
|
|
641
|
+
in srgb,
|
|
642
|
+
var(--lyplus-text-secondary) 50%,
|
|
643
|
+
#888888
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.lyrics-line.active:not(.lyrics-gap)
|
|
648
|
+
.background-vocal-container
|
|
649
|
+
.lyrics-syllable.finished,
|
|
650
|
+
.lyrics-line.pre-active
|
|
651
|
+
.background-vocal-container
|
|
652
|
+
.lyrics-syllable.finished {
|
|
653
|
+
background-color: color-mix(
|
|
654
|
+
in srgb,
|
|
655
|
+
var(--lyplus-text-primary) 50%,
|
|
656
|
+
#888888
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.background-vocal-container .lyrics-syllable.line-synced {
|
|
661
|
+
color: color-mix(
|
|
662
|
+
in srgb,
|
|
663
|
+
var(--lyplus-text-secondary) 50%,
|
|
664
|
+
#888888
|
|
665
|
+
) !important;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.lyrics-line.active:not(.lyrics-gap)
|
|
669
|
+
.background-vocal-container
|
|
670
|
+
.lyrics-syllable.line-synced,
|
|
671
|
+
.lyrics-line.pre-active
|
|
672
|
+
.background-vocal-container
|
|
673
|
+
.lyrics-syllable.line-synced {
|
|
674
|
+
color: color-mix(
|
|
675
|
+
in srgb,
|
|
676
|
+
var(--lyplus-text-primary) 50%,
|
|
677
|
+
#888888
|
|
678
|
+
) !important;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.lyrics-line.active:not(.lyrics-gap)
|
|
682
|
+
.background-vocal-container
|
|
683
|
+
.lyrics-syllable.line-synced.finished,
|
|
684
|
+
.lyrics-line.pre-active
|
|
685
|
+
.background-vocal-container
|
|
686
|
+
.lyrics-syllable.line-synced.finished {
|
|
687
|
+
color: color-mix(
|
|
688
|
+
in srgb,
|
|
689
|
+
var(--lyplus-text-primary) 50%,
|
|
690
|
+
#888888
|
|
691
|
+
) !important;
|
|
509
692
|
}
|
|
510
693
|
|
|
511
694
|
.lyrics-syllable.pre-highlight {
|
|
@@ -549,8 +732,11 @@ export class AmLyrics extends LitElement {
|
|
|
549
732
|
}
|
|
550
733
|
|
|
551
734
|
.lyrics-syllable.finished span.char {
|
|
552
|
-
transition: color 0.18s;
|
|
553
735
|
background-color: var(--lyplus-text-primary);
|
|
736
|
+
transition:
|
|
737
|
+
color 0.7s,
|
|
738
|
+
background-color 0.7s,
|
|
739
|
+
transform 0.7s ease;
|
|
554
740
|
}
|
|
555
741
|
|
|
556
742
|
/* Active char spans: structural only, wipe animation sets gradient */
|
|
@@ -741,7 +927,7 @@ export class AmLyrics extends LitElement {
|
|
|
741
927
|
position: relative;
|
|
742
928
|
box-sizing: border-box;
|
|
743
929
|
font-weight: normal;
|
|
744
|
-
transform: translateY(var(--lyrics-scroll-offset, 0px))
|
|
930
|
+
transform: translateY(var(--lyrics-scroll-offset, 0px));
|
|
745
931
|
transition:
|
|
746
932
|
opacity 0.3s ease,
|
|
747
933
|
transform 0.6s cubic-bezier(0.23, 1, 0.32, 1)
|
|
@@ -752,7 +938,7 @@ export class AmLyrics extends LitElement {
|
|
|
752
938
|
.lyrics-plus-empty {
|
|
753
939
|
display: block;
|
|
754
940
|
height: 100vh;
|
|
755
|
-
transform: translateY(var(--lyrics-scroll-offset, 0px))
|
|
941
|
+
transform: translateY(var(--lyrics-scroll-offset, 0px));
|
|
756
942
|
}
|
|
757
943
|
|
|
758
944
|
.lyrics-footer {
|
|
@@ -761,8 +947,8 @@ export class AmLyrics extends LitElement {
|
|
|
761
947
|
align-items: center;
|
|
762
948
|
flex-wrap: wrap;
|
|
763
949
|
text-align: left;
|
|
764
|
-
font-size:
|
|
765
|
-
color:
|
|
950
|
+
font-size: calc(var(--lyplus-font-size-base) * 0.5);
|
|
951
|
+
color: var(--lyplus-text-secondary);
|
|
766
952
|
padding: 20px 0 50vh 0;
|
|
767
953
|
margin-top: 10px;
|
|
768
954
|
font-weight: 400;
|
|
@@ -775,9 +961,9 @@ export class AmLyrics extends LitElement {
|
|
|
775
961
|
}
|
|
776
962
|
|
|
777
963
|
.lyrics-footer.lyrics-line {
|
|
778
|
-
font-size:
|
|
964
|
+
font-size: calc(var(--lyplus-font-size-base) * 0.5);
|
|
779
965
|
padding: 20px var(--lyplus-padding-line) 50vh var(--lyplus-padding-line);
|
|
780
|
-
|
|
966
|
+
margin-top: 0;
|
|
781
967
|
}
|
|
782
968
|
|
|
783
969
|
.lyrics-footer.active {
|
|
@@ -1065,7 +1251,7 @@ export class AmLyrics extends LitElement {
|
|
|
1065
1251
|
0% 100%;
|
|
1066
1252
|
background-position:
|
|
1067
1253
|
calc(100% + 0.375em) 0%,
|
|
1068
|
-
calc(100% + 0.36em)
|
|
1254
|
+
calc(100% + 0.36em) 0%;
|
|
1069
1255
|
}
|
|
1070
1256
|
to {
|
|
1071
1257
|
background-size:
|
|
@@ -1073,7 +1259,7 @@ export class AmLyrics extends LitElement {
|
|
|
1073
1259
|
100% 100%;
|
|
1074
1260
|
background-position:
|
|
1075
1261
|
-0.75em 0%,
|
|
1076
|
-
right
|
|
1262
|
+
right 0%;
|
|
1077
1263
|
}
|
|
1078
1264
|
}
|
|
1079
1265
|
|
|
@@ -1084,7 +1270,7 @@ export class AmLyrics extends LitElement {
|
|
|
1084
1270
|
0% 100%;
|
|
1085
1271
|
background-position:
|
|
1086
1272
|
calc(100% + 0.75em) 0%,
|
|
1087
|
-
calc(100% + 0.5em)
|
|
1273
|
+
calc(100% + 0.5em) 0%;
|
|
1088
1274
|
}
|
|
1089
1275
|
100% {
|
|
1090
1276
|
background-size:
|
|
@@ -1092,7 +1278,7 @@ export class AmLyrics extends LitElement {
|
|
|
1092
1278
|
100% 100%;
|
|
1093
1279
|
background-position:
|
|
1094
1280
|
-0.75em 0%,
|
|
1095
|
-
right
|
|
1281
|
+
right 0%;
|
|
1096
1282
|
}
|
|
1097
1283
|
}
|
|
1098
1284
|
|
|
@@ -1122,7 +1308,7 @@ export class AmLyrics extends LitElement {
|
|
|
1122
1308
|
0% 100%;
|
|
1123
1309
|
background-position:
|
|
1124
1310
|
calc(100% + 0.75em) 0%,
|
|
1125
|
-
right
|
|
1311
|
+
right 0%;
|
|
1126
1312
|
}
|
|
1127
1313
|
to {
|
|
1128
1314
|
background-size:
|
|
@@ -1130,7 +1316,7 @@ export class AmLyrics extends LitElement {
|
|
|
1130
1316
|
0% 100%;
|
|
1131
1317
|
background-position:
|
|
1132
1318
|
calc(100% + 0.375em) 0%,
|
|
1133
|
-
right
|
|
1319
|
+
right 0%;
|
|
1134
1320
|
}
|
|
1135
1321
|
}
|
|
1136
1322
|
|
|
@@ -1196,7 +1382,7 @@ export class AmLyrics extends LitElement {
|
|
|
1196
1382
|
}
|
|
1197
1383
|
|
|
1198
1384
|
/* Character grow animation — translate3d+scale3d for smooth transform,
|
|
1199
|
-
drop-shadow for glow
|
|
1385
|
+
drop-shadow for glow */
|
|
1200
1386
|
@keyframes grow-dynamic {
|
|
1201
1387
|
0% {
|
|
1202
1388
|
transform: translate3d(0, 0, 0) scale3d(1, 1, 1);
|
|
@@ -1233,6 +1419,16 @@ export class AmLyrics extends LitElement {
|
|
|
1233
1419
|
}
|
|
1234
1420
|
}
|
|
1235
1421
|
|
|
1422
|
+
@keyframes rise-char {
|
|
1423
|
+
0% {
|
|
1424
|
+
transform: translate3d(0, 0, 0);
|
|
1425
|
+
}
|
|
1426
|
+
65%,
|
|
1427
|
+
100% {
|
|
1428
|
+
transform: translate3d(0, var(--char-rise-y, -1.12px), 0);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1236
1432
|
@keyframes grow-static {
|
|
1237
1433
|
0%,
|
|
1238
1434
|
100% {
|
|
@@ -1479,10 +1675,10 @@ export class AmLyrics extends LitElement {
|
|
|
1479
1675
|
// Stop all running animations and clear highlights immediately
|
|
1480
1676
|
if (this.lyricsContainer) {
|
|
1481
1677
|
const activeLines = this.lyricsContainer.querySelectorAll(
|
|
1482
|
-
'.lyrics-line.active, .lyrics-line.pre-active',
|
|
1678
|
+
'.lyrics-line.active, .lyrics-line.pre-active, .lyrics-line.bg-expanded',
|
|
1483
1679
|
);
|
|
1484
1680
|
activeLines.forEach(line => {
|
|
1485
|
-
line.classList.remove('active', 'pre-active');
|
|
1681
|
+
line.classList.remove('active', 'pre-active', 'bg-expanded');
|
|
1486
1682
|
AmLyrics.resetSyllables(line as HTMLElement);
|
|
1487
1683
|
});
|
|
1488
1684
|
|
|
@@ -1589,6 +1785,9 @@ export class AmLyrics extends LitElement {
|
|
|
1589
1785
|
// Cached DOM elements for animation updates
|
|
1590
1786
|
private cachedLyricsLines: HTMLElement[] = [];
|
|
1591
1787
|
|
|
1788
|
+
// Cached line elements array for scroll/position queries
|
|
1789
|
+
private cachedLineArray: HTMLElement[] = [];
|
|
1790
|
+
|
|
1592
1791
|
// Cached line and gap element maps for fast lookup
|
|
1593
1792
|
private lineElementCache = new Map<number, HTMLElement>();
|
|
1594
1793
|
|
|
@@ -1609,6 +1808,7 @@ export class AmLyrics extends LitElement {
|
|
|
1609
1808
|
wordGroups: Syllable[][];
|
|
1610
1809
|
groupGrowable: boolean[];
|
|
1611
1810
|
groupGlowing: boolean[];
|
|
1811
|
+
groupCharRise: boolean[];
|
|
1612
1812
|
vwFullText: string[];
|
|
1613
1813
|
vwFullDuration: number[];
|
|
1614
1814
|
vwCharOffset: number[];
|
|
@@ -1634,10 +1834,10 @@ export class AmLyrics extends LitElement {
|
|
|
1634
1834
|
|
|
1635
1835
|
private animatingLines: HTMLElement[] = [];
|
|
1636
1836
|
|
|
1637
|
-
private scrollUnlockTimeout?: ReturnType<typeof setTimeout>;
|
|
1638
|
-
|
|
1639
1837
|
private scrollAnimationTimeout?: ReturnType<typeof setTimeout>;
|
|
1640
1838
|
|
|
1839
|
+
private scrollUnlockTimeout?: ReturnType<typeof setTimeout>;
|
|
1840
|
+
|
|
1641
1841
|
// AbortController for cancelling in-flight lyrics fetches
|
|
1642
1842
|
private fetchAbortController?: AbortController;
|
|
1643
1843
|
|
|
@@ -1646,6 +1846,12 @@ export class AmLyrics extends LitElement {
|
|
|
1646
1846
|
|
|
1647
1847
|
private visibleLineIds: Set<string> = new Set();
|
|
1648
1848
|
|
|
1849
|
+
// IntersectionObserver for viewport virtualization
|
|
1850
|
+
private visibilityObserver?: IntersectionObserver;
|
|
1851
|
+
|
|
1852
|
+
// Cached scroll padding top value
|
|
1853
|
+
private cachedScrollPaddingTop: number | null = null;
|
|
1854
|
+
|
|
1649
1855
|
// Cached element tracking to avoid repeated querySelectorAll calls
|
|
1650
1856
|
private preActiveLineElements: HTMLElement[] = [];
|
|
1651
1857
|
|
|
@@ -1677,14 +1883,14 @@ export class AmLyrics extends LitElement {
|
|
|
1677
1883
|
clearTimeout(this.clickSeekTimeout);
|
|
1678
1884
|
this.clickSeekTimeout = undefined;
|
|
1679
1885
|
}
|
|
1680
|
-
if (this.scrollUnlockTimeout) {
|
|
1681
|
-
clearTimeout(this.scrollUnlockTimeout);
|
|
1682
|
-
this.scrollUnlockTimeout = undefined;
|
|
1683
|
-
}
|
|
1684
1886
|
if (this.scrollAnimationTimeout) {
|
|
1685
1887
|
clearTimeout(this.scrollAnimationTimeout);
|
|
1686
1888
|
this.scrollAnimationTimeout = undefined;
|
|
1687
1889
|
}
|
|
1890
|
+
if (this.scrollUnlockTimeout) {
|
|
1891
|
+
clearTimeout(this.scrollUnlockTimeout);
|
|
1892
|
+
this.scrollUnlockTimeout = undefined;
|
|
1893
|
+
}
|
|
1688
1894
|
// Cancel any in-flight fetch requests
|
|
1689
1895
|
this.fetchAbortController?.abort();
|
|
1690
1896
|
this.fetchAbortController = undefined;
|
|
@@ -1702,6 +1908,8 @@ export class AmLyrics extends LitElement {
|
|
|
1702
1908
|
this.preActiveLineElements = [];
|
|
1703
1909
|
this.positionedLineElements = [];
|
|
1704
1910
|
this.activeGapLineElements = [];
|
|
1911
|
+
this.visibilityObserver?.disconnect();
|
|
1912
|
+
this.visibilityObserver = undefined;
|
|
1705
1913
|
}
|
|
1706
1914
|
|
|
1707
1915
|
private async fetchLyrics() {
|
|
@@ -1736,15 +1944,37 @@ export class AmLyrics extends LitElement {
|
|
|
1736
1944
|
const title = resolvedMetadata.metadata.title?.trim() || '';
|
|
1737
1945
|
const artist = resolvedMetadata.metadata.artist?.trim() || '';
|
|
1738
1946
|
|
|
1739
|
-
const
|
|
1947
|
+
const biniResult = await AmLyrics.fetchLyricsFromBiniLyrics(
|
|
1740
1948
|
title,
|
|
1741
1949
|
artist,
|
|
1742
1950
|
resolvedMetadata.catalogIsrc,
|
|
1743
1951
|
resolvedMetadata.metadata,
|
|
1744
1952
|
);
|
|
1953
|
+
if (biniResult && biniResult.lines.length > 0) {
|
|
1954
|
+
collectedSources.push(biniResult);
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
if (collectedSources.length === 0) {
|
|
1958
|
+
const unisonResult = await AmLyrics.fetchLyricsFromUnison(
|
|
1959
|
+
resolvedMetadata.metadata,
|
|
1960
|
+
);
|
|
1961
|
+
if (unisonResult && unisonResult.lines.length > 0) {
|
|
1962
|
+
collectedSources.push(unisonResult);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
if (collectedSources.length === 0) {
|
|
1967
|
+
const youLyResults = await AmLyrics.fetchLyricsFromYouLyPlus(
|
|
1968
|
+
title,
|
|
1969
|
+
artist,
|
|
1970
|
+
resolvedMetadata.catalogIsrc,
|
|
1971
|
+
resolvedMetadata.metadata,
|
|
1972
|
+
true,
|
|
1973
|
+
);
|
|
1745
1974
|
|
|
1746
|
-
|
|
1747
|
-
|
|
1975
|
+
if (youLyResults && youLyResults.length > 0) {
|
|
1976
|
+
collectedSources.push(...youLyResults);
|
|
1977
|
+
}
|
|
1748
1978
|
}
|
|
1749
1979
|
}
|
|
1750
1980
|
|
|
@@ -1853,24 +2083,30 @@ export class AmLyrics extends LitElement {
|
|
|
1853
2083
|
const isQQ = lower.includes('qq') || lower.includes('lyricsplus');
|
|
1854
2084
|
|
|
1855
2085
|
if (lower.includes('apple') && hasWordSync) return 1;
|
|
1856
|
-
if (
|
|
1857
|
-
if (lower.includes('
|
|
1858
|
-
if (
|
|
1859
|
-
if (hasWordSync) return 5;
|
|
1860
|
-
|
|
1861
|
-
if (
|
|
1862
|
-
|
|
1863
|
-
if (lower.includes('
|
|
1864
|
-
if (lower.includes('
|
|
1865
|
-
if (!hasWordSync && !isUnsynced) return 10;
|
|
1866
|
-
|
|
1867
|
-
if (lower.includes('
|
|
1868
|
-
if (
|
|
1869
|
-
if (
|
|
1870
|
-
|
|
1871
|
-
if (lower.includes('
|
|
1872
|
-
|
|
1873
|
-
return
|
|
2086
|
+
if (lower.includes('bini') && hasWordSync) return 2;
|
|
2087
|
+
if (lower.includes('unison') && hasWordSync) return 3;
|
|
2088
|
+
if (isQQ && hasWordSync) return 4;
|
|
2089
|
+
if (lower.includes('musixmatch') && hasWordSync) return 5;
|
|
2090
|
+
if (lower.includes('lrclib') && hasWordSync) return 6;
|
|
2091
|
+
if (hasWordSync) return 7;
|
|
2092
|
+
|
|
2093
|
+
if (lower.includes('apple') && !hasWordSync && !isUnsynced) return 8;
|
|
2094
|
+
if (lower.includes('bini') && !hasWordSync && !isUnsynced) return 9;
|
|
2095
|
+
if (lower.includes('unison') && !hasWordSync && !isUnsynced) return 10;
|
|
2096
|
+
if (isQQ && !hasWordSync && !isUnsynced) return 11;
|
|
2097
|
+
if (lower.includes('musixmatch') && !hasWordSync && !isUnsynced) return 12;
|
|
2098
|
+
if (lower.includes('lrclib') && !hasWordSync && !isUnsynced) return 13;
|
|
2099
|
+
if (!hasWordSync && !isUnsynced) return 14;
|
|
2100
|
+
|
|
2101
|
+
if (lower.includes('apple') && isUnsynced) return 15;
|
|
2102
|
+
if (lower.includes('bini') && isUnsynced) return 16;
|
|
2103
|
+
if (lower.includes('unison') && isUnsynced) return 17;
|
|
2104
|
+
if (isQQ && isUnsynced) return 18;
|
|
2105
|
+
if (lower.includes('musixmatch') && isUnsynced) return 19;
|
|
2106
|
+
if (lower.includes('lrclib') && isUnsynced) return 20;
|
|
2107
|
+
if (lower.includes('genius')) return 21;
|
|
2108
|
+
|
|
2109
|
+
return 30;
|
|
1874
2110
|
}
|
|
1875
2111
|
|
|
1876
2112
|
private static mergeAndSortSources(
|
|
@@ -2153,73 +2389,17 @@ export class AmLyrics extends LitElement {
|
|
|
2153
2389
|
return null;
|
|
2154
2390
|
}
|
|
2155
2391
|
|
|
2156
|
-
private static async
|
|
2392
|
+
private static async fetchLyricsFromBiniLyrics(
|
|
2157
2393
|
title: string,
|
|
2158
2394
|
artist: string,
|
|
2159
2395
|
isrc?: string,
|
|
2160
2396
|
metadata: { durationMs?: number; album?: string } = {},
|
|
2161
|
-
): Promise<YouLyPlusLyricsResult
|
|
2162
|
-
if ((!title || !artist) && !isrc) return
|
|
2163
|
-
|
|
2164
|
-
const params = new URLSearchParams();
|
|
2165
|
-
if (title) params.append('title', title);
|
|
2166
|
-
if (artist) params.append('artist', artist);
|
|
2167
|
-
if (isrc) params.append('isrc', isrc);
|
|
2168
|
-
|
|
2169
|
-
if (metadata.album) {
|
|
2170
|
-
params.append('album', metadata.album);
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
if (metadata.durationMs && metadata.durationMs > 0) {
|
|
2174
|
-
params.append(
|
|
2175
|
-
'duration',
|
|
2176
|
-
Math.round(metadata.durationMs / 1000).toString(),
|
|
2177
|
-
);
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
if (!DEFAULT_KPOE_SOURCE_ORDER.includes('apple')) {
|
|
2181
|
-
params.append('source', DEFAULT_KPOE_SOURCE_ORDER);
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
const getRank = (sourceLabel: string, parsedLines: any[]): number => {
|
|
2185
|
-
const lower = sourceLabel.toLowerCase();
|
|
2186
|
-
const hasWordSync = parsedLines.some(
|
|
2187
|
-
(line: any) =>
|
|
2188
|
-
line.text && Array.isArray(line.text) && line.text.length > 1,
|
|
2189
|
-
);
|
|
2190
|
-
|
|
2191
|
-
const isUnsynced =
|
|
2192
|
-
parsedLines.length > 0 &&
|
|
2193
|
-
parsedLines.every(
|
|
2194
|
-
(line: any) => line.timestamp === 0 && line.endtime === 0,
|
|
2195
|
-
);
|
|
2196
|
-
|
|
2197
|
-
const isQQ = lower.includes('qq') || lower.includes('lyricsplus');
|
|
2198
|
-
|
|
2199
|
-
if (lower.includes('apple') && hasWordSync) return 1;
|
|
2200
|
-
if (isQQ && hasWordSync) return 2;
|
|
2201
|
-
if (lower.includes('musixmatch') && hasWordSync) return 3;
|
|
2202
|
-
if (hasWordSync) return 4;
|
|
2203
|
-
|
|
2204
|
-
if (lower.includes('apple') && !hasWordSync && !isUnsynced) return 5;
|
|
2205
|
-
if (isQQ && !hasWordSync && !isUnsynced) return 6;
|
|
2206
|
-
if (lower.includes('musixmatch') && !hasWordSync && !isUnsynced) return 7;
|
|
2207
|
-
if (!hasWordSync && !isUnsynced) return 8;
|
|
2208
|
-
|
|
2209
|
-
if (lower.includes('apple') && isUnsynced) return 9;
|
|
2210
|
-
if (isQQ && isUnsynced) return 10;
|
|
2211
|
-
if (lower.includes('musixmatch') && isUnsynced) return 11;
|
|
2212
|
-
|
|
2213
|
-
return 20;
|
|
2214
|
-
};
|
|
2215
|
-
|
|
2216
|
-
const allResults: YouLyPlusLyricsResult[] = [];
|
|
2397
|
+
): Promise<YouLyPlusLyricsResult | null> {
|
|
2398
|
+
if ((!title || !artist) && !isrc) return null;
|
|
2217
2399
|
|
|
2218
|
-
// Try BiniLyrics cache API first
|
|
2219
2400
|
try {
|
|
2220
2401
|
let cacheData: any = null;
|
|
2221
2402
|
|
|
2222
|
-
// First attempt: Prefer ISRC search if available
|
|
2223
2403
|
if (isrc) {
|
|
2224
2404
|
try {
|
|
2225
2405
|
const isrcUrl = `https://lyrics-api.binimum.org/?isrc=${encodeURIComponent(isrc)}`;
|
|
@@ -2230,12 +2410,11 @@ export class AmLyrics extends LitElement {
|
|
|
2230
2410
|
cacheData = data;
|
|
2231
2411
|
}
|
|
2232
2412
|
}
|
|
2233
|
-
} catch
|
|
2413
|
+
} catch {
|
|
2234
2414
|
// Fall through to title/artist search
|
|
2235
2415
|
}
|
|
2236
2416
|
}
|
|
2237
2417
|
|
|
2238
|
-
// Second attempt: Fallback to title and artist search if ISRC search failed or was not available
|
|
2239
2418
|
if (!cacheData && title && artist) {
|
|
2240
2419
|
const cacheParams = new URLSearchParams({
|
|
2241
2420
|
track: title,
|
|
@@ -2260,60 +2439,17 @@ export class AmLyrics extends LitElement {
|
|
|
2260
2439
|
|
|
2261
2440
|
if (cacheData && cacheData.results && cacheData.results.length > 0) {
|
|
2262
2441
|
const result = cacheData.results[0];
|
|
2263
|
-
if (result.
|
|
2442
|
+
if (result.lyricsUrl) {
|
|
2264
2443
|
const ttmlRes = await fetchWithTimeout(result.lyricsUrl);
|
|
2265
2444
|
if (ttmlRes.ok) {
|
|
2266
2445
|
const ttmlText = await ttmlRes.text();
|
|
2267
2446
|
const parseResult = AmLyrics.parseTTML(ttmlText);
|
|
2268
2447
|
if (parseResult && parseResult.lines.length > 0) {
|
|
2269
|
-
|
|
2448
|
+
return {
|
|
2270
2449
|
lines: parseResult.lines,
|
|
2271
2450
|
source: 'BiniLyrics',
|
|
2272
2451
|
songwriters: parseResult.songwriters,
|
|
2273
|
-
}
|
|
2274
|
-
return allResults;
|
|
2275
|
-
}
|
|
2276
|
-
}
|
|
2277
|
-
} else {
|
|
2278
|
-
// Not word type, try fetching any word synced lyrics from lyricsplus
|
|
2279
|
-
const fallbackParams = new URLSearchParams(params);
|
|
2280
|
-
const fallbackUrl = `https://lyricsplus.binimum.org/v2/lyrics/get?${fallbackParams.toString()}`;
|
|
2281
|
-
try {
|
|
2282
|
-
const fallbackRes = await fetchWithTimeout(fallbackUrl);
|
|
2283
|
-
if (fallbackRes.ok) {
|
|
2284
|
-
const payload = await fallbackRes.json();
|
|
2285
|
-
const lines = AmLyrics.convertKPoeLyrics(payload);
|
|
2286
|
-
const hasWordSync = lines?.some(
|
|
2287
|
-
(line: any) =>
|
|
2288
|
-
line.text && Array.isArray(line.text) && line.text.length > 1,
|
|
2289
|
-
);
|
|
2290
|
-
if (lines && lines.length > 0 && hasWordSync) {
|
|
2291
|
-
const sourceLabel =
|
|
2292
|
-
payload?.metadata?.source ||
|
|
2293
|
-
payload?.metadata?.provider ||
|
|
2294
|
-
'LyricsPlus (KPoe)';
|
|
2295
|
-
allResults.push({ lines, source: sourceLabel });
|
|
2296
|
-
return allResults;
|
|
2297
|
-
}
|
|
2298
|
-
}
|
|
2299
|
-
} catch (fallbackError) {
|
|
2300
|
-
// Ignore fallback fetch error
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
// If fallback fails or has no word sync, fall back to bini lyrics
|
|
2304
|
-
if (result.lyricsUrl) {
|
|
2305
|
-
const ttmlRes = await fetchWithTimeout(result.lyricsUrl);
|
|
2306
|
-
if (ttmlRes.ok) {
|
|
2307
|
-
const ttmlText = await ttmlRes.text();
|
|
2308
|
-
const parseResult = AmLyrics.parseTTML(ttmlText);
|
|
2309
|
-
if (parseResult && parseResult.lines.length > 0) {
|
|
2310
|
-
allResults.push({
|
|
2311
|
-
lines: parseResult.lines,
|
|
2312
|
-
source: 'BiniLyrics',
|
|
2313
|
-
songwriters: parseResult.songwriters,
|
|
2314
|
-
});
|
|
2315
|
-
return allResults;
|
|
2316
|
-
}
|
|
2452
|
+
};
|
|
2317
2453
|
}
|
|
2318
2454
|
}
|
|
2319
2455
|
}
|
|
@@ -2323,6 +2459,92 @@ export class AmLyrics extends LitElement {
|
|
|
2323
2459
|
console.error('Cache API failed', e);
|
|
2324
2460
|
}
|
|
2325
2461
|
|
|
2462
|
+
return null;
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
private static async fetchLyricsFromYouLyPlus(
|
|
2466
|
+
title: string,
|
|
2467
|
+
artist: string,
|
|
2468
|
+
isrc?: string,
|
|
2469
|
+
metadata: { durationMs?: number; album?: string } = {},
|
|
2470
|
+
skipBiniCache = false,
|
|
2471
|
+
): Promise<YouLyPlusLyricsResult[]> {
|
|
2472
|
+
if ((!title || !artist) && !isrc) return [];
|
|
2473
|
+
|
|
2474
|
+
const params = new URLSearchParams();
|
|
2475
|
+
if (title) params.append('title', title);
|
|
2476
|
+
if (artist) params.append('artist', artist);
|
|
2477
|
+
if (isrc) params.append('isrc', isrc);
|
|
2478
|
+
|
|
2479
|
+
if (metadata.album) {
|
|
2480
|
+
params.append('album', metadata.album);
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
if (metadata.durationMs && metadata.durationMs > 0) {
|
|
2484
|
+
params.append(
|
|
2485
|
+
'duration',
|
|
2486
|
+
Math.round(metadata.durationMs / 1000).toString(),
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
if (!DEFAULT_KPOE_SOURCE_ORDER.includes('apple')) {
|
|
2491
|
+
params.append('source', DEFAULT_KPOE_SOURCE_ORDER);
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
const getRank = (sourceLabel: string, parsedLines: any[]): number => {
|
|
2495
|
+
const lower = sourceLabel.toLowerCase();
|
|
2496
|
+
const hasWordSync = parsedLines.some(
|
|
2497
|
+
(line: any) =>
|
|
2498
|
+
line.text && Array.isArray(line.text) && line.text.length > 1,
|
|
2499
|
+
);
|
|
2500
|
+
|
|
2501
|
+
const isUnsynced =
|
|
2502
|
+
parsedLines.length > 0 &&
|
|
2503
|
+
parsedLines.every(
|
|
2504
|
+
(line: any) => line.timestamp === 0 && line.endtime === 0,
|
|
2505
|
+
);
|
|
2506
|
+
|
|
2507
|
+
const isQQ = lower.includes('qq') || lower.includes('lyricsplus');
|
|
2508
|
+
|
|
2509
|
+
if (lower.includes('apple') && hasWordSync) return 1;
|
|
2510
|
+
if (lower.includes('bini') && hasWordSync) return 2;
|
|
2511
|
+
if (lower.includes('unison') && hasWordSync) return 3;
|
|
2512
|
+
if (isQQ && hasWordSync) return 4;
|
|
2513
|
+
if (lower.includes('musixmatch') && hasWordSync) return 5;
|
|
2514
|
+
if (hasWordSync) return 6;
|
|
2515
|
+
|
|
2516
|
+
if (lower.includes('apple') && !hasWordSync && !isUnsynced) return 7;
|
|
2517
|
+
if (lower.includes('bini') && !hasWordSync && !isUnsynced) return 8;
|
|
2518
|
+
if (lower.includes('unison') && !hasWordSync && !isUnsynced) return 9;
|
|
2519
|
+
if (isQQ && !hasWordSync && !isUnsynced) return 10;
|
|
2520
|
+
if (lower.includes('musixmatch') && !hasWordSync && !isUnsynced)
|
|
2521
|
+
return 11;
|
|
2522
|
+
if (!hasWordSync && !isUnsynced) return 12;
|
|
2523
|
+
|
|
2524
|
+
if (lower.includes('apple') && isUnsynced) return 13;
|
|
2525
|
+
if (lower.includes('bini') && isUnsynced) return 14;
|
|
2526
|
+
if (lower.includes('unison') && isUnsynced) return 15;
|
|
2527
|
+
if (isQQ && isUnsynced) return 16;
|
|
2528
|
+
if (lower.includes('musixmatch') && isUnsynced) return 17;
|
|
2529
|
+
|
|
2530
|
+
return 30;
|
|
2531
|
+
};
|
|
2532
|
+
|
|
2533
|
+
const allResults: YouLyPlusLyricsResult[] = [];
|
|
2534
|
+
|
|
2535
|
+
if (!skipBiniCache) {
|
|
2536
|
+
const biniResult = await AmLyrics.fetchLyricsFromBiniLyrics(
|
|
2537
|
+
title,
|
|
2538
|
+
artist,
|
|
2539
|
+
isrc,
|
|
2540
|
+
metadata,
|
|
2541
|
+
);
|
|
2542
|
+
if (biniResult) {
|
|
2543
|
+
allResults.push(biniResult);
|
|
2544
|
+
return allResults;
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2326
2548
|
// Shuffle servers so we pick a random one first, with all others as fallback
|
|
2327
2549
|
// Try up to 3 servers to improve reliability when some have CORS or connectivity issues
|
|
2328
2550
|
const shuffledServers = [...KPOE_SERVERS]
|
|
@@ -2596,15 +2818,94 @@ export class AmLyrics extends LitElement {
|
|
|
2596
2818
|
return null;
|
|
2597
2819
|
}
|
|
2598
2820
|
|
|
2599
|
-
private static
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
const
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2821
|
+
private static async fetchLyricsFromUnison(
|
|
2822
|
+
metadata: SongMetadata,
|
|
2823
|
+
): Promise<YouLyPlusLyricsResult | null> {
|
|
2824
|
+
const title = metadata.title?.trim();
|
|
2825
|
+
const artist = metadata.artist?.trim();
|
|
2826
|
+
if (!title || !artist) return null;
|
|
2827
|
+
|
|
2828
|
+
const params = new URLSearchParams();
|
|
2829
|
+
params.append('song', title);
|
|
2830
|
+
params.append('artist', artist);
|
|
2831
|
+
if (metadata.album) {
|
|
2832
|
+
params.append('album', metadata.album);
|
|
2833
|
+
}
|
|
2834
|
+
if (metadata.durationMs && metadata.durationMs > 0) {
|
|
2835
|
+
params.append(
|
|
2836
|
+
'duration',
|
|
2837
|
+
Math.round(metadata.durationMs / 1000).toString(),
|
|
2838
|
+
);
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
try {
|
|
2842
|
+
const response = await fetchWithTimeout(
|
|
2843
|
+
`https://unison.boidu.dev/lyrics?${params.toString()}`,
|
|
2844
|
+
);
|
|
2845
|
+
if (!response.ok) return null;
|
|
2846
|
+
|
|
2847
|
+
const data = await response.json();
|
|
2848
|
+
if (!data.success || !data.data?.lyrics) return null;
|
|
2849
|
+
|
|
2850
|
+
const lyricsData = data.data;
|
|
2851
|
+
const format = lyricsData.format || 'lrc';
|
|
2852
|
+
const syncType = lyricsData.syncType || 'linesync';
|
|
2853
|
+
const lyricsText = lyricsData.lyrics;
|
|
2854
|
+
|
|
2855
|
+
if (format === 'ttml') {
|
|
2856
|
+
const parseResult = AmLyrics.parseTTML(lyricsText);
|
|
2857
|
+
if (parseResult && parseResult.lines.length > 0) {
|
|
2858
|
+
return {
|
|
2859
|
+
lines: parseResult.lines,
|
|
2860
|
+
source: 'Unison',
|
|
2861
|
+
songwriters: parseResult.songwriters,
|
|
2862
|
+
};
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
if (format === 'lrc') {
|
|
2867
|
+
if (syncType === 'plain') {
|
|
2868
|
+
const plainLines = lyricsText
|
|
2869
|
+
.split('\n')
|
|
2870
|
+
.map((l: string) => l.trim())
|
|
2871
|
+
.filter((l: string) => l);
|
|
2872
|
+
if (plainLines.length > 0) {
|
|
2873
|
+
const lines: LyricsLine[] = plainLines.map(
|
|
2874
|
+
(text: string): LyricsLine => ({
|
|
2875
|
+
text: [{ text, part: false, timestamp: 0, endtime: 0 }],
|
|
2876
|
+
background: false,
|
|
2877
|
+
backgroundText: [],
|
|
2878
|
+
oppositeTurn: false,
|
|
2879
|
+
timestamp: 0,
|
|
2880
|
+
endtime: 0,
|
|
2881
|
+
isWordSynced: false,
|
|
2882
|
+
}),
|
|
2883
|
+
);
|
|
2884
|
+
return { lines, source: 'Unison (unsynced)' };
|
|
2885
|
+
}
|
|
2886
|
+
} else {
|
|
2887
|
+
const lines = AmLyrics.parseLrcSubtitles(lyricsText);
|
|
2888
|
+
if (lines.length > 0) {
|
|
2889
|
+
return { lines, source: 'Unison' };
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
} catch {
|
|
2894
|
+
// Unison fetch failed
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
return null;
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
private static calculateLineAlignments(
|
|
2901
|
+
lineSingers: (string | undefined)[],
|
|
2902
|
+
agentTypes: Record<string, string>,
|
|
2903
|
+
): ('start' | 'end' | undefined)[] {
|
|
2904
|
+
const lineSideAssignments = new Array(lineSingers.length).fill(undefined);
|
|
2905
|
+
let currentSideIsLeft = true;
|
|
2906
|
+
let lastPersonSingerId: string | null = null;
|
|
2907
|
+
let rightCount = 0;
|
|
2908
|
+
let totalCount = 0;
|
|
2608
2909
|
|
|
2609
2910
|
lineSingers.forEach((singerId, index) => {
|
|
2610
2911
|
let sideClass: 'start' | 'end' | undefined;
|
|
@@ -3163,24 +3464,36 @@ export class AmLyrics extends LitElement {
|
|
|
3163
3464
|
|
|
3164
3465
|
if (linesChanged || isSeek) {
|
|
3165
3466
|
if (this.lyricsContainer) {
|
|
3166
|
-
// Remove
|
|
3467
|
+
// Remove .active and .bg-expanded immediately when a line drops.
|
|
3468
|
+
// All visual fading is handled by CSS transitions — no JS delays,
|
|
3469
|
+
// so overlapping lyrics never get stuck with multiple .active lines.
|
|
3167
3470
|
for (const lineIndex of oldActiveLines) {
|
|
3168
3471
|
if (!newActiveLines.includes(lineIndex)) {
|
|
3169
3472
|
const lineElement = this._getLineElement(lineIndex);
|
|
3170
3473
|
if (lineElement) {
|
|
3171
|
-
|
|
3474
|
+
if (isSeek || this.isUserScrolling) {
|
|
3475
|
+
AmLyrics.unfinishSyllables(lineElement);
|
|
3476
|
+
} else {
|
|
3477
|
+
AmLyrics.finishSyllablesUpToTime(lineElement, newTime);
|
|
3478
|
+
}
|
|
3479
|
+
|
|
3480
|
+
lineElement.classList.remove('active', 'bg-expanded');
|
|
3481
|
+
|
|
3482
|
+
if (lineElement.classList.contains('pre-active')) {
|
|
3483
|
+
lineElement.classList.remove('pre-active');
|
|
3484
|
+
}
|
|
3172
3485
|
const preIdx = this.preActiveLineElements.indexOf(lineElement);
|
|
3173
3486
|
if (preIdx !== -1) this.preActiveLineElements.splice(preIdx, 1);
|
|
3174
|
-
AmLyrics.resetSyllables(lineElement);
|
|
3175
3487
|
}
|
|
3176
3488
|
}
|
|
3177
3489
|
}
|
|
3178
|
-
|
|
3490
|
+
|
|
3491
|
+
// Add 'active' and 'bg-expanded' to newly active lines
|
|
3179
3492
|
for (const lineIndex of newActiveLines) {
|
|
3180
3493
|
if (!oldActiveLines.includes(lineIndex)) {
|
|
3181
3494
|
const lineElement = this._getLineElement(lineIndex);
|
|
3182
3495
|
if (lineElement) {
|
|
3183
|
-
lineElement.classList.add('active');
|
|
3496
|
+
lineElement.classList.add('active', 'bg-expanded');
|
|
3184
3497
|
lineElement.classList.remove('pre-active');
|
|
3185
3498
|
const preIdx = this.preActiveLineElements.indexOf(lineElement);
|
|
3186
3499
|
if (preIdx !== -1) this.preActiveLineElements.splice(preIdx, 1);
|
|
@@ -3188,16 +3501,31 @@ export class AmLyrics extends LitElement {
|
|
|
3188
3501
|
}
|
|
3189
3502
|
}
|
|
3190
3503
|
|
|
3191
|
-
|
|
3192
|
-
|
|
3504
|
+
// Remove pre-active from lines that are now active (they no longer
|
|
3505
|
+
// need the unblur preview class) and from lines that dropped.
|
|
3506
|
+
for (const lineElement of this.preActiveLineElements) {
|
|
3507
|
+
const idx = AmLyrics.getLineIndexFromElement(lineElement);
|
|
3508
|
+
if (
|
|
3509
|
+
idx === null ||
|
|
3510
|
+
(!newActiveLines.includes(idx) &&
|
|
3511
|
+
lineElement !== this.currentPrimaryActiveLine)
|
|
3512
|
+
) {
|
|
3513
|
+
lineElement.classList.remove('pre-active');
|
|
3514
|
+
}
|
|
3193
3515
|
}
|
|
3516
|
+
this.preActiveLineElements = this.preActiveLineElements.filter(el =>
|
|
3517
|
+
el.classList.contains('pre-active'),
|
|
3518
|
+
);
|
|
3194
3519
|
}
|
|
3195
3520
|
|
|
3196
3521
|
this.startAnimationFromTime(newTime);
|
|
3197
|
-
|
|
3198
|
-
this._handleActiveLineScroll(oldActiveLines, isSeek);
|
|
3199
3522
|
}
|
|
3200
3523
|
|
|
3524
|
+
// Predictive scroll: run on every tick so we scroll *before* the next
|
|
3525
|
+
// line starts, matching YouLyPlus behaviour.
|
|
3526
|
+
this._handleActiveLineScroll(oldActiveLines, isSeek);
|
|
3527
|
+
this.clearPastLineHighlights();
|
|
3528
|
+
|
|
3201
3529
|
if (this.lyricsContainer) {
|
|
3202
3530
|
// Update syllables in active lines using cached elements
|
|
3203
3531
|
for (const lineIndex of this.activeLineIndices) {
|
|
@@ -3215,12 +3543,12 @@ export class AmLyrics extends LitElement {
|
|
|
3215
3543
|
// Imperatively manage gap active state
|
|
3216
3544
|
if (this.gapElementCache.size > 0) {
|
|
3217
3545
|
for (const [, gap] of this.gapElementCache) {
|
|
3218
|
-
const gapStartTime =
|
|
3219
|
-
gap.
|
|
3220
|
-
|
|
3221
|
-
const gapEndTime =
|
|
3222
|
-
gap.
|
|
3223
|
-
|
|
3546
|
+
const gapStartTime =
|
|
3547
|
+
(gap as any)._cachedStartTime ??
|
|
3548
|
+
parseFloat(gap.getAttribute('data-start-time') || '0');
|
|
3549
|
+
const gapEndTime =
|
|
3550
|
+
(gap as any)._cachedEndTime ??
|
|
3551
|
+
parseFloat(gap.getAttribute('data-end-time') || '0');
|
|
3224
3552
|
const shouldBeActive =
|
|
3225
3553
|
newTime >= gapStartTime && newTime < gapEndTime;
|
|
3226
3554
|
const isActive = gap.classList.contains('active');
|
|
@@ -3377,6 +3705,16 @@ export class AmLyrics extends LitElement {
|
|
|
3377
3705
|
const isFooterActive = newTime > lastLyric.endtime + 200; // Snappier 200ms buffer
|
|
3378
3706
|
if (isFooterActive && !footer.classList.contains('active')) {
|
|
3379
3707
|
footer.classList.add('active');
|
|
3708
|
+
// Clear pre-active from the last lyric so it doesn't stay
|
|
3709
|
+
// unblurred when the footer takes over.
|
|
3710
|
+
const lastLine = this.lyricsContainer.querySelector(
|
|
3711
|
+
'.lyrics-line:last-of-type',
|
|
3712
|
+
) as HTMLElement;
|
|
3713
|
+
if (lastLine) {
|
|
3714
|
+
lastLine.classList.remove('pre-active');
|
|
3715
|
+
const preIdx = this.preActiveLineElements.indexOf(lastLine);
|
|
3716
|
+
if (preIdx !== -1) this.preActiveLineElements.splice(preIdx, 1);
|
|
3717
|
+
}
|
|
3380
3718
|
if (
|
|
3381
3719
|
this.autoScroll &&
|
|
3382
3720
|
!this.isUserScrolling &&
|
|
@@ -3388,59 +3726,6 @@ export class AmLyrics extends LitElement {
|
|
|
3388
3726
|
footer.classList.remove('active');
|
|
3389
3727
|
}
|
|
3390
3728
|
}
|
|
3391
|
-
|
|
3392
|
-
// Pre-scroll: scroll to upcoming line ~0.5s before it starts
|
|
3393
|
-
if (
|
|
3394
|
-
this.autoScroll &&
|
|
3395
|
-
!this.isUserScrolling &&
|
|
3396
|
-
!this.isClickSeeking &&
|
|
3397
|
-
this.lyrics
|
|
3398
|
-
) {
|
|
3399
|
-
let preActiveLineIndex: number | null = null;
|
|
3400
|
-
|
|
3401
|
-
for (let i = 0; i < this.lyrics.length; i += 1) {
|
|
3402
|
-
const line = this.lyrics[i];
|
|
3403
|
-
const timeUntilStart = line.timestamp - newTime;
|
|
3404
|
-
|
|
3405
|
-
const nextLineEl = this._getLineElement(i);
|
|
3406
|
-
|
|
3407
|
-
const isBackToBack = this.activeLineIndices.length > 0;
|
|
3408
|
-
const leadTime = isBackToBack
|
|
3409
|
-
? PRE_SCROLL_LEAD_SHORT_MS
|
|
3410
|
-
: PRE_SCROLL_LEAD_MS;
|
|
3411
|
-
|
|
3412
|
-
if (timeUntilStart > leadTime) {
|
|
3413
|
-
break;
|
|
3414
|
-
}
|
|
3415
|
-
|
|
3416
|
-
if (timeUntilStart > 0 && timeUntilStart <= leadTime) {
|
|
3417
|
-
if (nextLineEl) {
|
|
3418
|
-
preActiveLineIndex = i;
|
|
3419
|
-
|
|
3420
|
-
if (!isBackToBack) {
|
|
3421
|
-
nextLineEl.classList.add('pre-active');
|
|
3422
|
-
if (!this.preActiveLineElements.includes(nextLineEl)) {
|
|
3423
|
-
this.preActiveLineElements.push(nextLineEl);
|
|
3424
|
-
}
|
|
3425
|
-
}
|
|
3426
|
-
this.clearPreActiveClasses(i);
|
|
3427
|
-
|
|
3428
|
-
const slowScrollDuration = Math.max(
|
|
3429
|
-
SCROLL_ANIMATION_DURATION_MS,
|
|
3430
|
-
timeUntilStart,
|
|
3431
|
-
);
|
|
3432
|
-
this.focusLine(
|
|
3433
|
-
nextLineEl,
|
|
3434
|
-
false,
|
|
3435
|
-
isBackToBack ? 500 : slowScrollDuration,
|
|
3436
|
-
);
|
|
3437
|
-
}
|
|
3438
|
-
break;
|
|
3439
|
-
}
|
|
3440
|
-
}
|
|
3441
|
-
|
|
3442
|
-
this.clearPreActiveClasses(preActiveLineIndex);
|
|
3443
|
-
}
|
|
3444
3729
|
}
|
|
3445
3730
|
}
|
|
3446
3731
|
|
|
@@ -3459,12 +3744,38 @@ export class AmLyrics extends LitElement {
|
|
|
3459
3744
|
const activeLines = this.findActiveLineIndices(this.currentTime);
|
|
3460
3745
|
for (const lineIndex of activeLines) {
|
|
3461
3746
|
const lineEl = this._getLineElement(lineIndex);
|
|
3462
|
-
if (lineEl) lineEl.classList.add('active');
|
|
3747
|
+
if (lineEl) lineEl.classList.add('active', 'bg-expanded');
|
|
3463
3748
|
}
|
|
3464
3749
|
|
|
3465
3750
|
// Trigger a faux time-change so that updateSyllablesForLine fires
|
|
3466
3751
|
// to setup inline syllable CSS wipe animations for whatever the current time is
|
|
3467
3752
|
this._onTimeChanged(0, this.currentTime);
|
|
3753
|
+
|
|
3754
|
+
// Ensure position classes are applied on initial render if not playing yet
|
|
3755
|
+
if (this.positionedLineElements.length === 0) {
|
|
3756
|
+
const firstLine = this.lyricsContainer.querySelector(
|
|
3757
|
+
'.lyrics-line',
|
|
3758
|
+
) as HTMLElement;
|
|
3759
|
+
if (firstLine) this.updatePositionClasses(firstLine);
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
// Set up IntersectionObserver for viewport virtualization
|
|
3763
|
+
this.visibilityObserver?.disconnect();
|
|
3764
|
+
this.visibilityObserver = new IntersectionObserver(
|
|
3765
|
+
entries => {
|
|
3766
|
+
entries.forEach(entry => {
|
|
3767
|
+
const el = entry.target as HTMLElement;
|
|
3768
|
+
el.classList.toggle('far-line', !entry.isIntersecting);
|
|
3769
|
+
});
|
|
3770
|
+
},
|
|
3771
|
+
{
|
|
3772
|
+
root: this.lyricsContainer,
|
|
3773
|
+
rootMargin: '200px',
|
|
3774
|
+
threshold: 0,
|
|
3775
|
+
},
|
|
3776
|
+
);
|
|
3777
|
+
const lines = this.lyricsContainer.querySelectorAll('.lyrics-line');
|
|
3778
|
+
lines.forEach(line => this.visibilityObserver!.observe(line));
|
|
3468
3779
|
}
|
|
3469
3780
|
}
|
|
3470
3781
|
|
|
@@ -3494,6 +3805,14 @@ export class AmLyrics extends LitElement {
|
|
|
3494
3805
|
clearTimeout(this.userScrollTimeoutId);
|
|
3495
3806
|
this.userScrollTimeoutId = undefined;
|
|
3496
3807
|
}
|
|
3808
|
+
if (this.scrollUnlockTimeout) {
|
|
3809
|
+
clearTimeout(this.scrollUnlockTimeout);
|
|
3810
|
+
this.scrollUnlockTimeout = undefined;
|
|
3811
|
+
}
|
|
3812
|
+
if (this.scrollAnimationTimeout) {
|
|
3813
|
+
clearTimeout(this.scrollAnimationTimeout);
|
|
3814
|
+
this.scrollAnimationTimeout = undefined;
|
|
3815
|
+
}
|
|
3497
3816
|
|
|
3498
3817
|
// Scroll to top
|
|
3499
3818
|
if (this.lyricsContainer) {
|
|
@@ -3526,45 +3845,73 @@ export class AmLyrics extends LitElement {
|
|
|
3526
3845
|
/**
|
|
3527
3846
|
* Handle scrolling when active line indices change.
|
|
3528
3847
|
* Called imperatively from _onTimeChanged instead of from updated().
|
|
3848
|
+
*
|
|
3849
|
+
* Uses predictive scroll like YouLyPlus: computes a scrollLookAheadMs based
|
|
3850
|
+
* on the gap to the next line, finds the primary line at predictiveTime,
|
|
3851
|
+
* and scrolls with a duration matching the lookahead.
|
|
3529
3852
|
*/
|
|
3530
3853
|
private _handleActiveLineScroll(
|
|
3531
3854
|
_oldActiveIndices: number[],
|
|
3532
3855
|
forceScroll = false,
|
|
3533
3856
|
): void {
|
|
3534
|
-
if (this.
|
|
3857
|
+
if (!this.lyricsContainer || !this.lyrics || this.lyrics.length === 0) {
|
|
3535
3858
|
return;
|
|
3536
3859
|
}
|
|
3537
3860
|
|
|
3538
|
-
|
|
3539
|
-
|
|
3861
|
+
// If the footer is already active, it set up its own scroll.
|
|
3862
|
+
// Don't override it with a scroll back to the last lyric.
|
|
3863
|
+
const footer = this.lyricsContainer.querySelector('.lyrics-footer');
|
|
3864
|
+
if (footer?.classList.contains('active')) {
|
|
3865
|
+
return;
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
// 1. Compute scroll lookahead based on gap to next line (YouLyPlus style)
|
|
3869
|
+
let scrollLookAheadMs = 350;
|
|
3870
|
+
const currentAudioIndex = this.getLineIndexAtTime(
|
|
3871
|
+
this.currentTime,
|
|
3872
|
+
this.lastActiveIndex,
|
|
3540
3873
|
);
|
|
3541
|
-
if (
|
|
3874
|
+
if (
|
|
3875
|
+
currentAudioIndex !== -1 &&
|
|
3876
|
+
currentAudioIndex + 1 < this.lyrics.length
|
|
3877
|
+
) {
|
|
3878
|
+
const currentLine = this.lyrics[currentAudioIndex];
|
|
3879
|
+
const nextLine = this.lyrics[currentAudioIndex + 1];
|
|
3880
|
+
const gap = nextLine.timestamp - currentLine.endtime;
|
|
3881
|
+
scrollLookAheadMs = Math.min(500, Math.max(350, gap));
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
// 2. Find scroll target at predictive time
|
|
3885
|
+
const predictiveTime = this.currentTime + scrollLookAheadMs;
|
|
3886
|
+
const predictiveActiveIndices = this.findActiveLineIndices(predictiveTime);
|
|
3887
|
+
|
|
3888
|
+
let targetLineIndex: number | null;
|
|
3889
|
+
if (predictiveActiveIndices.length > 0) {
|
|
3890
|
+
targetLineIndex = this.getPrimaryScrollLineIndex(
|
|
3891
|
+
predictiveActiveIndices,
|
|
3892
|
+
predictiveTime,
|
|
3893
|
+
);
|
|
3894
|
+
} else {
|
|
3895
|
+
// Fallback: closest line before predictiveTime
|
|
3896
|
+
targetLineIndex = this.getLineIndexAtTime(predictiveTime, 0);
|
|
3897
|
+
}
|
|
3898
|
+
|
|
3899
|
+
if (targetLineIndex === null || targetLineIndex === -1) return;
|
|
3542
3900
|
|
|
3543
3901
|
const targetLine = this._getLineElement(targetLineIndex);
|
|
3544
3902
|
if (!targetLine) return;
|
|
3545
3903
|
|
|
3546
|
-
//
|
|
3547
|
-
//
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
this.
|
|
3551
|
-
|
|
3552
|
-
if (
|
|
3553
|
-
prevPrimaryIndex !== null &&
|
|
3554
|
-
targetLineIndex > prevPrimaryIndex &&
|
|
3555
|
-
this.lyrics
|
|
3556
|
-
) {
|
|
3557
|
-
const gap =
|
|
3558
|
-
this.lyrics[targetLineIndex].timestamp -
|
|
3559
|
-
this.lyrics[prevPrimaryIndex].endtime;
|
|
3560
|
-
if (gap > 200) {
|
|
3561
|
-
scrollDuration = Math.min(
|
|
3562
|
-
Math.max(gap * 0.85, SCROLL_ANIMATION_DURATION_MS),
|
|
3563
|
-
4000,
|
|
3564
|
-
);
|
|
3904
|
+
// Unblur the upcoming target line early (pre-active) so background
|
|
3905
|
+
// vocals start their max-height/opacity transition in sync with scroll.
|
|
3906
|
+
if (!targetLine.classList.contains('active')) {
|
|
3907
|
+
targetLine.classList.add('pre-active');
|
|
3908
|
+
if (!this.preActiveLineElements.includes(targetLine)) {
|
|
3909
|
+
this.preActiveLineElements.push(targetLine);
|
|
3565
3910
|
}
|
|
3566
3911
|
}
|
|
3567
3912
|
|
|
3913
|
+
const scrollDuration = scrollLookAheadMs;
|
|
3914
|
+
|
|
3568
3915
|
this.focusLine(targetLine, forceScroll, scrollDuration);
|
|
3569
3916
|
}
|
|
3570
3917
|
|
|
@@ -3591,6 +3938,7 @@ export class AmLyrics extends LitElement {
|
|
|
3591
3938
|
|
|
3592
3939
|
this.lineElementCache.clear();
|
|
3593
3940
|
this.gapElementCache.clear();
|
|
3941
|
+
this.cachedLineArray = [];
|
|
3594
3942
|
|
|
3595
3943
|
if (!this.lyrics) return;
|
|
3596
3944
|
|
|
@@ -3603,8 +3951,21 @@ export class AmLyrics extends LitElement {
|
|
|
3603
3951
|
const gapEl = this.lyricsContainer.querySelector(
|
|
3604
3952
|
`#gap-${i}`,
|
|
3605
3953
|
) as HTMLElement | null;
|
|
3606
|
-
if (gapEl)
|
|
3954
|
+
if (gapEl) {
|
|
3955
|
+
// Cache numeric timing values to avoid parseFloat on every frame
|
|
3956
|
+
(gapEl as any)._cachedStartTime = parseFloat(
|
|
3957
|
+
gapEl.getAttribute('data-start-time') || '0',
|
|
3958
|
+
);
|
|
3959
|
+
(gapEl as any)._cachedEndTime = parseFloat(
|
|
3960
|
+
gapEl.getAttribute('data-end-time') || '0',
|
|
3961
|
+
);
|
|
3962
|
+
this.gapElementCache.set(i, gapEl);
|
|
3963
|
+
}
|
|
3607
3964
|
}
|
|
3965
|
+
|
|
3966
|
+
// Rebuild cached line array for scroll/position queries
|
|
3967
|
+
const lineElements = this.lyricsContainer.querySelectorAll('.lyrics-line');
|
|
3968
|
+
this.cachedLineArray = Array.from(lineElements) as HTMLElement[];
|
|
3608
3969
|
}
|
|
3609
3970
|
|
|
3610
3971
|
private _getLineElement(index: number): HTMLElement | null {
|
|
@@ -3635,9 +3996,13 @@ export class AmLyrics extends LitElement {
|
|
|
3635
3996
|
this.cachedLineData = null;
|
|
3636
3997
|
this.lineElementCache.clear();
|
|
3637
3998
|
this.gapElementCache.clear();
|
|
3999
|
+
this.cachedLineArray = [];
|
|
4000
|
+
this.cachedScrollPaddingTop = null;
|
|
3638
4001
|
this.preActiveLineElements = [];
|
|
3639
4002
|
this.positionedLineElements = [];
|
|
3640
4003
|
this.activeGapLineElements = [];
|
|
4004
|
+
this.visibilityObserver?.disconnect();
|
|
4005
|
+
this.visibilityObserver = undefined;
|
|
3641
4006
|
}
|
|
3642
4007
|
|
|
3643
4008
|
private _updateCachedIsUnsynced() {
|
|
@@ -3678,6 +4043,7 @@ export class AmLyrics extends LitElement {
|
|
|
3678
4043
|
|
|
3679
4044
|
const groupGrowable: boolean[] = new Array(wordGroups.length).fill(false);
|
|
3680
4045
|
const groupGlowing: boolean[] = new Array(wordGroups.length).fill(false);
|
|
4046
|
+
const groupCharRise: boolean[] = new Array(wordGroups.length).fill(false);
|
|
3681
4047
|
const vwFullText: string[] = new Array(wordGroups.length).fill('');
|
|
3682
4048
|
const vwFullDuration: number[] = new Array(wordGroups.length).fill(0);
|
|
3683
4049
|
const vwCharOffset: number[] = new Array(wordGroups.length).fill(0);
|
|
@@ -3732,11 +4098,14 @@ export class AmLyrics extends LitElement {
|
|
|
3732
4098
|
const isLineSynced =
|
|
3733
4099
|
line.isWordSynced === false || line.text.some(s => s.lineSynced);
|
|
3734
4100
|
const isGlowingVW = isGrowableVW && !isLineSynced;
|
|
4101
|
+
const isCharRiseVW =
|
|
4102
|
+
!isGrowableVW && !isLineSynced && !isCJK && !isRTL && wordLen >= 8;
|
|
3735
4103
|
|
|
3736
4104
|
let charOff = 0;
|
|
3737
4105
|
for (let gi = vwStart; gi <= vwEnd; gi += 1) {
|
|
3738
4106
|
groupGrowable[gi] = isGrowableVW;
|
|
3739
4107
|
groupGlowing[gi] = isGlowingVW;
|
|
4108
|
+
groupCharRise[gi] = isCharRiseVW;
|
|
3740
4109
|
vwFullText[gi] = combinedText;
|
|
3741
4110
|
vwFullDuration[gi] = combinedDuration;
|
|
3742
4111
|
vwCharOffset[gi] = charOff;
|
|
@@ -3753,6 +4122,7 @@ export class AmLyrics extends LitElement {
|
|
|
3753
4122
|
wordGroups,
|
|
3754
4123
|
groupGrowable,
|
|
3755
4124
|
groupGlowing,
|
|
4125
|
+
groupCharRise,
|
|
3756
4126
|
vwFullText,
|
|
3757
4127
|
vwFullDuration,
|
|
3758
4128
|
vwCharOffset,
|
|
@@ -3776,12 +4146,12 @@ export class AmLyrics extends LitElement {
|
|
|
3776
4146
|
const { font } = computedStyle; // Full font string
|
|
3777
4147
|
const fontSize = parseFloat(computedStyle.fontSize);
|
|
3778
4148
|
|
|
3779
|
-
const
|
|
3780
|
-
'.lyrics-word.growable',
|
|
4149
|
+
const charTimedWords = this.shadowRoot.querySelectorAll(
|
|
4150
|
+
'.lyrics-word.growable, .lyrics-word.char-rise',
|
|
3781
4151
|
);
|
|
3782
|
-
if (!
|
|
4152
|
+
if (!charTimedWords) return;
|
|
3783
4153
|
|
|
3784
|
-
|
|
4154
|
+
charTimedWords.forEach((wordSpan: any) => {
|
|
3785
4155
|
const syllableWraps = wordSpan.querySelectorAll('.lyrics-syllable-wrap');
|
|
3786
4156
|
|
|
3787
4157
|
// Flatten syllables
|
|
@@ -3888,33 +4258,124 @@ export class AmLyrics extends LitElement {
|
|
|
3888
4258
|
);
|
|
3889
4259
|
if (
|
|
3890
4260
|
currentPrimaryIndex !== null &&
|
|
3891
|
-
activeIndices.includes(currentPrimaryIndex)
|
|
3892
|
-
candidateIndex < currentPrimaryIndex
|
|
4261
|
+
activeIndices.includes(currentPrimaryIndex)
|
|
3893
4262
|
) {
|
|
3894
|
-
|
|
4263
|
+
if (activeIndices.length <= 3) {
|
|
4264
|
+
candidateIndex = currentPrimaryIndex;
|
|
4265
|
+
} else if (candidateIndex < currentPrimaryIndex) {
|
|
4266
|
+
candidateIndex = currentPrimaryIndex;
|
|
4267
|
+
}
|
|
3895
4268
|
}
|
|
3896
4269
|
|
|
3897
4270
|
return candidateIndex;
|
|
3898
4271
|
}
|
|
3899
4272
|
|
|
4273
|
+
private getPrimaryScrollLineIndex(
|
|
4274
|
+
_activeIndices: number[],
|
|
4275
|
+
time: number,
|
|
4276
|
+
): number | null {
|
|
4277
|
+
if (!this.lyrics || this.lyrics.length === 0) return null;
|
|
4278
|
+
|
|
4279
|
+
// YouLyPlus-style: primary is simply the line at predictive time.
|
|
4280
|
+
const primaryIndex = this.getLineIndexAtTime(time, this.lastActiveIndex);
|
|
4281
|
+
if (primaryIndex === -1) return null;
|
|
4282
|
+
|
|
4283
|
+
// Guard: if new primary is ahead of current but they share the same
|
|
4284
|
+
// end time, keep current to prevent bounce during overlaps.
|
|
4285
|
+
const currentPrimaryIndex = AmLyrics.getLineIndexFromElement(
|
|
4286
|
+
this.currentPrimaryActiveLine,
|
|
4287
|
+
);
|
|
4288
|
+
if (
|
|
4289
|
+
currentPrimaryIndex !== null &&
|
|
4290
|
+
primaryIndex > currentPrimaryIndex &&
|
|
4291
|
+
this.lyrics[currentPrimaryIndex] &&
|
|
4292
|
+
this.lyrics[primaryIndex] &&
|
|
4293
|
+
this.lyrics[currentPrimaryIndex].endtime ===
|
|
4294
|
+
this.lyrics[primaryIndex].endtime
|
|
4295
|
+
) {
|
|
4296
|
+
const activeCount = this.findActiveLineIndices(time).length;
|
|
4297
|
+
if (activeCount <= 3) {
|
|
4298
|
+
return currentPrimaryIndex;
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
|
|
4302
|
+
return primaryIndex;
|
|
4303
|
+
}
|
|
4304
|
+
|
|
4305
|
+
private getOverlapClusterForActiveIndices(
|
|
4306
|
+
activeIndices: number[],
|
|
4307
|
+
time: number,
|
|
4308
|
+
): {
|
|
4309
|
+
start: number;
|
|
4310
|
+
end: number;
|
|
4311
|
+
startedEnd: number;
|
|
4312
|
+
startedEndTime: number;
|
|
4313
|
+
} | null {
|
|
4314
|
+
if (!this.lyrics || activeIndices.length === 0) return null;
|
|
4315
|
+
|
|
4316
|
+
let start = activeIndices[0];
|
|
4317
|
+
while (
|
|
4318
|
+
start > 0 &&
|
|
4319
|
+
this.lyrics[start - 1].endtime >= this.lyrics[start].timestamp
|
|
4320
|
+
) {
|
|
4321
|
+
start -= 1;
|
|
4322
|
+
}
|
|
4323
|
+
|
|
4324
|
+
let end = start;
|
|
4325
|
+
let clusterEndTime = this.lyrics[start].endtime;
|
|
4326
|
+
while (
|
|
4327
|
+
end + 1 < this.lyrics.length &&
|
|
4328
|
+
this.lyrics[end + 1].timestamp <= clusterEndTime
|
|
4329
|
+
) {
|
|
4330
|
+
end += 1;
|
|
4331
|
+
clusterEndTime = Math.max(clusterEndTime, this.lyrics[end].endtime);
|
|
4332
|
+
}
|
|
4333
|
+
|
|
4334
|
+
let startedEnd = start;
|
|
4335
|
+
let startedEndTime = this.lyrics[start].endtime;
|
|
4336
|
+
for (let i = start; i <= end; i += 1) {
|
|
4337
|
+
if (this.lyrics[i].timestamp <= time) {
|
|
4338
|
+
startedEnd = i;
|
|
4339
|
+
startedEndTime = Math.max(startedEndTime, this.lyrics[i].endtime);
|
|
4340
|
+
} else {
|
|
4341
|
+
break;
|
|
4342
|
+
}
|
|
4343
|
+
}
|
|
4344
|
+
|
|
4345
|
+
return { start, end, startedEnd, startedEndTime };
|
|
4346
|
+
}
|
|
4347
|
+
|
|
3900
4348
|
private focusLine(
|
|
3901
4349
|
lineElement: HTMLElement,
|
|
3902
4350
|
forceScroll = false,
|
|
3903
4351
|
scrollDuration: number | undefined = undefined,
|
|
3904
4352
|
skipScroll = false,
|
|
4353
|
+
preservePrimary = false,
|
|
3905
4354
|
): void {
|
|
3906
4355
|
const primaryChanged = lineElement !== this.currentPrimaryActiveLine;
|
|
3907
4356
|
|
|
3908
|
-
if (primaryChanged) {
|
|
4357
|
+
if (primaryChanged && !preservePrimary) {
|
|
4358
|
+
// .active is now managed solely by findActiveLineIndices (which uses
|
|
4359
|
+
// effectiveEndTimes). Lines stay active until their extended end,
|
|
4360
|
+
// so we no longer need to remove .active here.
|
|
3909
4361
|
this.lastPrimaryActiveLine = this.currentPrimaryActiveLine;
|
|
3910
4362
|
this.currentPrimaryActiveLine = lineElement;
|
|
4363
|
+
const lineIndex = AmLyrics.getLineIndexFromElement(lineElement);
|
|
4364
|
+
if (lineIndex !== null) {
|
|
4365
|
+
this.lastActiveIndex = lineIndex;
|
|
4366
|
+
}
|
|
3911
4367
|
}
|
|
3912
4368
|
|
|
3913
|
-
|
|
4369
|
+
// Only update blur/opacity position classes when the primary line
|
|
4370
|
+
// actually changes (or on force scroll). Running this every tick
|
|
4371
|
+
// causes visual churn and upward glitches.
|
|
4372
|
+
if (primaryChanged || forceScroll) {
|
|
4373
|
+
this.updatePositionClasses(lineElement);
|
|
4374
|
+
}
|
|
3914
4375
|
|
|
3915
4376
|
if (
|
|
3916
4377
|
!skipScroll &&
|
|
3917
|
-
(forceScroll || primaryChanged) &&
|
|
4378
|
+
(forceScroll || primaryChanged || preservePrimary) &&
|
|
3918
4379
|
this.autoScroll &&
|
|
3919
4380
|
!this.isUserScrolling &&
|
|
3920
4381
|
!this.isClickSeeking
|
|
@@ -3941,6 +4402,8 @@ export class AmLyrics extends LitElement {
|
|
|
3941
4402
|
// Mark that user is currently scrolling
|
|
3942
4403
|
this.setUserScrolling(true);
|
|
3943
4404
|
|
|
4405
|
+
this.clearPastLineHighlights();
|
|
4406
|
+
|
|
3944
4407
|
// Clear any existing timeout
|
|
3945
4408
|
if (this.userScrollTimeoutId) {
|
|
3946
4409
|
clearTimeout(this.userScrollTimeoutId);
|
|
@@ -3953,31 +4416,82 @@ export class AmLyrics extends LitElement {
|
|
|
3953
4416
|
|
|
3954
4417
|
// Optionally scroll back to current active line when re-enabling auto-scroll
|
|
3955
4418
|
if (this.activeLineIndices.length > 0) {
|
|
3956
|
-
this.
|
|
4419
|
+
this._handleActiveLineScroll([], false);
|
|
3957
4420
|
}
|
|
3958
4421
|
}, 2000);
|
|
3959
4422
|
}
|
|
3960
4423
|
|
|
4424
|
+
private clearPastLineHighlights() {
|
|
4425
|
+
if (!this.lyricsContainer) return;
|
|
4426
|
+
|
|
4427
|
+
const lineElements = this.cachedLineArray.length
|
|
4428
|
+
? this.cachedLineArray
|
|
4429
|
+
: (Array.from(
|
|
4430
|
+
this.lyricsContainer.querySelectorAll(
|
|
4431
|
+
'.lyrics-line:not(.lyrics-gap)',
|
|
4432
|
+
),
|
|
4433
|
+
) as HTMLElement[]);
|
|
4434
|
+
const containerRect = this.lyricsContainer.getBoundingClientRect();
|
|
4435
|
+
const anchorY = containerRect.top + this.getScrollPaddingTop();
|
|
4436
|
+
|
|
4437
|
+
for (let i = 0; i < lineElements.length; i += 1) {
|
|
4438
|
+
const lineElement = lineElements[i];
|
|
4439
|
+
const isActive = lineElement.classList.contains('active');
|
|
4440
|
+
const lineRect = lineElement.getBoundingClientRect();
|
|
4441
|
+
const hasScrolledPast = lineRect.bottom < anchorY - 2;
|
|
4442
|
+
if (!isActive && hasScrolledPast) {
|
|
4443
|
+
AmLyrics.unfinishSyllables(lineElement);
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
|
|
4448
|
+
/**
|
|
4449
|
+
* Find the first (lowest-index) line whose raw time range contains `timeMs`.
|
|
4450
|
+
* Uses a stable forward scan so overlapping ranges always return the same
|
|
4451
|
+
* line, preventing primary-target jitter that causes scroll glitches.
|
|
4452
|
+
*/
|
|
4453
|
+
private getLineIndexAtTime(timeMs: number, startHintIndex = 0): number {
|
|
4454
|
+
if (!this.lyrics || this.lyrics.length === 0) return -1;
|
|
4455
|
+
const len = this.lyrics.length;
|
|
4456
|
+
|
|
4457
|
+
// 1. Check hint and immediate neighbours first (fast path)
|
|
4458
|
+
const hint = Math.max(0, Math.min(startHintIndex, len - 1));
|
|
4459
|
+
for (let i = hint; i < len; i += 1) {
|
|
4460
|
+
const line = this.lyrics[i];
|
|
4461
|
+
if (line.timestamp > timeMs) break;
|
|
4462
|
+
if (timeMs >= line.timestamp && timeMs < line.endtime) {
|
|
4463
|
+
return i;
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
for (let i = hint - 1; i >= 0; i -= 1) {
|
|
4467
|
+
const line = this.lyrics[i];
|
|
4468
|
+
if (timeMs >= line.timestamp && timeMs < line.endtime) {
|
|
4469
|
+
return i;
|
|
4470
|
+
}
|
|
4471
|
+
if (line.endtime < timeMs) break;
|
|
4472
|
+
}
|
|
4473
|
+
|
|
4474
|
+
// 2. Full forward scan — guaranteed deterministic for overlaps
|
|
4475
|
+
for (let i = 0; i < len; i += 1) {
|
|
4476
|
+
const line = this.lyrics[i];
|
|
4477
|
+
if (line.timestamp > timeMs) break;
|
|
4478
|
+
if (timeMs >= line.timestamp && timeMs < line.endtime) {
|
|
4479
|
+
return i;
|
|
4480
|
+
}
|
|
4481
|
+
}
|
|
4482
|
+
|
|
4483
|
+
return -1;
|
|
4484
|
+
}
|
|
4485
|
+
|
|
3961
4486
|
private findActiveLineIndices(time: number): number[] {
|
|
3962
4487
|
if (!this.lyrics || this.lyrics.length === 0) return [];
|
|
3963
4488
|
const activeLines: number[] = [];
|
|
3964
4489
|
|
|
3965
4490
|
for (let i = 0; i < this.lyrics.length; i += 1) {
|
|
3966
4491
|
const line = this.lyrics[i];
|
|
3967
|
-
let effectiveEndTime = line.endtime;
|
|
3968
|
-
|
|
3969
|
-
if (i < this.lyrics.length - 1) {
|
|
3970
|
-
const nextLineStart = this.lyrics[i + 1].timestamp;
|
|
3971
|
-
const gapDuration = nextLineStart - line.endtime;
|
|
3972
|
-
if (gapDuration < INSTRUMENTAL_THRESHOLD_MS) {
|
|
3973
|
-
if (effectiveEndTime < nextLineStart) {
|
|
3974
|
-
effectiveEndTime = Math.max(effectiveEndTime, nextLineStart - 500);
|
|
3975
|
-
}
|
|
3976
|
-
}
|
|
3977
|
-
}
|
|
3978
4492
|
|
|
3979
4493
|
if (line.timestamp > time) break;
|
|
3980
|
-
if (time >= line.timestamp && time
|
|
4494
|
+
if (time >= line.timestamp && time < line.endtime) {
|
|
3981
4495
|
activeLines.push(i);
|
|
3982
4496
|
}
|
|
3983
4497
|
}
|
|
@@ -4227,10 +4741,6 @@ export class AmLyrics extends LitElement {
|
|
|
4227
4741
|
}
|
|
4228
4742
|
|
|
4229
4743
|
// Clear scroll animation timeouts
|
|
4230
|
-
if (this.scrollUnlockTimeout) {
|
|
4231
|
-
clearTimeout(this.scrollUnlockTimeout);
|
|
4232
|
-
this.scrollUnlockTimeout = undefined;
|
|
4233
|
-
}
|
|
4234
4744
|
if (this.scrollAnimationTimeout) {
|
|
4235
4745
|
clearTimeout(this.scrollAnimationTimeout);
|
|
4236
4746
|
this.scrollAnimationTimeout = undefined;
|
|
@@ -4358,6 +4868,7 @@ export class AmLyrics extends LitElement {
|
|
|
4358
4868
|
const targetTranslateY = paddingTop - gapTarget.offsetTop;
|
|
4359
4869
|
|
|
4360
4870
|
this.isProgrammaticScroll = true;
|
|
4871
|
+
this.clearPastLineHighlights();
|
|
4361
4872
|
this.animateScrollYouLy(targetTranslateY, false);
|
|
4362
4873
|
|
|
4363
4874
|
setTimeout(() => {
|
|
@@ -4372,16 +4883,21 @@ export class AmLyrics extends LitElement {
|
|
|
4372
4883
|
* Get the scroll padding top value from CSS variable
|
|
4373
4884
|
*/
|
|
4374
4885
|
private getScrollPaddingTop(): number {
|
|
4886
|
+
if (this.cachedScrollPaddingTop !== null)
|
|
4887
|
+
return this.cachedScrollPaddingTop;
|
|
4375
4888
|
if (!this.lyricsContainer) return 0;
|
|
4376
4889
|
const style = getComputedStyle(this);
|
|
4377
4890
|
const paddingTopValue =
|
|
4378
4891
|
style.getPropertyValue('--lyrics-scroll-padding-top') || '25%';
|
|
4892
|
+
let result: number;
|
|
4379
4893
|
if (paddingTopValue.includes('%')) {
|
|
4380
|
-
|
|
4381
|
-
this.lyricsContainer.clientHeight * (parseFloat(paddingTopValue) / 100)
|
|
4382
|
-
|
|
4894
|
+
result =
|
|
4895
|
+
this.lyricsContainer.clientHeight * (parseFloat(paddingTopValue) / 100);
|
|
4896
|
+
} else {
|
|
4897
|
+
result = parseFloat(paddingTopValue) || 0;
|
|
4383
4898
|
}
|
|
4384
|
-
|
|
4899
|
+
this.cachedScrollPaddingTop = result;
|
|
4900
|
+
return result;
|
|
4385
4901
|
}
|
|
4386
4902
|
|
|
4387
4903
|
/**
|
|
@@ -4394,6 +4910,7 @@ export class AmLyrics extends LitElement {
|
|
|
4394
4910
|
): void {
|
|
4395
4911
|
if (!this.lyricsContainer) return;
|
|
4396
4912
|
const parent = this.lyricsContainer;
|
|
4913
|
+
const targetTop = Math.max(0, -newTranslateY);
|
|
4397
4914
|
|
|
4398
4915
|
if (!this.scrollAnimationState) {
|
|
4399
4916
|
this.scrollAnimationState = {
|
|
@@ -4406,23 +4923,31 @@ export class AmLyrics extends LitElement {
|
|
|
4406
4923
|
const animState = this.scrollAnimationState;
|
|
4407
4924
|
|
|
4408
4925
|
if (animState.isAnimating && !forceScroll) {
|
|
4926
|
+
const pendingTop =
|
|
4927
|
+
animState.pendingUpdate === null
|
|
4928
|
+
? null
|
|
4929
|
+
: Math.max(0, -animState.pendingUpdate);
|
|
4930
|
+
if (
|
|
4931
|
+
Math.abs(parent.scrollTop - targetTop) < 2 ||
|
|
4932
|
+
(pendingTop !== null && Math.abs(pendingTop - targetTop) < 2)
|
|
4933
|
+
) {
|
|
4934
|
+
return;
|
|
4935
|
+
}
|
|
4409
4936
|
animState.pendingUpdate = newTranslateY;
|
|
4410
4937
|
return;
|
|
4411
4938
|
}
|
|
4412
4939
|
|
|
4413
|
-
if (this.scrollUnlockTimeout) {
|
|
4414
|
-
clearTimeout(this.scrollUnlockTimeout);
|
|
4415
|
-
this.scrollUnlockTimeout = undefined;
|
|
4416
|
-
}
|
|
4417
|
-
|
|
4418
4940
|
if (this.scrollAnimationTimeout) {
|
|
4419
4941
|
clearTimeout(this.scrollAnimationTimeout);
|
|
4420
4942
|
this.scrollAnimationTimeout = undefined;
|
|
4421
4943
|
}
|
|
4944
|
+
if (this.scrollUnlockTimeout) {
|
|
4945
|
+
clearTimeout(this.scrollUnlockTimeout);
|
|
4946
|
+
this.scrollUnlockTimeout = undefined;
|
|
4947
|
+
}
|
|
4422
4948
|
|
|
4423
4949
|
const { animatingLines } = this;
|
|
4424
4950
|
|
|
4425
|
-
const targetTop = Math.max(0, -newTranslateY);
|
|
4426
4951
|
const appliedTranslateY = -targetTop;
|
|
4427
4952
|
const prevOffset = -parent.scrollTop;
|
|
4428
4953
|
const delta = prevOffset - appliedTranslateY;
|
|
@@ -4439,7 +4964,6 @@ export class AmLyrics extends LitElement {
|
|
|
4439
4964
|
// Clean up any lingering scroll animations before smooth scroll
|
|
4440
4965
|
for (const line of animatingLines) {
|
|
4441
4966
|
line.classList.remove('scroll-animate');
|
|
4442
|
-
line.style.removeProperty('will-change');
|
|
4443
4967
|
line.style.removeProperty('--scroll-delta');
|
|
4444
4968
|
line.style.removeProperty('--lyrics-line-delay');
|
|
4445
4969
|
line.style.removeProperty('--scroll-duration');
|
|
@@ -4451,15 +4975,23 @@ export class AmLyrics extends LitElement {
|
|
|
4451
4975
|
return;
|
|
4452
4976
|
}
|
|
4453
4977
|
|
|
4454
|
-
// --- Step 1: Remove scroll-animate
|
|
4978
|
+
// --- Step 1: Remove scroll-animate and custom properties from ALL
|
|
4979
|
+
// previously animating lines so stale deltas don't interfere. ---
|
|
4455
4980
|
for (const line of animatingLines) {
|
|
4456
4981
|
line.classList.remove('scroll-animate');
|
|
4982
|
+
line.style.removeProperty('--scroll-delta');
|
|
4983
|
+
line.style.removeProperty('--lyrics-line-delay');
|
|
4984
|
+
line.style.removeProperty('--scroll-duration');
|
|
4457
4985
|
}
|
|
4458
4986
|
animatingLines.length = 0;
|
|
4459
4987
|
|
|
4460
|
-
// Get lines for staggered animation
|
|
4461
|
-
|
|
4462
|
-
|
|
4988
|
+
// Get lines for staggered animation — use cached array
|
|
4989
|
+
if (this.cachedLineArray.length === 0) {
|
|
4990
|
+
const lineElements =
|
|
4991
|
+
this.lyricsContainer.querySelectorAll('.lyrics-line');
|
|
4992
|
+
this.cachedLineArray = Array.from(lineElements) as HTMLElement[];
|
|
4993
|
+
}
|
|
4994
|
+
const lineArray = this.cachedLineArray;
|
|
4463
4995
|
|
|
4464
4996
|
const referenceLine =
|
|
4465
4997
|
this.currentPrimaryActiveLine ||
|
|
@@ -4471,58 +5003,83 @@ export class AmLyrics extends LitElement {
|
|
|
4471
5003
|
const referenceIndex = lineArray.indexOf(referenceLine);
|
|
4472
5004
|
if (referenceIndex === -1) return;
|
|
4473
5005
|
|
|
4474
|
-
const
|
|
4475
|
-
|
|
4476
|
-
|
|
5006
|
+
const duration = Math.min(
|
|
5007
|
+
450,
|
|
5008
|
+
scrollDuration ?? SCROLL_ANIMATION_DURATION_MS,
|
|
5009
|
+
);
|
|
5010
|
+
const delayIncrement = duration * 0.1;
|
|
5011
|
+
const lookAhead = 20;
|
|
4477
5012
|
const len = lineArray.length;
|
|
4478
5013
|
|
|
4479
|
-
const start = Math.max(0, referenceIndex -
|
|
5014
|
+
const start = Math.max(0, referenceIndex - lookAhead);
|
|
4480
5015
|
const end = Math.min(len, referenceIndex + lookAhead);
|
|
4481
5016
|
|
|
4482
5017
|
let maxAnimationDuration = 0;
|
|
4483
|
-
let delayCounter = 0;
|
|
4484
|
-
|
|
4485
|
-
// --- Step 2: Set CSS custom properties on target lines ---
|
|
4486
5018
|
const newAnimatingLines: HTMLElement[] = [];
|
|
5019
|
+
const scrollingDown = delta >= 0;
|
|
5020
|
+
|
|
5021
|
+
if (scrollingDown) {
|
|
5022
|
+
let delayCounter = 0;
|
|
5023
|
+
for (let i = start; i < end; i += 1) {
|
|
5024
|
+
const line = lineArray[i];
|
|
5025
|
+
const delay = i >= referenceIndex ? delayCounter * delayIncrement : 0;
|
|
5026
|
+
|
|
5027
|
+
if (i >= referenceIndex && !line.classList.contains('lyrics-gap')) {
|
|
5028
|
+
delayCounter += 1;
|
|
5029
|
+
}
|
|
5030
|
+
|
|
5031
|
+
line.style.setProperty('--scroll-delta', `${delta}px`);
|
|
5032
|
+
line.style.setProperty('--lyrics-line-delay', `${delay}ms`);
|
|
5033
|
+
line.style.setProperty('--scroll-duration', `${duration + 100}ms`);
|
|
5034
|
+
|
|
5035
|
+
newAnimatingLines.push(line);
|
|
4487
5036
|
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
5037
|
+
const lineDuration = duration + delay;
|
|
5038
|
+
if (lineDuration > maxAnimationDuration) {
|
|
5039
|
+
maxAnimationDuration = lineDuration;
|
|
5040
|
+
}
|
|
5041
|
+
}
|
|
5042
|
+
} else {
|
|
5043
|
+
let delayCounter = 0;
|
|
5044
|
+
for (let i = end - 1; i >= start; i -= 1) {
|
|
5045
|
+
const line = lineArray[i];
|
|
5046
|
+
const delay = i <= referenceIndex ? delayCounter * delayIncrement : 0;
|
|
4493
5047
|
|
|
4494
|
-
|
|
5048
|
+
if (i <= referenceIndex && !line.classList.contains('lyrics-gap')) {
|
|
5049
|
+
delayCounter += 1;
|
|
5050
|
+
}
|
|
4495
5051
|
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
5052
|
+
line.style.setProperty('--scroll-delta', `${delta}px`);
|
|
5053
|
+
line.style.setProperty('--lyrics-line-delay', `${delay}ms`);
|
|
5054
|
+
line.style.setProperty('--scroll-duration', `${duration + 100}ms`);
|
|
4499
5055
|
|
|
4500
|
-
|
|
5056
|
+
newAnimatingLines.push(line);
|
|
4501
5057
|
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
5058
|
+
const lineDuration = duration + delay;
|
|
5059
|
+
if (lineDuration > maxAnimationDuration) {
|
|
5060
|
+
maxAnimationDuration = lineDuration;
|
|
5061
|
+
}
|
|
4505
5062
|
}
|
|
4506
5063
|
}
|
|
4507
5064
|
|
|
4508
5065
|
// --- Step 3: Force reflow so the browser sees the class removal ---
|
|
4509
|
-
//
|
|
4510
|
-
//
|
|
4511
|
-
parent.
|
|
5066
|
+
// Use offsetHeight which is cheaper than getBoundingClientRect
|
|
5067
|
+
// eslint-disable-next-line no-void
|
|
5068
|
+
void parent.offsetHeight;
|
|
4512
5069
|
|
|
4513
5070
|
// --- Step 4: Re-add scroll-animate class to start fresh animations ---
|
|
4514
5071
|
for (const line of newAnimatingLines) {
|
|
4515
5072
|
line.classList.add('scroll-animate');
|
|
4516
|
-
line.style.willChange = 'transform';
|
|
4517
5073
|
animatingLines.push(line);
|
|
4518
5074
|
}
|
|
4519
5075
|
|
|
4520
5076
|
animState.isAnimating = true;
|
|
4521
|
-
const BASE_DURATION = scrollDuration ?? SCROLL_ANIMATION_DURATION_MS;
|
|
4522
5077
|
|
|
5078
|
+
// YouLyPlus-style early unlock: allow new scrolls to start after a
|
|
5079
|
+
// short base duration, even if CSS animations are still running.
|
|
5080
|
+
const BASE_DURATION = 400;
|
|
4523
5081
|
this.scrollUnlockTimeout = setTimeout(() => {
|
|
4524
5082
|
animState.isAnimating = false;
|
|
4525
|
-
|
|
4526
5083
|
if (animState.pendingUpdate !== null) {
|
|
4527
5084
|
const pendingValue = animState.pendingUpdate;
|
|
4528
5085
|
animState.pendingUpdate = null;
|
|
@@ -4534,7 +5091,6 @@ export class AmLyrics extends LitElement {
|
|
|
4534
5091
|
for (let i = 0; i < animatingLines.length; i += 1) {
|
|
4535
5092
|
const line = animatingLines[i];
|
|
4536
5093
|
line.classList.remove('scroll-animate');
|
|
4537
|
-
line.style.removeProperty('will-change');
|
|
4538
5094
|
line.style.removeProperty('--scroll-delta');
|
|
4539
5095
|
line.style.removeProperty('--lyrics-line-delay');
|
|
4540
5096
|
line.style.removeProperty('--scroll-duration');
|
|
@@ -4576,10 +5132,14 @@ export class AmLyrics extends LitElement {
|
|
|
4576
5132
|
lineToScroll.classList.add('lyrics-activest');
|
|
4577
5133
|
this.positionedLineElements.push(lineToScroll);
|
|
4578
5134
|
|
|
4579
|
-
|
|
4580
|
-
this.
|
|
4581
|
-
|
|
5135
|
+
if (this.cachedLineArray.length === 0) {
|
|
5136
|
+
this.cachedLineArray = Array.from(
|
|
5137
|
+
this.lyricsContainer.querySelectorAll('.lyrics-line'),
|
|
5138
|
+
) as HTMLElement[];
|
|
5139
|
+
}
|
|
5140
|
+
const lineElements = this.cachedLineArray;
|
|
4582
5141
|
const scrollLineIndex = lineElements.indexOf(lineToScroll);
|
|
5142
|
+
if (scrollLineIndex === -1) return;
|
|
4583
5143
|
|
|
4584
5144
|
for (
|
|
4585
5145
|
let i = Math.max(0, scrollLineIndex - 4);
|
|
@@ -4646,6 +5206,8 @@ export class AmLyrics extends LitElement {
|
|
|
4646
5206
|
this.userScrollTimeoutId = undefined;
|
|
4647
5207
|
}
|
|
4648
5208
|
|
|
5209
|
+
this.clearPastLineHighlights();
|
|
5210
|
+
|
|
4649
5211
|
const duration = scrollDuration ?? SCROLL_ANIMATION_DURATION_MS;
|
|
4650
5212
|
setTimeout(() => {
|
|
4651
5213
|
this.isProgrammaticScroll = false;
|
|
@@ -4674,6 +5236,7 @@ export class AmLyrics extends LitElement {
|
|
|
4674
5236
|
? (Array.from(wordElement.querySelectorAll('span.char')) as HTMLElement[])
|
|
4675
5237
|
: [];
|
|
4676
5238
|
const isGrowable = wordElement?.classList.contains('growable');
|
|
5239
|
+
const isCharRise = wordElement?.classList.contains('char-rise');
|
|
4677
5240
|
const isFirstSyllable =
|
|
4678
5241
|
syllable.getAttribute('data-syllable-index') === '0';
|
|
4679
5242
|
const isFirstInContainer = isFirstSyllable; // Simplified
|
|
@@ -4742,6 +5305,21 @@ export class AmLyrics extends LitElement {
|
|
|
4742
5305
|
});
|
|
4743
5306
|
}
|
|
4744
5307
|
|
|
5308
|
+
if (isCharRise && isFirstSyllable && allWordCharSpans.length > 0) {
|
|
5309
|
+
const finalDuration = Math.max(wordDurationMs, syllableDurationMs);
|
|
5310
|
+
const baseDelayPerChar = finalDuration * 0.09;
|
|
5311
|
+
const riseDurationMs = finalDuration * 1.5;
|
|
5312
|
+
|
|
5313
|
+
allWordCharSpans.forEach(span => {
|
|
5314
|
+
const charIndex = parseFloat(span.dataset.syllableCharIndex || '0');
|
|
5315
|
+
const riseDelay = baseDelayPerChar * charIndex;
|
|
5316
|
+
charAnimationsMap.set(
|
|
5317
|
+
span,
|
|
5318
|
+
`rise-char ${riseDurationMs}ms ease-in-out ${riseDelay}ms forwards`,
|
|
5319
|
+
);
|
|
5320
|
+
});
|
|
5321
|
+
}
|
|
5322
|
+
|
|
4745
5323
|
// Step 2: Wipe Pass
|
|
4746
5324
|
if (charSpans.length > 0) {
|
|
4747
5325
|
charSpans.forEach((span, charIndex) => {
|
|
@@ -4764,7 +5342,11 @@ export class AmLyrics extends LitElement {
|
|
|
4764
5342
|
|
|
4765
5343
|
const animationParts = [];
|
|
4766
5344
|
|
|
4767
|
-
if (
|
|
5345
|
+
if (
|
|
5346
|
+
existingAnimation &&
|
|
5347
|
+
(existingAnimation.includes('grow-dynamic') ||
|
|
5348
|
+
existingAnimation.includes('rise-char'))
|
|
5349
|
+
) {
|
|
4768
5350
|
animationParts.push(existingAnimation.split(',')[0].trim());
|
|
4769
5351
|
}
|
|
4770
5352
|
if (charIndex > 0) {
|
|
@@ -4846,13 +5428,13 @@ export class AmLyrics extends LitElement {
|
|
|
4846
5428
|
syllable.style.backgroundColor = 'var(--lyplus-text-secondary)';
|
|
4847
5429
|
|
|
4848
5430
|
// Reset character animations — disable transition so finished chars don't slowly fade
|
|
4849
|
-
syllable.querySelectorAll('span.char')
|
|
4850
|
-
|
|
5431
|
+
const charSpans = syllable.querySelectorAll('span.char');
|
|
5432
|
+
for (let i = 0; i < charSpans.length; i += 1) {
|
|
5433
|
+
const el = charSpans[i] as HTMLElement;
|
|
4851
5434
|
el.style.animation = '';
|
|
4852
|
-
el.style.willChange = '';
|
|
4853
5435
|
el.style.transition = 'none';
|
|
4854
5436
|
el.style.backgroundColor = 'var(--lyplus-text-secondary)';
|
|
4855
|
-
}
|
|
5437
|
+
}
|
|
4856
5438
|
|
|
4857
5439
|
// Immediately remove all state classes
|
|
4858
5440
|
syllable.classList.remove(
|
|
@@ -4861,30 +5443,124 @@ export class AmLyrics extends LitElement {
|
|
|
4861
5443
|
'pre-highlight',
|
|
4862
5444
|
'cleanup',
|
|
4863
5445
|
);
|
|
4864
|
-
|
|
4865
|
-
// In next frame, clear inline styles so CSS transitions can resume for future use
|
|
4866
|
-
requestAnimationFrame(() => {
|
|
4867
|
-
syllable.style.removeProperty('background-color');
|
|
4868
|
-
syllable.style.removeProperty('transition');
|
|
4869
|
-
syllable.querySelectorAll('span.char').forEach(span => {
|
|
4870
|
-
const el = span as HTMLElement;
|
|
4871
|
-
el.style.removeProperty('background-color');
|
|
4872
|
-
el.style.removeProperty('transition');
|
|
4873
|
-
el.style.removeProperty('will-change');
|
|
4874
|
-
});
|
|
4875
|
-
});
|
|
4876
5446
|
}
|
|
4877
5447
|
|
|
4878
5448
|
/**
|
|
4879
|
-
* Reset all syllables in a line
|
|
5449
|
+
* Reset all syllables in a line — batches deferred cleanup into a single rAF
|
|
4880
5450
|
*/
|
|
4881
5451
|
private static resetSyllables(line: HTMLElement): void {
|
|
4882
5452
|
if (!line) return;
|
|
5453
|
+
line.classList.remove('persist-highlight');
|
|
4883
5454
|
// eslint-disable-next-line no-param-reassign
|
|
4884
5455
|
(line as any)._cachedSyllableElements = null;
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
5456
|
+
const syllables = line.getElementsByClassName('lyrics-syllable');
|
|
5457
|
+
for (let i = 0; i < syllables.length; i += 1) {
|
|
5458
|
+
AmLyrics.resetSyllable(syllables[i] as HTMLElement);
|
|
5459
|
+
}
|
|
5460
|
+
// Batch deferred style cleanup into a single rAF for all syllables in the line
|
|
5461
|
+
requestAnimationFrame(() => {
|
|
5462
|
+
for (let i = 0; i < syllables.length; i += 1) {
|
|
5463
|
+
const syllable = syllables[i] as HTMLElement;
|
|
5464
|
+
syllable.style.removeProperty('background-color');
|
|
5465
|
+
syllable.style.removeProperty('transition');
|
|
5466
|
+
const chars = syllable.querySelectorAll('span.char');
|
|
5467
|
+
for (let j = 0; j < chars.length; j += 1) {
|
|
5468
|
+
const el = chars[j] as HTMLElement;
|
|
5469
|
+
el.style.removeProperty('background-color');
|
|
5470
|
+
el.style.removeProperty('transition');
|
|
5471
|
+
el.style.removeProperty('will-change');
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
});
|
|
5475
|
+
}
|
|
5476
|
+
|
|
5477
|
+
/**
|
|
5478
|
+
* Gentle reset for normal playback: remove highlight/finished classes
|
|
5479
|
+
* without forcing inline styles. Lets CSS transition fade syllables
|
|
5480
|
+
* back to secondary colour smoothly.
|
|
5481
|
+
*/
|
|
5482
|
+
private static unfinishSyllables(line: HTMLElement): void {
|
|
5483
|
+
if (!line) return;
|
|
5484
|
+
line.classList.remove('persist-highlight');
|
|
5485
|
+
const syllables = line.getElementsByClassName('lyrics-syllable');
|
|
5486
|
+
for (let i = 0; i < syllables.length; i += 1) {
|
|
5487
|
+
const s = syllables[i] as HTMLElement;
|
|
5488
|
+
s.classList.remove('highlight', 'finished', 'pre-highlight', 'cleanup');
|
|
5489
|
+
s.style.animation = '';
|
|
5490
|
+
s.style.removeProperty('--pre-wipe-duration');
|
|
5491
|
+
s.style.removeProperty('--pre-wipe-delay');
|
|
5492
|
+
s.style.removeProperty('background-color');
|
|
5493
|
+
s.style.removeProperty('transition');
|
|
5494
|
+
const chars = s.querySelectorAll('span.char');
|
|
5495
|
+
for (let j = 0; j < chars.length; j += 1) {
|
|
5496
|
+
const el = chars[j] as HTMLElement;
|
|
5497
|
+
el.style.animation = '';
|
|
5498
|
+
el.style.removeProperty('will-change');
|
|
5499
|
+
el.style.removeProperty('background-color');
|
|
5500
|
+
el.style.removeProperty('transition');
|
|
5501
|
+
el.style.removeProperty('filter');
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
5505
|
+
|
|
5506
|
+
private static finishSyllablesUpToTime(
|
|
5507
|
+
line: HTMLElement,
|
|
5508
|
+
currentTimeMs: number,
|
|
5509
|
+
): void {
|
|
5510
|
+
if (!line) return;
|
|
5511
|
+
let hasFinishedSyllable = false;
|
|
5512
|
+
|
|
5513
|
+
let syllables: HTMLElement[] = (line as any)._cachedSyllableElements;
|
|
5514
|
+
if (!syllables) {
|
|
5515
|
+
syllables = Array.from(
|
|
5516
|
+
line.querySelectorAll('.lyrics-syllable'),
|
|
5517
|
+
) as HTMLElement[];
|
|
5518
|
+
for (let i = 0; i < syllables.length; i += 1) {
|
|
5519
|
+
const syllable = syllables[i];
|
|
5520
|
+
(syllable as any)._cachedStartTime = parseFloat(
|
|
5521
|
+
syllable.getAttribute('data-start-time') || '0',
|
|
5522
|
+
);
|
|
5523
|
+
(syllable as any)._cachedEndTime = parseFloat(
|
|
5524
|
+
syllable.getAttribute('data-end-time') || '0',
|
|
5525
|
+
);
|
|
5526
|
+
}
|
|
5527
|
+
// eslint-disable-next-line no-param-reassign
|
|
5528
|
+
(line as any)._cachedSyllableElements = syllables;
|
|
5529
|
+
}
|
|
5530
|
+
|
|
5531
|
+
for (let i = 0; i < syllables.length; i += 1) {
|
|
5532
|
+
const syllable = syllables[i];
|
|
5533
|
+
const startTime = (syllable as any)._cachedStartTime;
|
|
5534
|
+
if (Number.isFinite(startTime) && currentTimeMs >= startTime) {
|
|
5535
|
+
const { classList } = syllable;
|
|
5536
|
+
if (!classList.contains('finished')) {
|
|
5537
|
+
if (!classList.contains('highlight')) {
|
|
5538
|
+
AmLyrics.updateSyllableAnimation(
|
|
5539
|
+
syllable,
|
|
5540
|
+
Math.max(0, currentTimeMs - startTime),
|
|
5541
|
+
);
|
|
5542
|
+
}
|
|
5543
|
+
classList.add('finished');
|
|
5544
|
+
}
|
|
5545
|
+
hasFinishedSyllable = true;
|
|
5546
|
+
classList.remove('highlight');
|
|
5547
|
+
classList.remove('pre-highlight');
|
|
5548
|
+
classList.add('cleanup');
|
|
5549
|
+
syllable.style.animation = '';
|
|
5550
|
+
syllable.style.removeProperty('--pre-wipe-duration');
|
|
5551
|
+
syllable.style.removeProperty('--pre-wipe-delay');
|
|
5552
|
+
const chars = syllable.querySelectorAll('span.char');
|
|
5553
|
+
for (let ci = 0; ci < chars.length; ci += 1) {
|
|
5554
|
+
(chars[ci] as HTMLElement).style.animation = '';
|
|
5555
|
+
}
|
|
5556
|
+
}
|
|
5557
|
+
}
|
|
5558
|
+
|
|
5559
|
+
if (hasFinishedSyllable) {
|
|
5560
|
+
line.classList.add('persist-highlight');
|
|
5561
|
+
} else {
|
|
5562
|
+
line.classList.remove('persist-highlight');
|
|
5563
|
+
}
|
|
4888
5564
|
}
|
|
4889
5565
|
|
|
4890
5566
|
/**
|
|
@@ -4901,16 +5577,23 @@ export class AmLyrics extends LitElement {
|
|
|
4901
5577
|
syllables = Array.from(
|
|
4902
5578
|
line.querySelectorAll('.lyrics-syllable'),
|
|
4903
5579
|
) as HTMLElement[];
|
|
5580
|
+
for (let i = 0; i < syllables.length; i += 1) {
|
|
5581
|
+
const syllable = syllables[i];
|
|
5582
|
+
(syllable as any)._cachedStartTime = parseFloat(
|
|
5583
|
+
syllable.getAttribute('data-start-time') || '0',
|
|
5584
|
+
);
|
|
5585
|
+
(syllable as any)._cachedEndTime = parseFloat(
|
|
5586
|
+
syllable.getAttribute('data-end-time') || '0',
|
|
5587
|
+
);
|
|
5588
|
+
}
|
|
4904
5589
|
// eslint-disable-next-line no-param-reassign
|
|
4905
5590
|
(line as any)._cachedSyllableElements = syllables;
|
|
4906
5591
|
}
|
|
4907
5592
|
|
|
4908
5593
|
for (let i = 0; i < syllables.length; i += 1) {
|
|
4909
5594
|
const syllable = syllables[i];
|
|
4910
|
-
const startTime =
|
|
4911
|
-
|
|
4912
|
-
);
|
|
4913
|
-
const endTime = parseFloat(syllable.getAttribute('data-end-time') || '0');
|
|
5595
|
+
const startTime = (syllable as any)._cachedStartTime;
|
|
5596
|
+
const endTime = (syllable as any)._cachedEndTime;
|
|
4914
5597
|
|
|
4915
5598
|
if (Number.isFinite(startTime) && Number.isFinite(endTime)) {
|
|
4916
5599
|
const { classList } = syllable;
|
|
@@ -4957,6 +5640,7 @@ export class AmLyrics extends LitElement {
|
|
|
4957
5640
|
);
|
|
4958
5641
|
}
|
|
4959
5642
|
classList.add('finished');
|
|
5643
|
+
// Keep the completed wipe state until user scroll resets it.
|
|
4960
5644
|
}
|
|
4961
5645
|
} else if (hasHighlight || hasFinished) {
|
|
4962
5646
|
// Not yet started
|
|
@@ -5286,47 +5970,49 @@ export class AmLyrics extends LitElement {
|
|
|
5286
5970
|
// Create background vocals container (with romanization support)
|
|
5287
5971
|
const backgroundVocalElement = hasBackground
|
|
5288
5972
|
? html`<p class="background-vocal-container">
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5973
|
+
<span class="background-vocal-wrap">
|
|
5974
|
+
${line.backgroundText!.map((syllable, syllableIndex) => {
|
|
5975
|
+
const startTimeMs = syllable.timestamp;
|
|
5976
|
+
const endTimeMs = syllable.endtime;
|
|
5977
|
+
const durationMs = endTimeMs - startTimeMs;
|
|
5978
|
+
|
|
5979
|
+
const bgRomanizedText =
|
|
5980
|
+
this.showRomanization &&
|
|
5981
|
+
syllable.romanizedText &&
|
|
5982
|
+
syllable.romanizedText.trim() !== syllable.text.trim()
|
|
5983
|
+
? html`<span
|
|
5984
|
+
class="lyrics-syllable transliteration no-chars ${syllable.lineSynced
|
|
5985
|
+
? 'line-synced'
|
|
5986
|
+
: ''}"
|
|
5987
|
+
data-start-time="${startTimeMs}"
|
|
5988
|
+
data-end-time="${endTimeMs}"
|
|
5989
|
+
data-duration="${durationMs}"
|
|
5990
|
+
data-syllable-index="0"
|
|
5991
|
+
data-wipe-ratio="1"
|
|
5992
|
+
>${syllable.romanizedText}</span
|
|
5993
|
+
>`
|
|
5994
|
+
: '';
|
|
5995
|
+
|
|
5996
|
+
return html`<span class="lyrics-word"
|
|
5997
|
+
><span
|
|
5998
|
+
class="lyrics-syllable-wrap${bgRomanizedText
|
|
5999
|
+
? ' has-transliteration'
|
|
6000
|
+
: ''}"
|
|
6001
|
+
><span
|
|
6002
|
+
class="lyrics-syllable no-chars${syllable.lineSynced
|
|
6003
|
+
? ' line-synced'
|
|
5301
6004
|
: ''}"
|
|
5302
6005
|
data-start-time="${startTimeMs}"
|
|
5303
6006
|
data-end-time="${endTimeMs}"
|
|
5304
6007
|
data-duration="${durationMs}"
|
|
5305
|
-
data-syllable-index="
|
|
6008
|
+
data-syllable-index="${syllableIndex}"
|
|
5306
6009
|
data-wipe-ratio="1"
|
|
5307
|
-
>${syllable.
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
class="lyrics-syllable-wrap${bgRomanizedText
|
|
5314
|
-
? ' has-transliteration'
|
|
5315
|
-
: ''}"
|
|
5316
|
-
><span
|
|
5317
|
-
class="lyrics-syllable no-chars${syllable.lineSynced
|
|
5318
|
-
? ' line-synced'
|
|
5319
|
-
: ''}"
|
|
5320
|
-
data-start-time="${startTimeMs}"
|
|
5321
|
-
data-end-time="${endTimeMs}"
|
|
5322
|
-
data-duration="${durationMs}"
|
|
5323
|
-
data-syllable-index="${syllableIndex}"
|
|
5324
|
-
data-wipe-ratio="1"
|
|
5325
|
-
>${syllable.text}</span
|
|
5326
|
-
>${bgRomanizedText}</span
|
|
5327
|
-
></span
|
|
5328
|
-
>`;
|
|
5329
|
-
})}
|
|
6010
|
+
>${syllable.text}</span
|
|
6011
|
+
>${bgRomanizedText}</span
|
|
6012
|
+
></span
|
|
6013
|
+
>`;
|
|
6014
|
+
})}
|
|
6015
|
+
</span>
|
|
5330
6016
|
</p>`
|
|
5331
6017
|
: '';
|
|
5332
6018
|
|
|
@@ -5343,6 +6029,7 @@ export class AmLyrics extends LitElement {
|
|
|
5343
6029
|
const wordGroups = lineData?.wordGroups ?? [];
|
|
5344
6030
|
const groupGrowable = lineData?.groupGrowable ?? [];
|
|
5345
6031
|
const groupGlowing = lineData?.groupGlowing ?? [];
|
|
6032
|
+
const groupCharRise = lineData?.groupCharRise ?? [];
|
|
5346
6033
|
const vwFullText = lineData?.vwFullText ?? [];
|
|
5347
6034
|
const vwFullDuration = lineData?.vwFullDuration ?? [];
|
|
5348
6035
|
const vwCharOffset = lineData?.vwCharOffset ?? [];
|
|
@@ -5355,12 +6042,18 @@ export class AmLyrics extends LitElement {
|
|
|
5355
6042
|
${wordGroups.map((group, groupIdx) => {
|
|
5356
6043
|
const isGrowable = groupGrowable[groupIdx];
|
|
5357
6044
|
const isGlowing = groupGlowing[groupIdx];
|
|
6045
|
+
const isCharRise = groupCharRise[groupIdx];
|
|
6046
|
+
const isAnimatedByChar = isGrowable || isCharRise;
|
|
5358
6047
|
const groupLineSynced = group.some(s => s.lineSynced);
|
|
5359
6048
|
|
|
5360
|
-
const wordText =
|
|
5361
|
-
const wordDuration =
|
|
6049
|
+
const wordText = isAnimatedByChar ? vwFullText[groupIdx] : '';
|
|
6050
|
+
const wordDuration = isAnimatedByChar
|
|
6051
|
+
? vwFullDuration[groupIdx]
|
|
6052
|
+
: 0;
|
|
5362
6053
|
const wordNumChars = wordText.length;
|
|
5363
|
-
const groupCharOffset =
|
|
6054
|
+
const groupCharOffset = isAnimatedByChar
|
|
6055
|
+
? vwCharOffset[groupIdx]
|
|
6056
|
+
: 0;
|
|
5364
6057
|
|
|
5365
6058
|
let sylCharAccumulator = 0;
|
|
5366
6059
|
|
|
@@ -5382,9 +6075,11 @@ export class AmLyrics extends LitElement {
|
|
|
5382
6075
|
);
|
|
5383
6076
|
|
|
5384
6077
|
return html`<span
|
|
5385
|
-
class="lyrics-word${isGrowable ? ' growable' : ''}${
|
|
5386
|
-
? '
|
|
5387
|
-
: ''}${
|
|
6078
|
+
class="lyrics-word${isGrowable ? ' growable' : ''}${isCharRise
|
|
6079
|
+
? ' char-rise'
|
|
6080
|
+
: ''}${isGlowing ? ' glowing' : ''}${shouldAllowBreak
|
|
6081
|
+
? ' allow-break'
|
|
6082
|
+
: ''}"
|
|
5388
6083
|
style="--rise-duration: ${riseDuration}s"
|
|
5389
6084
|
>${group.map((syllable, sylIdx) => {
|
|
5390
6085
|
const startTimeMs = syllable.timestamp;
|
|
@@ -5411,7 +6106,7 @@ export class AmLyrics extends LitElement {
|
|
|
5411
6106
|
|
|
5412
6107
|
let syllableContent: any = text;
|
|
5413
6108
|
|
|
5414
|
-
if (
|
|
6109
|
+
if (isAnimatedByChar) {
|
|
5415
6110
|
let charIndexInsideSyllable = 0;
|
|
5416
6111
|
const numCharsInSyllable =
|
|
5417
6112
|
text.replace(/\s/g, '').length || 1;
|
|
@@ -5519,7 +6214,7 @@ export class AmLyrics extends LitElement {
|
|
|
5519
6214
|
><span
|
|
5520
6215
|
class="lyrics-syllable${groupLineSynced
|
|
5521
6216
|
? ' line-synced'
|
|
5522
|
-
: ''}${
|
|
6217
|
+
: ''}${isAnimatedByChar ? ' has-chars' : ' no-chars'}"
|
|
5523
6218
|
data-start-time="${startTimeMs}"
|
|
5524
6219
|
data-end-time="${endTimeMs}"
|
|
5525
6220
|
data-duration="${durationMs}"
|