@uimaxbai/am-lyrics 1.5.4 → 1.5.6
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 +31 -1
- package/dist/src/AmLyrics.d.ts.map +1 -1
- package/dist/src/am-lyrics.js +934 -319
- package/dist/src/am-lyrics.js.map +1 -1
- package/dist/src/react.js +934 -319
- package/dist/src/react.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/AmLyrics.ts +1246 -353
package/src/AmLyrics.ts
CHANGED
|
@@ -2,7 +2,7 @@ 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.
|
|
5
|
+
const VERSION = '1.5.6';
|
|
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;
|
|
@@ -11,6 +11,15 @@ const GAP_PULSE_DURATION_MS = 4000;
|
|
|
11
11
|
const GAP_PULSE_CYCLE_MS = GAP_PULSE_DURATION_MS * 2;
|
|
12
12
|
const GAP_EXIT_LEAD_MS = 600;
|
|
13
13
|
const GAP_MIN_SCALE = 0.85;
|
|
14
|
+
const NEXT_WORD_PRE_WIPE_MAX_GAP_MS = 180;
|
|
15
|
+
const NEXT_WORD_PRE_WIPE_MIN_DURATION_MS = 80;
|
|
16
|
+
const NEXT_WORD_PRE_WIPE_MAX_DURATION_MS = 240;
|
|
17
|
+
const BASE_WIPE_GRADIENT_EM = 0.75;
|
|
18
|
+
const LONG_WORD_WIPE_EXTRA_EM = 0.45;
|
|
19
|
+
const LONG_WORD_WIPE_EXTRA_RATIO = 0.35;
|
|
20
|
+
const SHORT_WORD_DRAG_MIN_DURATION_MS = 760;
|
|
21
|
+
const SHORT_WORD_GLOW_MIN_DURATION_MS = 1320;
|
|
22
|
+
const WORD_PRE_WIPE_HANDOFF_LEAD_MS = 100;
|
|
14
23
|
|
|
15
24
|
/**
|
|
16
25
|
* Fetch with an automatic timeout via AbortSignal.
|
|
@@ -105,9 +114,6 @@ interface ResolvedMetadata {
|
|
|
105
114
|
|
|
106
115
|
export class AmLyrics extends LitElement {
|
|
107
116
|
static styles = css`
|
|
108
|
-
/* ==========================================================================
|
|
109
|
-
YOULYPLUS-INSPIRED STYLING - Design Tokens & Variables
|
|
110
|
-
========================================================================== */
|
|
111
117
|
:host {
|
|
112
118
|
--lyplus-lyrics-palette: var(
|
|
113
119
|
--am-lyrics-highlight-color,
|
|
@@ -136,6 +142,8 @@ export class AmLyrics extends LitElement {
|
|
|
136
142
|
--lyplus-blur-amount: 0.07em;
|
|
137
143
|
--lyplus-blur-amount-near: 0.035em;
|
|
138
144
|
--lyplus-fade-gap-timing-function: ease-out;
|
|
145
|
+
--wipe-gradient-width: 0.75em;
|
|
146
|
+
--wipe-gradient-half: 0.375em;
|
|
139
147
|
|
|
140
148
|
--lyrics-scroll-padding-top: 25%;
|
|
141
149
|
|
|
@@ -163,6 +171,9 @@ export class AmLyrics extends LitElement {
|
|
|
163
171
|
max-height: 100vh;
|
|
164
172
|
overflow-y: auto;
|
|
165
173
|
-webkit-overflow-scrolling: touch;
|
|
174
|
+
-webkit-touch-callout: none;
|
|
175
|
+
-webkit-user-select: none;
|
|
176
|
+
user-select: none;
|
|
166
177
|
box-sizing: border-box;
|
|
167
178
|
scrollbar-width: none;
|
|
168
179
|
overflow-anchor: none;
|
|
@@ -292,7 +303,7 @@ export class AmLyrics extends LitElement {
|
|
|
292
303
|
.lyrics-line.bg-expanded .background-vocal-container {
|
|
293
304
|
max-height: 4em;
|
|
294
305
|
opacity: 1;
|
|
295
|
-
will-change:
|
|
306
|
+
will-change: opacity;
|
|
296
307
|
}
|
|
297
308
|
|
|
298
309
|
.lyrics-line.bg-expanded .background-vocal-wrap {
|
|
@@ -309,6 +320,18 @@ export class AmLyrics extends LitElement {
|
|
|
309
320
|
opacity: 1;
|
|
310
321
|
}
|
|
311
322
|
|
|
323
|
+
/* Predictive scrolling begins before the next timestamp. Start dimming
|
|
324
|
+
the outgoing line at the same moment so it settles with the scroll. */
|
|
325
|
+
.lyrics-line.scroll-exiting {
|
|
326
|
+
opacity: 0.8;
|
|
327
|
+
color: var(--lyplus-text-secondary);
|
|
328
|
+
transition:
|
|
329
|
+
opacity var(--scroll-duration, 400ms) cubic-bezier(0.41, 0, 0.12, 0.99),
|
|
330
|
+
transform var(--scroll-duration, 400ms)
|
|
331
|
+
cubic-bezier(0.41, 0, 0.12, 0.99) var(--lyrics-line-delay, 0ms),
|
|
332
|
+
filter var(--scroll-duration, 400ms) ease;
|
|
333
|
+
}
|
|
334
|
+
|
|
312
335
|
.lyrics-line.persist-highlight {
|
|
313
336
|
filter: none !important;
|
|
314
337
|
opacity: 1;
|
|
@@ -444,11 +467,22 @@ export class AmLyrics extends LitElement {
|
|
|
444
467
|
white-space: nowrap;
|
|
445
468
|
}
|
|
446
469
|
|
|
470
|
+
.lyrics-word.char-drag {
|
|
471
|
+
display: inline-block;
|
|
472
|
+
vertical-align: baseline;
|
|
473
|
+
white-space: nowrap;
|
|
474
|
+
}
|
|
475
|
+
|
|
447
476
|
.lyrics-word.char-rise.allow-break {
|
|
448
477
|
display: inline;
|
|
449
478
|
white-space: normal;
|
|
450
479
|
}
|
|
451
480
|
|
|
481
|
+
.lyrics-word.char-drag.allow-break {
|
|
482
|
+
display: inline;
|
|
483
|
+
white-space: normal;
|
|
484
|
+
}
|
|
485
|
+
|
|
452
486
|
.lyrics-syllable-wrap {
|
|
453
487
|
display: inline;
|
|
454
488
|
}
|
|
@@ -503,44 +537,30 @@ export class AmLyrics extends LitElement {
|
|
|
503
537
|
.lyrics-line.active:not(.lyrics-gap)
|
|
504
538
|
.lyrics-syllable.pre-highlight.no-chars {
|
|
505
539
|
background-repeat: no-repeat;
|
|
506
|
-
background-image:
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
var(--
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
var(--lyplus-text-primary, #fff) 100%,
|
|
516
|
-
#0000 100%
|
|
517
|
-
);
|
|
518
|
-
background-size:
|
|
519
|
-
0.5em 100%,
|
|
520
|
-
0% 100%;
|
|
521
|
-
background-position:
|
|
522
|
-
-0.5em 0%,
|
|
523
|
-
-0.25em 0%;
|
|
540
|
+
background-image: linear-gradient(
|
|
541
|
+
90deg,
|
|
542
|
+
var(--lyplus-text-primary, #fff) 0%,
|
|
543
|
+
var(--lyplus-text-primary, #fff)
|
|
544
|
+
calc(100% - var(--wipe-gradient-width, 0.75em)),
|
|
545
|
+
#0000 100%
|
|
546
|
+
);
|
|
547
|
+
background-size: 0% 100%;
|
|
548
|
+
background-position: left;
|
|
524
549
|
}
|
|
525
550
|
|
|
526
551
|
.lyrics-line.active:not(.lyrics-gap) .lyrics-syllable.highlight.rtl-text,
|
|
527
552
|
.lyrics-line.active:not(.lyrics-gap)
|
|
528
553
|
.lyrics-syllable.pre-highlight.rtl-text {
|
|
529
554
|
direction: rtl;
|
|
530
|
-
background-image:
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
transparent 100%
|
|
540
|
-
);
|
|
541
|
-
background-position:
|
|
542
|
-
calc(100% + 0.5em) 0%,
|
|
543
|
-
right 0%;
|
|
555
|
+
background-image: linear-gradient(
|
|
556
|
+
-90deg,
|
|
557
|
+
var(--lyplus-text-primary) 0%,
|
|
558
|
+
var(--lyplus-text-primary)
|
|
559
|
+
calc(100% - var(--wipe-gradient-width, 0.75em)),
|
|
560
|
+
transparent 100%
|
|
561
|
+
);
|
|
562
|
+
background-size: 0% 100%;
|
|
563
|
+
background-position: right 0%;
|
|
544
564
|
}
|
|
545
565
|
|
|
546
566
|
/* Background vocals: muted gray wipe instead of white.
|
|
@@ -557,18 +577,13 @@ export class AmLyrics extends LitElement {
|
|
|
557
577
|
.lyrics-line.pre-active
|
|
558
578
|
.background-vocal-container
|
|
559
579
|
.lyrics-syllable.pre-highlight.no-chars {
|
|
560
|
-
background-image:
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
linear-gradient(
|
|
568
|
-
90deg,
|
|
569
|
-
color-mix(in srgb, var(--lyplus-text-primary, #fff) 50%, #888888) 100%,
|
|
570
|
-
#0000 100%
|
|
571
|
-
);
|
|
580
|
+
background-image: linear-gradient(
|
|
581
|
+
90deg,
|
|
582
|
+
color-mix(in srgb, var(--lyplus-text-primary, #fff) 50%, #888888) 0%,
|
|
583
|
+
color-mix(in srgb, var(--lyplus-text-primary, #fff) 50%, #888888)
|
|
584
|
+
calc(100% - var(--wipe-gradient-width, 0.75em)),
|
|
585
|
+
#0000 100%
|
|
586
|
+
);
|
|
572
587
|
}
|
|
573
588
|
|
|
574
589
|
.lyrics-line.active
|
|
@@ -583,17 +598,13 @@ export class AmLyrics extends LitElement {
|
|
|
583
598
|
.lyrics-line.pre-active
|
|
584
599
|
.background-vocal-container
|
|
585
600
|
.lyrics-syllable.pre-highlight.rtl-text {
|
|
586
|
-
background-image:
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
-90deg,
|
|
594
|
-
color-mix(in srgb, var(--lyplus-text-primary) 50%, #888888) 100%,
|
|
595
|
-
transparent 100%
|
|
596
|
-
);
|
|
601
|
+
background-image: linear-gradient(
|
|
602
|
+
-90deg,
|
|
603
|
+
color-mix(in srgb, var(--lyplus-text-primary) 50%, #888888) 0%,
|
|
604
|
+
color-mix(in srgb, var(--lyplus-text-primary) 50%, #888888)
|
|
605
|
+
calc(100% - var(--wipe-gradient-width, 0.75em)),
|
|
606
|
+
transparent 100%
|
|
607
|
+
);
|
|
597
608
|
}
|
|
598
609
|
|
|
599
610
|
/* Non-growable words float up with a gentle curve */
|
|
@@ -617,6 +628,10 @@ export class AmLyrics extends LitElement {
|
|
|
617
628
|
transform: translate3d(0, var(--char-rise-y, -1.12px), 0);
|
|
618
629
|
}
|
|
619
630
|
|
|
631
|
+
.lyrics-word.char-drag .lyrics-syllable.cleanup .char {
|
|
632
|
+
transform: translate3d(0, var(--char-rise-y, -1.12px), 0);
|
|
633
|
+
}
|
|
634
|
+
|
|
620
635
|
.lyrics-line.persist-highlight
|
|
621
636
|
.lyrics-word.growable
|
|
622
637
|
.lyrics-syllable.finished
|
|
@@ -624,6 +639,10 @@ export class AmLyrics extends LitElement {
|
|
|
624
639
|
.lyrics-line.persist-highlight
|
|
625
640
|
.lyrics-word.char-rise
|
|
626
641
|
.lyrics-syllable.finished
|
|
642
|
+
.char,
|
|
643
|
+
.lyrics-line.persist-highlight
|
|
644
|
+
.lyrics-word.char-drag
|
|
645
|
+
.lyrics-syllable.finished
|
|
627
646
|
.char {
|
|
628
647
|
transform: translate3d(0, var(--char-rise-y, -1.12px), 0);
|
|
629
648
|
}
|
|
@@ -734,6 +753,10 @@ export class AmLyrics extends LitElement {
|
|
|
734
753
|
transform 0.7s ease;
|
|
735
754
|
}
|
|
736
755
|
|
|
756
|
+
.lyrics-word.char-drag span.char {
|
|
757
|
+
transition: color 0.18s;
|
|
758
|
+
}
|
|
759
|
+
|
|
737
760
|
/* Active char spans: structural only, wipe animation sets gradient */
|
|
738
761
|
.lyrics-line.active .lyrics-syllable span.char {
|
|
739
762
|
background-clip: text;
|
|
@@ -752,52 +775,34 @@ export class AmLyrics extends LitElement {
|
|
|
752
775
|
#0000 100%
|
|
753
776
|
);
|
|
754
777
|
background-size:
|
|
755
|
-
0.
|
|
778
|
+
var(--wipe-gradient-width, 0.75em) 100%,
|
|
756
779
|
0% 100%;
|
|
757
780
|
background-position:
|
|
758
|
-
-0.
|
|
759
|
-
|
|
781
|
+
calc(-1 * var(--wipe-gradient-width, 0.75em)) 0%,
|
|
782
|
+
left;
|
|
760
783
|
transition:
|
|
761
784
|
transform 0.7s ease,
|
|
762
785
|
color 0.18s;
|
|
763
786
|
}
|
|
764
787
|
|
|
765
788
|
.lyrics-line.active .lyrics-syllable span.char.highlight {
|
|
766
|
-
background-image:
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
#0000 100%
|
|
776
|
-
);
|
|
777
|
-
background-position:
|
|
778
|
-
calc(100% + 0.5em) 0%,
|
|
779
|
-
calc(100% + 0.25em) 0%;
|
|
789
|
+
background-image: linear-gradient(
|
|
790
|
+
-90deg,
|
|
791
|
+
var(--lyplus-text-primary, #fff) 0%,
|
|
792
|
+
var(--lyplus-text-primary, #fff)
|
|
793
|
+
calc(100% - var(--wipe-gradient-width, 0.75em)),
|
|
794
|
+
#0000 100%
|
|
795
|
+
);
|
|
796
|
+
background-size: 0% 100%;
|
|
797
|
+
background-position: right 0%;
|
|
780
798
|
}
|
|
781
799
|
|
|
782
|
-
.lyrics-line.active .lyrics-syllable
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
#0000 100%
|
|
789
|
-
),
|
|
790
|
-
linear-gradient(
|
|
791
|
-
90deg,
|
|
792
|
-
var(--lyplus-text-primary, #fff) 100%,
|
|
793
|
-
#0000 100%
|
|
794
|
-
);
|
|
795
|
-
background-size:
|
|
796
|
-
0.75em 100%,
|
|
797
|
-
0% 100%;
|
|
798
|
-
background-position:
|
|
799
|
-
-0.85em 0%,
|
|
800
|
-
-0.25em 0%;
|
|
800
|
+
.lyrics-line.active .lyrics-syllable span.char.pre-wipe-lead {
|
|
801
|
+
animation-name: char-pre-wipe;
|
|
802
|
+
animation-duration: var(--pre-wipe-duration);
|
|
803
|
+
animation-delay: var(--pre-wipe-delay);
|
|
804
|
+
animation-timing-function: linear;
|
|
805
|
+
animation-fill-mode: forwards;
|
|
801
806
|
}
|
|
802
807
|
|
|
803
808
|
/* ==========================================================================
|
|
@@ -874,13 +879,7 @@ export class AmLyrics extends LitElement {
|
|
|
874
879
|
color: var(--lyplus-text-primary) !important;
|
|
875
880
|
}
|
|
876
881
|
|
|
877
|
-
.lyrics-line.
|
|
878
|
-
animation: fade-in-line 0.14s ease-out forwards !important;
|
|
879
|
-
color: var(--lyplus-text-primary) !important;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
.lyrics-line.active .lyrics-syllable.line-synced span.char,
|
|
883
|
-
.lyrics-line.pre-active .lyrics-syllable.line-synced span.char {
|
|
882
|
+
.lyrics-line.active .lyrics-syllable.line-synced span.char {
|
|
884
883
|
background-image: none !important;
|
|
885
884
|
background-color: var(--lyplus-text-primary) !important;
|
|
886
885
|
transition: background-color 120ms ease-out !important;
|
|
@@ -1053,6 +1052,49 @@ export class AmLyrics extends LitElement {
|
|
|
1053
1052
|
gap: 4px;
|
|
1054
1053
|
}
|
|
1055
1054
|
|
|
1055
|
+
.source-switch-btn {
|
|
1056
|
+
position: relative;
|
|
1057
|
+
display: inline-flex;
|
|
1058
|
+
align-items: center;
|
|
1059
|
+
padding: 2px 8px;
|
|
1060
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
1061
|
+
min-height: 28px;
|
|
1062
|
+
background: transparent;
|
|
1063
|
+
border-radius: 6px;
|
|
1064
|
+
color: #aaa;
|
|
1065
|
+
cursor: pointer;
|
|
1066
|
+
font-family: inherit;
|
|
1067
|
+
font-size: 11px;
|
|
1068
|
+
transition:
|
|
1069
|
+
color 0.2s ease,
|
|
1070
|
+
border-color 0.2s ease,
|
|
1071
|
+
background-color 0.2s ease,
|
|
1072
|
+
transform 0.12s ease;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
.source-switch-btn::before {
|
|
1076
|
+
content: '';
|
|
1077
|
+
position: absolute;
|
|
1078
|
+
inset: -6px;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
.source-switch-btn:active:not(:disabled) {
|
|
1082
|
+
transform: scale(0.96);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
.source-switch-btn:disabled {
|
|
1086
|
+
cursor: default;
|
|
1087
|
+
opacity: 0.7;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.source-switch-svg {
|
|
1091
|
+
margin-right: 4px;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
.source-switch-svg.is-loading {
|
|
1095
|
+
animation: source-switch-spin 1s linear infinite;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1056
1098
|
.control-button {
|
|
1057
1099
|
background: transparent;
|
|
1058
1100
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
@@ -1061,7 +1103,10 @@ export class AmLyrics extends LitElement {
|
|
|
1061
1103
|
font-size: 0.8em;
|
|
1062
1104
|
color: rgba(255, 255, 255, 0.6);
|
|
1063
1105
|
cursor: pointer;
|
|
1064
|
-
transition:
|
|
1106
|
+
transition:
|
|
1107
|
+
color 0.2s,
|
|
1108
|
+
border-color 0.2s,
|
|
1109
|
+
background-color 0.2s;
|
|
1065
1110
|
font-weight: normal;
|
|
1066
1111
|
}
|
|
1067
1112
|
|
|
@@ -1199,136 +1244,157 @@ export class AmLyrics extends LitElement {
|
|
|
1199
1244
|
KEYFRAME ANIMATIONS
|
|
1200
1245
|
========================================================================== */
|
|
1201
1246
|
|
|
1247
|
+
@keyframes source-switch-spin {
|
|
1248
|
+
to {
|
|
1249
|
+
transform: rotate(360deg);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1202
1253
|
/* Wipe animation for syllables */
|
|
1203
1254
|
@keyframes wipe {
|
|
1204
1255
|
from {
|
|
1205
|
-
background-size:
|
|
1206
|
-
|
|
1207
|
-
0% 100%;
|
|
1208
|
-
background-position:
|
|
1209
|
-
-0.375em 0%,
|
|
1210
|
-
left;
|
|
1256
|
+
background-size: 0% 100%;
|
|
1257
|
+
background-position: left;
|
|
1211
1258
|
}
|
|
1212
1259
|
to {
|
|
1213
|
-
background-size:
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1260
|
+
background-size: calc(100% + var(--wipe-gradient-width, 0.75em)) 100%;
|
|
1261
|
+
background-position: left;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
@keyframes wipe-from-pre {
|
|
1266
|
+
from {
|
|
1267
|
+
background-size: var(--wipe-gradient-width, 0.75em) 100%;
|
|
1268
|
+
background-position: left;
|
|
1269
|
+
}
|
|
1270
|
+
to {
|
|
1271
|
+
background-size: calc(100% + var(--wipe-gradient-width, 0.75em)) 100%;
|
|
1272
|
+
background-position: left;
|
|
1219
1273
|
}
|
|
1220
1274
|
}
|
|
1221
1275
|
|
|
1222
1276
|
@keyframes start-wipe {
|
|
1223
1277
|
0% {
|
|
1224
|
-
background-size:
|
|
1225
|
-
|
|
1226
|
-
0% 100%;
|
|
1227
|
-
background-position:
|
|
1228
|
-
-0.75em 0%,
|
|
1229
|
-
-0.375em 0%;
|
|
1278
|
+
background-size: 0% 100%;
|
|
1279
|
+
background-position: left;
|
|
1230
1280
|
}
|
|
1231
1281
|
100% {
|
|
1232
|
-
background-size:
|
|
1233
|
-
|
|
1234
|
-
100% 100%;
|
|
1235
|
-
background-position:
|
|
1236
|
-
calc(100% + 0.375em) 0%,
|
|
1237
|
-
left;
|
|
1282
|
+
background-size: calc(100% + var(--wipe-gradient-width, 0.75em)) 100%;
|
|
1283
|
+
background-position: left;
|
|
1238
1284
|
}
|
|
1239
1285
|
}
|
|
1240
1286
|
|
|
1241
1287
|
@keyframes wipe-rtl {
|
|
1242
1288
|
from {
|
|
1243
|
-
background-size:
|
|
1244
|
-
|
|
1245
|
-
0% 100%;
|
|
1246
|
-
background-position:
|
|
1247
|
-
calc(100% + 0.375em) 0%,
|
|
1248
|
-
calc(100% + 0.36em) 0%;
|
|
1289
|
+
background-size: 0% 100%;
|
|
1290
|
+
background-position: right 0%;
|
|
1249
1291
|
}
|
|
1250
1292
|
to {
|
|
1251
|
-
background-size:
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1293
|
+
background-size: calc(100% + var(--wipe-gradient-width, 0.75em)) 100%;
|
|
1294
|
+
background-position: right 0%;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
@keyframes wipe-from-pre-rtl {
|
|
1299
|
+
from {
|
|
1300
|
+
background-size: var(--wipe-gradient-width, 0.75em) 100%;
|
|
1301
|
+
background-position: right 0%;
|
|
1302
|
+
}
|
|
1303
|
+
to {
|
|
1304
|
+
background-size: calc(100% + var(--wipe-gradient-width, 0.75em)) 100%;
|
|
1305
|
+
background-position: right 0%;
|
|
1257
1306
|
}
|
|
1258
1307
|
}
|
|
1259
1308
|
|
|
1260
1309
|
@keyframes start-wipe-rtl {
|
|
1261
1310
|
0% {
|
|
1262
|
-
background-size:
|
|
1263
|
-
|
|
1264
|
-
0% 100%;
|
|
1265
|
-
background-position:
|
|
1266
|
-
calc(100% + 0.75em) 0%,
|
|
1267
|
-
calc(100% + 0.5em) 0%;
|
|
1311
|
+
background-size: 0% 100%;
|
|
1312
|
+
background-position: right 0%;
|
|
1268
1313
|
}
|
|
1269
1314
|
100% {
|
|
1270
|
-
background-size:
|
|
1271
|
-
|
|
1272
|
-
100% 100%;
|
|
1273
|
-
background-position:
|
|
1274
|
-
-0.75em 0%,
|
|
1275
|
-
right 0%;
|
|
1315
|
+
background-size: calc(100% + var(--wipe-gradient-width, 0.75em)) 100%;
|
|
1316
|
+
background-position: right 0%;
|
|
1276
1317
|
}
|
|
1277
1318
|
}
|
|
1278
1319
|
|
|
1279
1320
|
@keyframes pre-wipe-universal {
|
|
1321
|
+
from {
|
|
1322
|
+
background-size: 0% 100%;
|
|
1323
|
+
background-position: left;
|
|
1324
|
+
}
|
|
1325
|
+
to {
|
|
1326
|
+
background-size: var(--wipe-gradient-width, 0.75em) 100%;
|
|
1327
|
+
background-position: left;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
@keyframes pre-wipe-universal-rtl {
|
|
1332
|
+
from {
|
|
1333
|
+
background-size: 0% 100%;
|
|
1334
|
+
background-position: right 0%;
|
|
1335
|
+
}
|
|
1336
|
+
to {
|
|
1337
|
+
background-size: var(--wipe-gradient-width, 0.75em) 100%;
|
|
1338
|
+
background-position: right 0%;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/* Character-rendered words use a separate moving gradient in front of
|
|
1343
|
+
their solid fill. This makes the individual glyph wipes read as one
|
|
1344
|
+
continuous word-level wipe. */
|
|
1345
|
+
@keyframes char-pre-wipe {
|
|
1280
1346
|
from {
|
|
1281
1347
|
background-size:
|
|
1282
|
-
0.75em 100%,
|
|
1348
|
+
var(--wipe-gradient-width, 0.75em) 100%,
|
|
1283
1349
|
0% 100%;
|
|
1284
1350
|
background-position:
|
|
1285
|
-
-0.75em 0%,
|
|
1351
|
+
calc(-1 * var(--wipe-gradient-width, 0.75em)) 0%,
|
|
1286
1352
|
left;
|
|
1287
1353
|
}
|
|
1288
1354
|
to {
|
|
1289
1355
|
background-size:
|
|
1290
|
-
0.75em 100%,
|
|
1356
|
+
var(--wipe-gradient-width, 0.75em) 100%,
|
|
1291
1357
|
0% 100%;
|
|
1292
1358
|
background-position:
|
|
1293
|
-
-0.375em 0%,
|
|
1359
|
+
calc(-1 * var(--wipe-gradient-half, 0.375em)) 0%,
|
|
1294
1360
|
left;
|
|
1295
1361
|
}
|
|
1296
1362
|
}
|
|
1297
1363
|
|
|
1298
|
-
@keyframes
|
|
1364
|
+
@keyframes char-start-wipe {
|
|
1299
1365
|
from {
|
|
1300
1366
|
background-size:
|
|
1301
|
-
0.75em 100%,
|
|
1367
|
+
var(--wipe-gradient-width, 0.75em) 100%,
|
|
1302
1368
|
0% 100%;
|
|
1303
1369
|
background-position:
|
|
1304
|
-
calc(
|
|
1305
|
-
|
|
1370
|
+
calc(-1 * var(--wipe-gradient-width, 0.75em)) 0%,
|
|
1371
|
+
left;
|
|
1306
1372
|
}
|
|
1307
1373
|
to {
|
|
1308
1374
|
background-size:
|
|
1309
|
-
0.75em 100%,
|
|
1310
|
-
|
|
1375
|
+
var(--wipe-gradient-width, 0.75em) 100%,
|
|
1376
|
+
100% 100%;
|
|
1311
1377
|
background-position:
|
|
1312
|
-
calc(100% + 0.375em) 0%,
|
|
1313
|
-
|
|
1378
|
+
calc(100% + var(--wipe-gradient-half, 0.375em)) 0%,
|
|
1379
|
+
left;
|
|
1314
1380
|
}
|
|
1315
1381
|
}
|
|
1316
1382
|
|
|
1317
|
-
@keyframes
|
|
1383
|
+
@keyframes char-wipe {
|
|
1318
1384
|
from {
|
|
1319
1385
|
background-size:
|
|
1320
|
-
0.75em 100%,
|
|
1386
|
+
var(--wipe-gradient-width, 0.75em) 100%,
|
|
1321
1387
|
0% 100%;
|
|
1322
1388
|
background-position:
|
|
1323
|
-
-0.
|
|
1389
|
+
calc(-1 * var(--wipe-gradient-half, 0.375em)) 0%,
|
|
1324
1390
|
left;
|
|
1325
1391
|
}
|
|
1326
1392
|
to {
|
|
1327
1393
|
background-size:
|
|
1328
|
-
0.75em 100%,
|
|
1329
|
-
|
|
1394
|
+
var(--wipe-gradient-width, 0.75em) 100%,
|
|
1395
|
+
100% 100%;
|
|
1330
1396
|
background-position:
|
|
1331
|
-
-0.375em 0%,
|
|
1397
|
+
calc(100% + var(--wipe-gradient-half, 0.375em)) 0%,
|
|
1332
1398
|
left;
|
|
1333
1399
|
}
|
|
1334
1400
|
}
|
|
@@ -1423,6 +1489,15 @@ export class AmLyrics extends LitElement {
|
|
|
1423
1489
|
}
|
|
1424
1490
|
}
|
|
1425
1491
|
|
|
1492
|
+
@keyframes drag-char {
|
|
1493
|
+
0% {
|
|
1494
|
+
transform: translate3d(0, 0, 0);
|
|
1495
|
+
}
|
|
1496
|
+
100% {
|
|
1497
|
+
transform: translate3d(0, var(--char-rise-y, -1.12px), 0);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1426
1501
|
@keyframes grow-static {
|
|
1427
1502
|
0%,
|
|
1428
1503
|
100% {
|
|
@@ -1668,14 +1743,20 @@ export class AmLyrics extends LitElement {
|
|
|
1668
1743
|
this.preActiveLineElements = [];
|
|
1669
1744
|
this.positionedLineElements = [];
|
|
1670
1745
|
this.activeGapLineElements = [];
|
|
1746
|
+
this.clearBackgroundExpandedLine();
|
|
1671
1747
|
|
|
1672
1748
|
// Stop all running animations and clear highlights immediately
|
|
1673
1749
|
if (this.lyricsContainer) {
|
|
1674
1750
|
const activeLines = this.lyricsContainer.querySelectorAll(
|
|
1675
|
-
'.lyrics-line.active, .lyrics-line.pre-active, .lyrics-line.bg-expanded',
|
|
1751
|
+
'.lyrics-line.active, .lyrics-line.pre-active, .lyrics-line.bg-expanded, .lyrics-line.scroll-exiting',
|
|
1676
1752
|
);
|
|
1677
1753
|
activeLines.forEach(line => {
|
|
1678
|
-
line.classList.remove(
|
|
1754
|
+
line.classList.remove(
|
|
1755
|
+
'active',
|
|
1756
|
+
'pre-active',
|
|
1757
|
+
'bg-expanded',
|
|
1758
|
+
'scroll-exiting',
|
|
1759
|
+
);
|
|
1679
1760
|
AmLyrics.resetSyllables(line as HTMLElement);
|
|
1680
1761
|
});
|
|
1681
1762
|
|
|
@@ -1740,10 +1821,7 @@ export class AmLyrics extends LitElement {
|
|
|
1740
1821
|
(switchBtn as HTMLButtonElement).disabled = this.isFetchingAlternatives;
|
|
1741
1822
|
}
|
|
1742
1823
|
if (svgEl) {
|
|
1743
|
-
svgEl.
|
|
1744
|
-
'style',
|
|
1745
|
-
`margin-right: 4px; ${this.isFetchingAlternatives ? 'animation: spin 1s linear infinite;' : ''}`,
|
|
1746
|
-
);
|
|
1824
|
+
svgEl.classList.toggle('is-loading', this.isFetchingAlternatives);
|
|
1747
1825
|
}
|
|
1748
1826
|
if (labelEl) {
|
|
1749
1827
|
labelEl.textContent = this.isFetchingAlternatives
|
|
@@ -1806,6 +1884,7 @@ export class AmLyrics extends LitElement {
|
|
|
1806
1884
|
groupGrowable: boolean[];
|
|
1807
1885
|
groupGlowing: boolean[];
|
|
1808
1886
|
groupCharRise: boolean[];
|
|
1887
|
+
groupCharDrag: boolean[];
|
|
1809
1888
|
vwFullText: string[];
|
|
1810
1889
|
vwFullDuration: number[];
|
|
1811
1890
|
vwCharOffset: number[];
|
|
@@ -1821,6 +1900,8 @@ export class AmLyrics extends LitElement {
|
|
|
1821
1900
|
|
|
1822
1901
|
private lastPrimaryActiveLine: HTMLElement | null = null;
|
|
1823
1902
|
|
|
1903
|
+
private backgroundExpandedLine: HTMLElement | null = null;
|
|
1904
|
+
|
|
1824
1905
|
// Scroll animation state
|
|
1825
1906
|
private scrollAnimationState: {
|
|
1826
1907
|
isAnimating: boolean;
|
|
@@ -2076,6 +2157,7 @@ export class AmLyrics extends LitElement {
|
|
|
2076
2157
|
this.preActiveLineElements = [];
|
|
2077
2158
|
this.positionedLineElements = [];
|
|
2078
2159
|
this.activeGapLineElements = [];
|
|
2160
|
+
this.clearBackgroundExpandedLine();
|
|
2079
2161
|
|
|
2080
2162
|
if (this.lyricsContainer) {
|
|
2081
2163
|
this.isProgrammaticScroll = true;
|
|
@@ -2140,20 +2222,26 @@ export class AmLyrics extends LitElement {
|
|
|
2140
2222
|
return 30;
|
|
2141
2223
|
}
|
|
2142
2224
|
|
|
2225
|
+
private static getDisplaySourceLabel(sourceLabel: string): string {
|
|
2226
|
+
return sourceLabel.toLowerCase().includes('lyricsplus')
|
|
2227
|
+
? 'QQ'
|
|
2228
|
+
: sourceLabel;
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
private static getSourceKey(sourceLabel: string | null | undefined): string {
|
|
2232
|
+
const lower = (sourceLabel || '').trim().toLowerCase();
|
|
2233
|
+
if (!lower) return '';
|
|
2234
|
+
if (lower.includes('lyricsplus') || lower === 'qq') return 'qq';
|
|
2235
|
+
return lower.replace(/\s+/g, ' ');
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2143
2238
|
private static mergeAndSortSources(
|
|
2144
|
-
collectedSources:
|
|
2145
|
-
):
|
|
2146
|
-
const uniqueSourcesMap = new Map<
|
|
2147
|
-
string,
|
|
2148
|
-
{ lines: LyricsLine[]; source: string }
|
|
2149
|
-
>();
|
|
2239
|
+
collectedSources: YouLyPlusLyricsResult[],
|
|
2240
|
+
): YouLyPlusLyricsResult[] {
|
|
2241
|
+
const uniqueSourcesMap = new Map<string, YouLyPlusLyricsResult>();
|
|
2150
2242
|
|
|
2151
2243
|
for (const source of collectedSources) {
|
|
2152
|
-
const normalizedSource = source.source
|
|
2153
|
-
.toLowerCase()
|
|
2154
|
-
.includes('lyricsplus')
|
|
2155
|
-
? 'QQ'
|
|
2156
|
-
: source.source;
|
|
2244
|
+
const normalizedSource = AmLyrics.getDisplaySourceLabel(source.source);
|
|
2157
2245
|
|
|
2158
2246
|
if (!uniqueSourcesMap.has(normalizedSource)) {
|
|
2159
2247
|
uniqueSourcesMap.set(normalizedSource, {
|
|
@@ -2170,9 +2258,63 @@ export class AmLyrics extends LitElement {
|
|
|
2170
2258
|
);
|
|
2171
2259
|
}
|
|
2172
2260
|
|
|
2261
|
+
private findCurrentSourceIndex(
|
|
2262
|
+
sources = this.availableSources,
|
|
2263
|
+
sourceLabel = this.lyricsSource,
|
|
2264
|
+
lines = this.lyrics,
|
|
2265
|
+
): number {
|
|
2266
|
+
const identityIndex = sources.findIndex(source => source.lines === lines);
|
|
2267
|
+
if (identityIndex !== -1) return identityIndex;
|
|
2268
|
+
|
|
2269
|
+
const sourceKey = AmLyrics.getSourceKey(sourceLabel);
|
|
2270
|
+
if (!sourceKey) return -1;
|
|
2271
|
+
|
|
2272
|
+
return sources.findIndex(
|
|
2273
|
+
source => AmLyrics.getSourceKey(source.source) === sourceKey,
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
private static getNextSourceIndex(
|
|
2278
|
+
sources: YouLyPlusLyricsResult[],
|
|
2279
|
+
currentIndex: number,
|
|
2280
|
+
currentSourceLabel: string | null,
|
|
2281
|
+
currentLines: LyricsLine[] | undefined,
|
|
2282
|
+
): number {
|
|
2283
|
+
if (sources.length <= 1) return -1;
|
|
2284
|
+
|
|
2285
|
+
if (currentIndex !== -1) {
|
|
2286
|
+
return (currentIndex + 1) % sources.length;
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
const currentKey = AmLyrics.getSourceKey(currentSourceLabel);
|
|
2290
|
+
const fallbackIndex = sources.findIndex(
|
|
2291
|
+
source =>
|
|
2292
|
+
source.lines !== currentLines &&
|
|
2293
|
+
AmLyrics.getSourceKey(source.source) !== currentKey,
|
|
2294
|
+
);
|
|
2295
|
+
|
|
2296
|
+
return fallbackIndex === -1 ? 0 : fallbackIndex;
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
private async applySourceAtIndex(index: number) {
|
|
2300
|
+
const sourceResult = this.availableSources[index];
|
|
2301
|
+
if (!sourceResult) return;
|
|
2302
|
+
|
|
2303
|
+
this.currentSourceIndex = index;
|
|
2304
|
+
this.lyrics = sourceResult.lines;
|
|
2305
|
+
this.lyricsSource = sourceResult.source;
|
|
2306
|
+
if (sourceResult.songwriters) {
|
|
2307
|
+
this.songwriters = sourceResult.songwriters;
|
|
2308
|
+
}
|
|
2309
|
+
await this.onLyricsLoaded();
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2173
2312
|
private async switchSource() {
|
|
2174
2313
|
if (this.isFetchingAlternatives) return;
|
|
2175
2314
|
|
|
2315
|
+
const currentSourceLabel = this.lyricsSource;
|
|
2316
|
+
const currentLines = this.lyrics;
|
|
2317
|
+
|
|
2176
2318
|
if (!this.hasFetchedAllProviders) {
|
|
2177
2319
|
this.isFetchingAlternatives = true;
|
|
2178
2320
|
this._updateFooter();
|
|
@@ -2249,11 +2391,13 @@ export class AmLyrics extends LitElement {
|
|
|
2249
2391
|
...this.availableSources,
|
|
2250
2392
|
...newSources,
|
|
2251
2393
|
]);
|
|
2252
|
-
// Re-sync current index since sorting
|
|
2253
|
-
|
|
2254
|
-
|
|
2394
|
+
// Re-sync current index since sorting or label normalization can
|
|
2395
|
+
// shift the currently displayed source underneath the old index.
|
|
2396
|
+
this.currentSourceIndex = this.findCurrentSourceIndex(
|
|
2397
|
+
this.availableSources,
|
|
2398
|
+
currentSourceLabel,
|
|
2399
|
+
currentLines,
|
|
2255
2400
|
);
|
|
2256
|
-
if (this.currentSourceIndex === -1) this.currentSourceIndex = 0;
|
|
2257
2401
|
}
|
|
2258
2402
|
}
|
|
2259
2403
|
} finally {
|
|
@@ -2264,15 +2408,20 @@ export class AmLyrics extends LitElement {
|
|
|
2264
2408
|
}
|
|
2265
2409
|
|
|
2266
2410
|
if (this.availableSources.length > 1) {
|
|
2267
|
-
this.
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
this.
|
|
2411
|
+
const currentIndex = this.findCurrentSourceIndex(
|
|
2412
|
+
this.availableSources,
|
|
2413
|
+
currentSourceLabel,
|
|
2414
|
+
currentLines,
|
|
2415
|
+
);
|
|
2416
|
+
const nextIndex = AmLyrics.getNextSourceIndex(
|
|
2417
|
+
this.availableSources,
|
|
2418
|
+
currentIndex,
|
|
2419
|
+
currentSourceLabel,
|
|
2420
|
+
currentLines,
|
|
2421
|
+
);
|
|
2422
|
+
if (nextIndex !== -1) {
|
|
2423
|
+
await this.applySourceAtIndex(nextIndex);
|
|
2274
2424
|
}
|
|
2275
|
-
await this.onLyricsLoaded();
|
|
2276
2425
|
}
|
|
2277
2426
|
}
|
|
2278
2427
|
|
|
@@ -3538,13 +3687,21 @@ export class AmLyrics extends LitElement {
|
|
|
3538
3687
|
if (!newActiveLines.includes(lineIndex)) {
|
|
3539
3688
|
const lineElement = this._getLineElement(lineIndex);
|
|
3540
3689
|
if (lineElement) {
|
|
3541
|
-
if (
|
|
3690
|
+
if (
|
|
3691
|
+
isSeek ||
|
|
3692
|
+
this.isUserScrolling ||
|
|
3693
|
+
AmLyrics.isLineSyncedLine(this.lyrics?.[lineIndex])
|
|
3694
|
+
) {
|
|
3542
3695
|
AmLyrics.unfinishSyllables(lineElement);
|
|
3543
3696
|
} else {
|
|
3544
3697
|
AmLyrics.finishSyllablesUpToTime(lineElement, newTime);
|
|
3545
3698
|
}
|
|
3546
3699
|
|
|
3547
|
-
lineElement.classList.remove(
|
|
3700
|
+
lineElement.classList.remove(
|
|
3701
|
+
'active',
|
|
3702
|
+
'bg-expanded',
|
|
3703
|
+
'scroll-exiting',
|
|
3704
|
+
);
|
|
3548
3705
|
|
|
3549
3706
|
if (lineElement.classList.contains('pre-active')) {
|
|
3550
3707
|
lineElement.classList.remove('pre-active');
|
|
@@ -3555,13 +3712,14 @@ export class AmLyrics extends LitElement {
|
|
|
3555
3712
|
}
|
|
3556
3713
|
}
|
|
3557
3714
|
|
|
3558
|
-
// Add 'active'
|
|
3715
|
+
// Add 'active' to newly active lines. Background expansion is driven
|
|
3716
|
+
// separately by the current scroll target.
|
|
3559
3717
|
for (const lineIndex of newActiveLines) {
|
|
3560
3718
|
if (!oldActiveLines.includes(lineIndex)) {
|
|
3561
3719
|
const lineElement = this._getLineElement(lineIndex);
|
|
3562
3720
|
if (lineElement) {
|
|
3563
|
-
lineElement.classList.add('active'
|
|
3564
|
-
lineElement.classList.remove('pre-active');
|
|
3721
|
+
lineElement.classList.add('active');
|
|
3722
|
+
lineElement.classList.remove('pre-active', 'scroll-exiting');
|
|
3565
3723
|
const preIdx = this.preActiveLineElements.indexOf(lineElement);
|
|
3566
3724
|
if (preIdx !== -1) this.preActiveLineElements.splice(preIdx, 1);
|
|
3567
3725
|
}
|
|
@@ -3810,7 +3968,13 @@ export class AmLyrics extends LitElement {
|
|
|
3810
3968
|
}
|
|
3811
3969
|
|
|
3812
3970
|
updated(changedProperties: Map<string | number | symbol, unknown>) {
|
|
3813
|
-
|
|
3971
|
+
const lyricsDomBecameRenderable =
|
|
3972
|
+
changedProperties.has('lyrics') ||
|
|
3973
|
+
(changedProperties.has('isLoading') &&
|
|
3974
|
+
!this.isLoading &&
|
|
3975
|
+
Boolean(this.lyrics));
|
|
3976
|
+
|
|
3977
|
+
if (lyricsDomBecameRenderable) {
|
|
3814
3978
|
this._invalidateCaches();
|
|
3815
3979
|
this._ensureLineDataCache();
|
|
3816
3980
|
this._updateCachedIsUnsynced();
|
|
@@ -3824,8 +3988,14 @@ export class AmLyrics extends LitElement {
|
|
|
3824
3988
|
const activeLines = this.findActiveLineIndices(this.currentTime);
|
|
3825
3989
|
for (const lineIndex of activeLines) {
|
|
3826
3990
|
const lineEl = this._getLineElement(lineIndex);
|
|
3827
|
-
if (lineEl) lineEl.classList.add('active'
|
|
3991
|
+
if (lineEl) lineEl.classList.add('active');
|
|
3828
3992
|
}
|
|
3993
|
+
const primaryActiveLine = this.getPrimaryActiveLineIndex(activeLines);
|
|
3994
|
+
this.setBackgroundExpandedLine(
|
|
3995
|
+
primaryActiveLine !== null
|
|
3996
|
+
? this._getLineElement(primaryActiveLine)
|
|
3997
|
+
: null,
|
|
3998
|
+
);
|
|
3829
3999
|
|
|
3830
4000
|
// Trigger a faux time-change so that updateSyllablesForLine fires
|
|
3831
4001
|
// to setup inline syllable CSS wipe animations for whatever the current time is
|
|
@@ -3872,6 +4042,7 @@ export class AmLyrics extends LitElement {
|
|
|
3872
4042
|
this.preActiveLineElements = [];
|
|
3873
4043
|
this.positionedLineElements = [];
|
|
3874
4044
|
this.activeGapLineElements = [];
|
|
4045
|
+
this.clearBackgroundExpandedLine();
|
|
3875
4046
|
this.setUserScrolling(false);
|
|
3876
4047
|
|
|
3877
4048
|
// Cancel any running animations
|
|
@@ -3943,6 +4114,7 @@ export class AmLyrics extends LitElement {
|
|
|
3943
4114
|
// Don't override it with a scroll back to the last lyric.
|
|
3944
4115
|
const footer = this.lyricsContainer.querySelector('.lyrics-footer');
|
|
3945
4116
|
if (footer?.classList.contains('active')) {
|
|
4117
|
+
this.setBackgroundExpandedLine(null);
|
|
3946
4118
|
return;
|
|
3947
4119
|
}
|
|
3948
4120
|
|
|
@@ -3994,10 +4166,22 @@ export class AmLyrics extends LitElement {
|
|
|
3994
4166
|
targetElement = this._getLineElement(targetLineIdx);
|
|
3995
4167
|
}
|
|
3996
4168
|
}
|
|
3997
|
-
if (!targetElement)
|
|
4169
|
+
if (!targetElement) {
|
|
4170
|
+
this.setBackgroundExpandedLine(null);
|
|
4171
|
+
return;
|
|
4172
|
+
}
|
|
4173
|
+
|
|
4174
|
+
const scrollDuration = scrollLookAheadMs;
|
|
4175
|
+
if (targetElement !== this.currentPrimaryActiveLine || forceScroll) {
|
|
4176
|
+
targetElement.style.setProperty(
|
|
4177
|
+
'--scroll-duration',
|
|
4178
|
+
`${scrollDuration}ms`,
|
|
4179
|
+
);
|
|
4180
|
+
}
|
|
4181
|
+
this.setBackgroundExpandedLine(targetElement);
|
|
3998
4182
|
|
|
3999
|
-
// Unblur the upcoming target line early
|
|
4000
|
-
//
|
|
4183
|
+
// Unblur the upcoming target line early while the separate bg-expanded
|
|
4184
|
+
// class starts background vocal height/opacity in sync with scroll.
|
|
4001
4185
|
if (!targetElement.classList.contains('active')) {
|
|
4002
4186
|
targetElement.classList.add('pre-active');
|
|
4003
4187
|
if (!this.preActiveLineElements.includes(targetElement)) {
|
|
@@ -4005,8 +4189,6 @@ export class AmLyrics extends LitElement {
|
|
|
4005
4189
|
}
|
|
4006
4190
|
}
|
|
4007
4191
|
|
|
4008
|
-
const scrollDuration = scrollLookAheadMs;
|
|
4009
|
-
|
|
4010
4192
|
this.focusLine(targetElement, forceScroll, scrollDuration);
|
|
4011
4193
|
}
|
|
4012
4194
|
|
|
@@ -4096,6 +4278,7 @@ export class AmLyrics extends LitElement {
|
|
|
4096
4278
|
this.preActiveLineElements = [];
|
|
4097
4279
|
this.positionedLineElements = [];
|
|
4098
4280
|
this.activeGapLineElements = [];
|
|
4281
|
+
this.clearBackgroundExpandedLine();
|
|
4099
4282
|
this.visibilityObserver?.disconnect();
|
|
4100
4283
|
this.visibilityObserver = undefined;
|
|
4101
4284
|
}
|
|
@@ -4139,6 +4322,7 @@ export class AmLyrics extends LitElement {
|
|
|
4139
4322
|
const groupGrowable: boolean[] = new Array(wordGroups.length).fill(false);
|
|
4140
4323
|
const groupGlowing: boolean[] = new Array(wordGroups.length).fill(false);
|
|
4141
4324
|
const groupCharRise: boolean[] = new Array(wordGroups.length).fill(false);
|
|
4325
|
+
const groupCharDrag: boolean[] = new Array(wordGroups.length).fill(false);
|
|
4142
4326
|
const vwFullText: string[] = new Array(wordGroups.length).fill('');
|
|
4143
4327
|
const vwFullDuration: number[] = new Array(wordGroups.length).fill(0);
|
|
4144
4328
|
const vwCharOffset: number[] = new Array(wordGroups.length).fill(0);
|
|
@@ -4183,9 +4367,13 @@ export class AmLyrics extends LitElement {
|
|
|
4183
4367
|
line.isWordSynced === false || line.text.some(s => s.lineSynced);
|
|
4184
4368
|
let isGrowableVW = canAnimateByChar && wordLen > 0 && wordLen <= 7;
|
|
4185
4369
|
if (isGrowableVW) {
|
|
4186
|
-
if (wordLen
|
|
4370
|
+
if (wordLen <= 1) {
|
|
4187
4371
|
isGrowableVW =
|
|
4188
4372
|
combinedDuration >= 1050 && combinedDuration >= wordLen * 525;
|
|
4373
|
+
} else if (wordLen <= 3) {
|
|
4374
|
+
isGrowableVW =
|
|
4375
|
+
combinedDuration >=
|
|
4376
|
+
SHORT_WORD_GLOW_MIN_DURATION_MS + (wordLen - 2) * 140;
|
|
4189
4377
|
} else {
|
|
4190
4378
|
isGrowableVW =
|
|
4191
4379
|
combinedDuration >= 850 && combinedDuration >= wordLen * 190;
|
|
@@ -4194,6 +4382,11 @@ export class AmLyrics extends LitElement {
|
|
|
4194
4382
|
|
|
4195
4383
|
const hasCharRiseDuration =
|
|
4196
4384
|
combinedDuration >= Math.max(700, wordLen * 85);
|
|
4385
|
+
const hasTinyWordDragDuration =
|
|
4386
|
+
wordLen >= 2 &&
|
|
4387
|
+
wordLen <= 3 &&
|
|
4388
|
+
combinedDuration >=
|
|
4389
|
+
Math.max(SHORT_WORD_DRAG_MIN_DURATION_MS, wordLen * 150);
|
|
4197
4390
|
const hasLongShortWordDuration =
|
|
4198
4391
|
wordLen >= 4 && combinedDuration >= Math.max(1300, wordLen * 260);
|
|
4199
4392
|
const isCharRiseVW =
|
|
@@ -4202,6 +4395,11 @@ export class AmLyrics extends LitElement {
|
|
|
4202
4395
|
!isGrowableVW &&
|
|
4203
4396
|
((wordLen >= 8 && hasCharRiseDuration) ||
|
|
4204
4397
|
(wordLen < 8 && hasLongShortWordDuration));
|
|
4398
|
+
const isCharDragVW =
|
|
4399
|
+
canAnimateByChar &&
|
|
4400
|
+
!isLineSynced &&
|
|
4401
|
+
!isGrowableVW &&
|
|
4402
|
+
hasTinyWordDragDuration;
|
|
4205
4403
|
|
|
4206
4404
|
const isGlowingVW = isGrowableVW && !isLineSynced;
|
|
4207
4405
|
|
|
@@ -4210,6 +4408,7 @@ export class AmLyrics extends LitElement {
|
|
|
4210
4408
|
groupGrowable[gi] = isGrowableVW;
|
|
4211
4409
|
groupGlowing[gi] = isGlowingVW;
|
|
4212
4410
|
groupCharRise[gi] = isCharRiseVW;
|
|
4411
|
+
groupCharDrag[gi] = isCharDragVW;
|
|
4213
4412
|
vwFullText[gi] = combinedText;
|
|
4214
4413
|
vwFullDuration[gi] = combinedDuration;
|
|
4215
4414
|
vwCharOffset[gi] = charOff;
|
|
@@ -4227,6 +4426,7 @@ export class AmLyrics extends LitElement {
|
|
|
4227
4426
|
groupGrowable,
|
|
4228
4427
|
groupGlowing,
|
|
4229
4428
|
groupCharRise,
|
|
4429
|
+
groupCharDrag,
|
|
4230
4430
|
vwFullText,
|
|
4231
4431
|
vwFullDuration,
|
|
4232
4432
|
vwCharOffset,
|
|
@@ -4248,62 +4448,146 @@ export class AmLyrics extends LitElement {
|
|
|
4248
4448
|
|
|
4249
4449
|
const computedStyle = getComputedStyle(referenceSyllable);
|
|
4250
4450
|
const { font } = computedStyle; // Full font string
|
|
4251
|
-
const fontSize = parseFloat(computedStyle.fontSize);
|
|
4252
|
-
|
|
4253
|
-
const charTimedWords = this.shadowRoot.querySelectorAll(
|
|
4254
|
-
'.lyrics-word.growable, .lyrics-word.char-rise',
|
|
4255
|
-
);
|
|
4256
|
-
if (!charTimedWords) return;
|
|
4451
|
+
const fontSize = Number.parseFloat(computedStyle.fontSize) || 16;
|
|
4257
4452
|
|
|
4258
|
-
charTimedWords.
|
|
4259
|
-
|
|
4453
|
+
const charTimedWords = Array.from(
|
|
4454
|
+
this.shadowRoot.querySelectorAll(
|
|
4455
|
+
'.lyrics-word.growable, .lyrics-word.char-rise, .lyrics-word.char-drag',
|
|
4456
|
+
),
|
|
4457
|
+
) as HTMLElement[];
|
|
4458
|
+
if (charTimedWords.length === 0) return;
|
|
4459
|
+
|
|
4460
|
+
const wordsByVirtualId = new Map<string, HTMLElement[]>();
|
|
4461
|
+
charTimedWords.forEach((wordSpan, index) => {
|
|
4462
|
+
const virtualWordId = wordSpan.dataset.virtualWordId || `word-${index}`;
|
|
4463
|
+
const words = wordsByVirtualId.get(virtualWordId);
|
|
4464
|
+
if (words) {
|
|
4465
|
+
words.push(wordSpan);
|
|
4466
|
+
} else {
|
|
4467
|
+
wordsByVirtualId.set(virtualWordId, [wordSpan]);
|
|
4468
|
+
}
|
|
4469
|
+
});
|
|
4260
4470
|
|
|
4261
|
-
|
|
4471
|
+
wordsByVirtualId.forEach(wordSpans => {
|
|
4262
4472
|
const syllables: HTMLElement[] = [];
|
|
4263
|
-
|
|
4264
|
-
const
|
|
4265
|
-
|
|
4473
|
+
wordSpans.forEach(wordSpan => {
|
|
4474
|
+
const syllableWraps = wordSpan.querySelectorAll(
|
|
4475
|
+
'.lyrics-syllable-wrap',
|
|
4476
|
+
);
|
|
4477
|
+
syllableWraps.forEach(wrap => {
|
|
4478
|
+
const syl = wrap.querySelector('.lyrics-syllable');
|
|
4479
|
+
if (syl) syllables.push(syl as HTMLElement);
|
|
4480
|
+
});
|
|
4266
4481
|
});
|
|
4267
4482
|
|
|
4268
|
-
syllables.
|
|
4269
|
-
const
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
const duration = parseFloat(sylSpan.dataset.duration || '0');
|
|
4279
|
-
const velocityPxPerMs =
|
|
4280
|
-
duration > 0 ? totalSyllableWidth / duration : 0;
|
|
4281
|
-
|
|
4282
|
-
// Gradient width in pixels = 0.375 * fontSize
|
|
4283
|
-
// This matches YouLyPlus visual gradient size
|
|
4284
|
-
const gradientWidthPx = 0.375 * fontSize;
|
|
4285
|
-
const gradientDurationMs =
|
|
4286
|
-
velocityPxPerMs > 0 ? gradientWidthPx / velocityPxPerMs : 100;
|
|
4287
|
-
|
|
4288
|
-
let cumulativeCharWidth = 0;
|
|
4289
|
-
|
|
4290
|
-
charSpans.forEach((spanArg: any, i: number) => {
|
|
4291
|
-
const charWidth = charWidths[i];
|
|
4292
|
-
const span = spanArg;
|
|
4483
|
+
const charSpans = syllables.flatMap(syl => {
|
|
4484
|
+
const spans = Array.from(
|
|
4485
|
+
syl.querySelectorAll('.char'),
|
|
4486
|
+
) as HTMLElement[];
|
|
4487
|
+
const target = syl as any;
|
|
4488
|
+
target._cachedCharSpans = spans;
|
|
4489
|
+
return spans;
|
|
4490
|
+
});
|
|
4491
|
+
if (charSpans.length === 0) return;
|
|
4293
4492
|
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4493
|
+
wordSpans.forEach(wordSpan => {
|
|
4494
|
+
const target = wordSpan as any;
|
|
4495
|
+
target._cachedVirtualWordElements = wordSpans;
|
|
4496
|
+
target._cachedVirtualWordCharSpans = charSpans;
|
|
4497
|
+
});
|
|
4297
4498
|
|
|
4298
|
-
|
|
4299
|
-
|
|
4499
|
+
const syllableEntries = syllables.map(syl => {
|
|
4500
|
+
const spans = (syl as any)._cachedCharSpans as HTMLElement[];
|
|
4501
|
+
const charWidths = spans.map(span =>
|
|
4502
|
+
this._getTextWidth(span.textContent || '', font),
|
|
4503
|
+
);
|
|
4504
|
+
const totalWidth = charWidths.reduce((a, b) => a + b, 0);
|
|
4505
|
+
return {
|
|
4506
|
+
syl,
|
|
4507
|
+
spans,
|
|
4508
|
+
charWidths,
|
|
4509
|
+
totalWidth,
|
|
4510
|
+
start: parseFloat(syl.getAttribute('data-start-time') || ''),
|
|
4511
|
+
end: parseFloat(syl.getAttribute('data-end-time') || ''),
|
|
4512
|
+
};
|
|
4513
|
+
});
|
|
4514
|
+
const totalWordWidth = syllableEntries.reduce(
|
|
4515
|
+
(total, entry) => total + entry.totalWidth,
|
|
4516
|
+
0,
|
|
4517
|
+
);
|
|
4518
|
+
if (totalWordWidth <= 0) return;
|
|
4300
4519
|
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4520
|
+
const virtualWordStart = Math.min(
|
|
4521
|
+
...syllableEntries
|
|
4522
|
+
.map(entry => entry.start)
|
|
4523
|
+
.filter(start => Number.isFinite(start)),
|
|
4524
|
+
);
|
|
4525
|
+
const virtualWordEnd = Math.max(
|
|
4526
|
+
...syllableEntries
|
|
4527
|
+
.map(entry => entry.end)
|
|
4528
|
+
.filter(end => Number.isFinite(end)),
|
|
4529
|
+
);
|
|
4530
|
+
const virtualWordDuration = virtualWordEnd - virtualWordStart;
|
|
4531
|
+
const hasTimedSyllables =
|
|
4532
|
+
Number.isFinite(virtualWordStart) &&
|
|
4533
|
+
Number.isFinite(virtualWordEnd) &&
|
|
4534
|
+
virtualWordDuration > 0;
|
|
4535
|
+
const wordVelocityPxPerMs = hasTimedSyllables
|
|
4536
|
+
? totalWordWidth / virtualWordDuration
|
|
4537
|
+
: 0;
|
|
4538
|
+
const gradientLeadWidthPx =
|
|
4539
|
+
(BASE_WIPE_GRADIENT_EM * Math.max(1, fontSize)) / 2;
|
|
4540
|
+
const measuredPreWipeDuration =
|
|
4541
|
+
wordVelocityPxPerMs > 0
|
|
4542
|
+
? gradientLeadWidthPx / wordVelocityPxPerMs
|
|
4543
|
+
: 100;
|
|
4544
|
+
|
|
4545
|
+
let cumulativeCharWidth = 0;
|
|
4546
|
+
syllableEntries.forEach(entry => {
|
|
4547
|
+
let cumulativeSyllableWidth = 0;
|
|
4548
|
+
const syllableDuration = entry.end - entry.start;
|
|
4549
|
+
const useSyllableTiming =
|
|
4550
|
+
hasTimedSyllables &&
|
|
4551
|
+
Number.isFinite(entry.start) &&
|
|
4552
|
+
Number.isFinite(entry.end) &&
|
|
4553
|
+
syllableDuration > 0 &&
|
|
4554
|
+
entry.totalWidth > 0;
|
|
4555
|
+
|
|
4556
|
+
entry.spans.forEach((span, index) => {
|
|
4557
|
+
const charWidth = entry.charWidths[index];
|
|
4558
|
+
let startPercent = cumulativeCharWidth / totalWordWidth;
|
|
4559
|
+
let durationPercent = charWidth / totalWordWidth;
|
|
4560
|
+
if (useSyllableTiming) {
|
|
4561
|
+
const charStartMs =
|
|
4562
|
+
entry.start -
|
|
4563
|
+
virtualWordStart +
|
|
4564
|
+
(cumulativeSyllableWidth / entry.totalWidth) * syllableDuration;
|
|
4565
|
+
const charDurationMs =
|
|
4566
|
+
(charWidth / entry.totalWidth) * syllableDuration;
|
|
4567
|
+
startPercent = AmLyrics.clamp(
|
|
4568
|
+
charStartMs / virtualWordDuration,
|
|
4569
|
+
0,
|
|
4570
|
+
1,
|
|
4571
|
+
);
|
|
4572
|
+
durationPercent = AmLyrics.clamp(
|
|
4573
|
+
charDurationMs / virtualWordDuration,
|
|
4574
|
+
0,
|
|
4575
|
+
1,
|
|
4576
|
+
);
|
|
4304
4577
|
}
|
|
4578
|
+
const target = span;
|
|
4579
|
+
|
|
4580
|
+
target.dataset.wipeStart = startPercent.toFixed(4);
|
|
4581
|
+
target.dataset.wipeDuration = durationPercent.toFixed(4);
|
|
4582
|
+
target.dataset.preWipeDuration = measuredPreWipeDuration.toFixed(2);
|
|
4583
|
+
target.style.setProperty('--word-wipe-width', `${totalWordWidth}px`);
|
|
4584
|
+
target.style.setProperty(
|
|
4585
|
+
'--char-wipe-position',
|
|
4586
|
+
`${-cumulativeCharWidth}px`,
|
|
4587
|
+
);
|
|
4305
4588
|
|
|
4306
4589
|
cumulativeCharWidth += charWidth;
|
|
4590
|
+
cumulativeSyllableWidth += charWidth;
|
|
4307
4591
|
});
|
|
4308
4592
|
});
|
|
4309
4593
|
});
|
|
@@ -4313,6 +4597,37 @@ export class AmLyrics extends LitElement {
|
|
|
4313
4597
|
return a.length === b.length && a.every((val, i) => val === b[i]);
|
|
4314
4598
|
}
|
|
4315
4599
|
|
|
4600
|
+
private static isLineSyncedLine(line: LyricsLine | undefined): boolean {
|
|
4601
|
+
if (!line) return false;
|
|
4602
|
+
return line.isWordSynced === false || line.text.some(s => s.lineSynced);
|
|
4603
|
+
}
|
|
4604
|
+
|
|
4605
|
+
private getLineHighlightEndTime(index: number): number {
|
|
4606
|
+
if (!this.lyrics) return 0;
|
|
4607
|
+
const line = this.lyrics[index];
|
|
4608
|
+
if (!line) return 0;
|
|
4609
|
+
|
|
4610
|
+
const rawEnd = Math.max(line.endtime, line.timestamp);
|
|
4611
|
+
|
|
4612
|
+
const nextLine = this.lyrics[index + 1];
|
|
4613
|
+
if (!nextLine || nextLine.timestamp <= line.timestamp) {
|
|
4614
|
+
return rawEnd > line.timestamp ? rawEnd + 200 : rawEnd;
|
|
4615
|
+
}
|
|
4616
|
+
|
|
4617
|
+
if (rawEnd > line.timestamp) {
|
|
4618
|
+
if (nextLine.timestamp < rawEnd) {
|
|
4619
|
+
return rawEnd;
|
|
4620
|
+
}
|
|
4621
|
+
|
|
4622
|
+
const gapToNext = nextLine.timestamp - rawEnd;
|
|
4623
|
+
if (gapToNext >= INSTRUMENTAL_THRESHOLD_MS) {
|
|
4624
|
+
return rawEnd;
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4627
|
+
|
|
4628
|
+
return nextLine.timestamp;
|
|
4629
|
+
}
|
|
4630
|
+
|
|
4316
4631
|
private static getLineIndexFromElement(
|
|
4317
4632
|
lineElement: HTMLElement | null,
|
|
4318
4633
|
): number | null {
|
|
@@ -4350,6 +4665,31 @@ export class AmLyrics extends LitElement {
|
|
|
4350
4665
|
this.preActiveLineElements = keptLines;
|
|
4351
4666
|
}
|
|
4352
4667
|
|
|
4668
|
+
private setBackgroundExpandedLine(lineElement: HTMLElement | null): void {
|
|
4669
|
+
const target =
|
|
4670
|
+
lineElement &&
|
|
4671
|
+
!lineElement.classList.contains('lyrics-gap') &&
|
|
4672
|
+
lineElement.querySelector('.background-vocal-container')
|
|
4673
|
+
? lineElement
|
|
4674
|
+
: null;
|
|
4675
|
+
|
|
4676
|
+
if (this.backgroundExpandedLine === target) {
|
|
4677
|
+
if (target && !target.classList.contains('bg-expanded')) {
|
|
4678
|
+
target.classList.add('bg-expanded');
|
|
4679
|
+
}
|
|
4680
|
+
return;
|
|
4681
|
+
}
|
|
4682
|
+
|
|
4683
|
+
this.backgroundExpandedLine?.classList.remove('bg-expanded');
|
|
4684
|
+
this.backgroundExpandedLine = target;
|
|
4685
|
+
target?.classList.add('bg-expanded');
|
|
4686
|
+
}
|
|
4687
|
+
|
|
4688
|
+
private clearBackgroundExpandedLine(): void {
|
|
4689
|
+
this.backgroundExpandedLine?.classList.remove('bg-expanded');
|
|
4690
|
+
this.backgroundExpandedLine = null;
|
|
4691
|
+
}
|
|
4692
|
+
|
|
4353
4693
|
private getPrimaryActiveLineIndex(activeIndices: number[]): number | null {
|
|
4354
4694
|
if (activeIndices.length === 0) return null;
|
|
4355
4695
|
|
|
@@ -4463,7 +4803,15 @@ export class AmLyrics extends LitElement {
|
|
|
4463
4803
|
// effectiveEndTimes). Lines stay active until their extended end,
|
|
4464
4804
|
// so we no longer need to remove .active here.
|
|
4465
4805
|
this.lastPrimaryActiveLine = this.currentPrimaryActiveLine;
|
|
4806
|
+
if (this.lastPrimaryActiveLine) {
|
|
4807
|
+
this.lastPrimaryActiveLine.style.setProperty(
|
|
4808
|
+
'--scroll-duration',
|
|
4809
|
+
`${scrollDuration ?? SCROLL_ANIMATION_DURATION_MS}ms`,
|
|
4810
|
+
);
|
|
4811
|
+
this.lastPrimaryActiveLine.classList.add('scroll-exiting');
|
|
4812
|
+
}
|
|
4466
4813
|
this.currentPrimaryActiveLine = lineElement;
|
|
4814
|
+
this.currentPrimaryActiveLine.classList.remove('scroll-exiting');
|
|
4467
4815
|
const lineIndex = AmLyrics.getLineIndexFromElement(lineElement);
|
|
4468
4816
|
if (lineIndex !== null) {
|
|
4469
4817
|
this.lastActiveIndex = lineIndex;
|
|
@@ -4593,9 +4941,10 @@ export class AmLyrics extends LitElement {
|
|
|
4593
4941
|
|
|
4594
4942
|
for (let i = 0; i < this.lyrics.length; i += 1) {
|
|
4595
4943
|
const line = this.lyrics[i];
|
|
4944
|
+
const highlightEndTime = this.getLineHighlightEndTime(i);
|
|
4596
4945
|
|
|
4597
4946
|
if (line.timestamp > time) break;
|
|
4598
|
-
if (time >= line.timestamp && time <
|
|
4947
|
+
if (time >= line.timestamp && time < highlightEndTime) {
|
|
4599
4948
|
activeLines.push(i);
|
|
4600
4949
|
}
|
|
4601
4950
|
}
|
|
@@ -4830,7 +5179,7 @@ export class AmLyrics extends LitElement {
|
|
|
4830
5179
|
allLines.forEach(lineEl => {
|
|
4831
5180
|
AmLyrics.resetSyllables(lineEl as HTMLElement);
|
|
4832
5181
|
// Remove scroll-animate class and properties to stop any scroll animations
|
|
4833
|
-
lineEl.classList.remove('scroll-animate');
|
|
5182
|
+
lineEl.classList.remove('scroll-animate', 'scroll-exiting');
|
|
4834
5183
|
(lineEl as HTMLElement).style.removeProperty('--scroll-delta');
|
|
4835
5184
|
(lineEl as HTMLElement).style.removeProperty('--lyrics-line-delay');
|
|
4836
5185
|
});
|
|
@@ -4862,6 +5211,7 @@ export class AmLyrics extends LitElement {
|
|
|
4862
5211
|
this.lastPrimaryActiveLine = null;
|
|
4863
5212
|
this.activeLineIds.clear();
|
|
4864
5213
|
this.animatingLines = [];
|
|
5214
|
+
this.setBackgroundExpandedLine(null);
|
|
4865
5215
|
|
|
4866
5216
|
// Find the clicked line element and scroll to it with forceScroll (like YouLyPlus)
|
|
4867
5217
|
// Timestamps are already in milliseconds — match the data-start-time attribute directly
|
|
@@ -4884,6 +5234,7 @@ export class AmLyrics extends LitElement {
|
|
|
4884
5234
|
}, 800);
|
|
4885
5235
|
|
|
4886
5236
|
this.scrollToActiveLineYouLy(clickedLineElement, true);
|
|
5237
|
+
this.setBackgroundExpandedLine(clickedLineElement);
|
|
4887
5238
|
}
|
|
4888
5239
|
|
|
4889
5240
|
const event = new CustomEvent('line-click', {
|
|
@@ -5112,6 +5463,7 @@ export class AmLyrics extends LitElement {
|
|
|
5112
5463
|
scrollDuration ?? SCROLL_ANIMATION_DURATION_MS,
|
|
5113
5464
|
);
|
|
5114
5465
|
const delayIncrement = duration * 0.1;
|
|
5466
|
+
const maxStaggerSteps = 4;
|
|
5115
5467
|
const lookAhead = 20;
|
|
5116
5468
|
const len = lineArray.length;
|
|
5117
5469
|
|
|
@@ -5120,13 +5472,17 @@ export class AmLyrics extends LitElement {
|
|
|
5120
5472
|
|
|
5121
5473
|
let maxAnimationDuration = 0;
|
|
5122
5474
|
const newAnimatingLines: HTMLElement[] = [];
|
|
5475
|
+
const lineDelays = new Map<HTMLElement, number>();
|
|
5123
5476
|
const scrollingDown = delta >= 0;
|
|
5124
5477
|
|
|
5125
5478
|
if (scrollingDown) {
|
|
5126
5479
|
let delayCounter = 0;
|
|
5127
5480
|
for (let i = start; i < end; i += 1) {
|
|
5128
5481
|
const line = lineArray[i];
|
|
5129
|
-
const delay =
|
|
5482
|
+
const delay =
|
|
5483
|
+
i >= referenceIndex
|
|
5484
|
+
? Math.min(delayCounter, maxStaggerSteps) * delayIncrement
|
|
5485
|
+
: 0;
|
|
5130
5486
|
|
|
5131
5487
|
if (i >= referenceIndex && !line.classList.contains('lyrics-gap')) {
|
|
5132
5488
|
delayCounter += 1;
|
|
@@ -5134,11 +5490,11 @@ export class AmLyrics extends LitElement {
|
|
|
5134
5490
|
|
|
5135
5491
|
line.style.setProperty('--scroll-delta', `${delta}px`);
|
|
5136
5492
|
line.style.setProperty('--lyrics-line-delay', `${delay}ms`);
|
|
5137
|
-
|
|
5493
|
+
lineDelays.set(line, delay);
|
|
5138
5494
|
|
|
5139
5495
|
newAnimatingLines.push(line);
|
|
5140
5496
|
|
|
5141
|
-
const lineDuration = duration + delay;
|
|
5497
|
+
const lineDuration = duration + 100 + delay;
|
|
5142
5498
|
if (lineDuration > maxAnimationDuration) {
|
|
5143
5499
|
maxAnimationDuration = lineDuration;
|
|
5144
5500
|
}
|
|
@@ -5147,7 +5503,10 @@ export class AmLyrics extends LitElement {
|
|
|
5147
5503
|
let delayCounter = 0;
|
|
5148
5504
|
for (let i = end - 1; i >= start; i -= 1) {
|
|
5149
5505
|
const line = lineArray[i];
|
|
5150
|
-
const delay =
|
|
5506
|
+
const delay =
|
|
5507
|
+
i <= referenceIndex
|
|
5508
|
+
? Math.min(delayCounter, maxStaggerSteps) * delayIncrement
|
|
5509
|
+
: 0;
|
|
5151
5510
|
|
|
5152
5511
|
if (i <= referenceIndex && !line.classList.contains('lyrics-gap')) {
|
|
5153
5512
|
delayCounter += 1;
|
|
@@ -5155,17 +5514,27 @@ export class AmLyrics extends LitElement {
|
|
|
5155
5514
|
|
|
5156
5515
|
line.style.setProperty('--scroll-delta', `${delta}px`);
|
|
5157
5516
|
line.style.setProperty('--lyrics-line-delay', `${delay}ms`);
|
|
5158
|
-
|
|
5517
|
+
lineDelays.set(line, delay);
|
|
5159
5518
|
|
|
5160
5519
|
newAnimatingLines.push(line);
|
|
5161
5520
|
|
|
5162
|
-
const lineDuration = duration + delay;
|
|
5521
|
+
const lineDuration = duration + 100 + delay;
|
|
5163
5522
|
if (lineDuration > maxAnimationDuration) {
|
|
5164
5523
|
maxAnimationDuration = lineDuration;
|
|
5165
5524
|
}
|
|
5166
5525
|
}
|
|
5167
5526
|
}
|
|
5168
5527
|
|
|
5528
|
+
/* Preserve the staggered starts, but make every line settle together.
|
|
5529
|
+
This keeps the selected line from drifting after its neighbours. */
|
|
5530
|
+
for (const line of newAnimatingLines) {
|
|
5531
|
+
const delay = lineDelays.get(line) ?? 0;
|
|
5532
|
+
line.style.setProperty(
|
|
5533
|
+
'--scroll-duration',
|
|
5534
|
+
`${Math.max(100, maxAnimationDuration - delay)}ms`,
|
|
5535
|
+
);
|
|
5536
|
+
}
|
|
5537
|
+
|
|
5169
5538
|
// --- Step 3: Force reflow so the browser sees the class removal ---
|
|
5170
5539
|
// Use offsetHeight which is cheaper than getBoundingClientRect
|
|
5171
5540
|
// eslint-disable-next-line no-void
|
|
@@ -5322,44 +5691,419 @@ export class AmLyrics extends LitElement {
|
|
|
5322
5691
|
|
|
5323
5692
|
/**
|
|
5324
5693
|
* Update syllable highlight animation - apply CSS wipe animation
|
|
5325
|
-
* (Exact copy from YouLyPlus _updateSyllableAnimation)
|
|
5326
5694
|
*/
|
|
5327
|
-
private static
|
|
5695
|
+
private static clamp(value: number, min: number, max: number): number {
|
|
5696
|
+
return Math.min(max, Math.max(min, value));
|
|
5697
|
+
}
|
|
5698
|
+
|
|
5699
|
+
private static getVisibleCharacterCount(element: HTMLElement): number {
|
|
5700
|
+
const attrLength = parseFloat(
|
|
5701
|
+
element.getAttribute('data-word-length') || '',
|
|
5702
|
+
);
|
|
5703
|
+
if (Number.isFinite(attrLength) && attrLength > 0) return attrLength;
|
|
5704
|
+
return (element.textContent || '').replace(/\s/g, '').length;
|
|
5705
|
+
}
|
|
5706
|
+
|
|
5707
|
+
private static getLongWordWipeScale(charCount: number): number {
|
|
5708
|
+
if (charCount <= 6) return 1;
|
|
5709
|
+
return (
|
|
5710
|
+
1 +
|
|
5711
|
+
AmLyrics.clamp((charCount - 6) / 10, 0, 1) * LONG_WORD_WIPE_EXTRA_RATIO
|
|
5712
|
+
);
|
|
5713
|
+
}
|
|
5714
|
+
|
|
5715
|
+
private static applyWipeShape(element: HTMLElement, charCount: number): void {
|
|
5716
|
+
const extra =
|
|
5717
|
+
AmLyrics.clamp((charCount - 6) / 10, 0, 1) * LONG_WORD_WIPE_EXTRA_EM;
|
|
5718
|
+
const width = BASE_WIPE_GRADIENT_EM + extra;
|
|
5719
|
+
element.style.setProperty('--wipe-gradient-width', `${width.toFixed(3)}em`);
|
|
5720
|
+
element.style.setProperty(
|
|
5721
|
+
'--wipe-gradient-half',
|
|
5722
|
+
`${(width / 2).toFixed(3)}em`,
|
|
5723
|
+
);
|
|
5724
|
+
}
|
|
5725
|
+
|
|
5726
|
+
private static ensureWordWipeGeometry(
|
|
5727
|
+
charSpans: HTMLElement[],
|
|
5728
|
+
charCount: number,
|
|
5729
|
+
): void {
|
|
5730
|
+
if (charSpans.length === 0) return;
|
|
5731
|
+
|
|
5732
|
+
const approxWidthCh = Math.max(1, charCount || charSpans.length);
|
|
5733
|
+
charSpans.forEach((span, index) => {
|
|
5734
|
+
if (!span.style.getPropertyValue('--word-wipe-width')) {
|
|
5735
|
+
span.style.setProperty('--word-wipe-width', `${approxWidthCh}ch`);
|
|
5736
|
+
}
|
|
5737
|
+
|
|
5738
|
+
if (!span.style.getPropertyValue('--char-wipe-position')) {
|
|
5739
|
+
const startPct = Number.parseFloat(
|
|
5740
|
+
span.dataset.wipeStart || `${index / Math.max(1, charSpans.length)}`,
|
|
5741
|
+
);
|
|
5742
|
+
span.style.setProperty(
|
|
5743
|
+
'--char-wipe-position',
|
|
5744
|
+
`${-(AmLyrics.clamp(startPct, 0, 1) * approxWidthCh)}ch`,
|
|
5745
|
+
);
|
|
5746
|
+
}
|
|
5747
|
+
});
|
|
5748
|
+
}
|
|
5749
|
+
|
|
5750
|
+
private static clearPreHighlight(syllable: HTMLElement): void {
|
|
5751
|
+
const target = syllable;
|
|
5752
|
+
target.classList.remove('pre-highlight');
|
|
5753
|
+
target.style.removeProperty('--pre-wipe-duration');
|
|
5754
|
+
target.style.removeProperty('--pre-wipe-delay');
|
|
5755
|
+
target.style.animation = '';
|
|
5756
|
+
target
|
|
5757
|
+
.querySelectorAll('.pre-wipe-lead')
|
|
5758
|
+
.forEach(element => AmLyrics.clearPreWipeLead(element as HTMLElement));
|
|
5759
|
+
}
|
|
5760
|
+
|
|
5761
|
+
private static clearPreWipeLead(element: HTMLElement): void {
|
|
5762
|
+
element.classList.remove('pre-wipe-lead');
|
|
5763
|
+
element.style.removeProperty('--pre-wipe-duration');
|
|
5764
|
+
element.style.removeProperty('--pre-wipe-delay');
|
|
5765
|
+
}
|
|
5766
|
+
|
|
5767
|
+
private static hasTextBoundaryAfter(syllable: HTMLElement): boolean {
|
|
5768
|
+
return /\s$/.test(syllable.textContent || '');
|
|
5769
|
+
}
|
|
5770
|
+
|
|
5771
|
+
private static getSyllableWordIndex(syllable: HTMLElement): string {
|
|
5772
|
+
const wordElement = AmLyrics.getWordElementForSyllable(syllable);
|
|
5773
|
+
const virtualWordId = wordElement?.dataset.virtualWordId;
|
|
5774
|
+
if (virtualWordId) {
|
|
5775
|
+
return `virtual:${virtualWordId}`;
|
|
5776
|
+
}
|
|
5777
|
+
|
|
5778
|
+
const virtualWordStart = wordElement?.dataset.virtualWordStart;
|
|
5779
|
+
const virtualWordEnd = wordElement?.dataset.virtualWordEnd;
|
|
5780
|
+
if (virtualWordStart || virtualWordEnd) {
|
|
5781
|
+
return `virtual:${virtualWordStart || ''}:${virtualWordEnd || ''}`;
|
|
5782
|
+
}
|
|
5783
|
+
|
|
5784
|
+
return (
|
|
5785
|
+
syllable.getAttribute('data-word-index') ||
|
|
5786
|
+
syllable.getAttribute('data-syllable-index') ||
|
|
5787
|
+
''
|
|
5788
|
+
);
|
|
5789
|
+
}
|
|
5790
|
+
|
|
5791
|
+
private static getNextWordSyllable(
|
|
5792
|
+
syllables: HTMLElement[],
|
|
5793
|
+
index: number,
|
|
5794
|
+
): HTMLElement | null {
|
|
5795
|
+
const current = syllables[index];
|
|
5796
|
+
const currentWordIndex = AmLyrics.getSyllableWordIndex(current);
|
|
5797
|
+
const previousSyllable = current;
|
|
5798
|
+
|
|
5799
|
+
for (let i = index + 1; i < syllables.length; i += 1) {
|
|
5800
|
+
const candidate = syllables[i];
|
|
5801
|
+
if (candidate.classList.contains('transliteration')) {
|
|
5802
|
+
// eslint-disable-next-line no-continue
|
|
5803
|
+
continue;
|
|
5804
|
+
}
|
|
5805
|
+
|
|
5806
|
+
const candidateWordIndex = AmLyrics.getSyllableWordIndex(candidate);
|
|
5807
|
+
if (
|
|
5808
|
+
candidateWordIndex === currentWordIndex ||
|
|
5809
|
+
!AmLyrics.hasTextBoundaryAfter(previousSyllable)
|
|
5810
|
+
) {
|
|
5811
|
+
return null;
|
|
5812
|
+
}
|
|
5813
|
+
|
|
5814
|
+
return candidate;
|
|
5815
|
+
}
|
|
5816
|
+
|
|
5817
|
+
return null;
|
|
5818
|
+
}
|
|
5819
|
+
|
|
5820
|
+
private static getPreviousNonTransliterationSyllable(
|
|
5821
|
+
syllables: HTMLElement[],
|
|
5822
|
+
index: number,
|
|
5823
|
+
): HTMLElement | null {
|
|
5824
|
+
for (let i = index - 1; i >= 0; i -= 1) {
|
|
5825
|
+
const candidate = syllables[i];
|
|
5826
|
+
if (!candidate.classList.contains('transliteration')) {
|
|
5827
|
+
return candidate;
|
|
5828
|
+
}
|
|
5829
|
+
}
|
|
5830
|
+
|
|
5831
|
+
return null;
|
|
5832
|
+
}
|
|
5833
|
+
|
|
5834
|
+
private static getRenderedWordSyllables(
|
|
5328
5835
|
syllable: HTMLElement,
|
|
5329
|
-
|
|
5836
|
+
): HTMLElement[] {
|
|
5837
|
+
const wordElement = AmLyrics.getWordElementForSyllable(syllable);
|
|
5838
|
+
const wordElements = AmLyrics.getCachedVirtualWordElements(wordElement);
|
|
5839
|
+
const wordSyllables = wordElements.flatMap(
|
|
5840
|
+
element =>
|
|
5841
|
+
Array.from(
|
|
5842
|
+
element.querySelectorAll('.lyrics-syllable'),
|
|
5843
|
+
) as HTMLElement[],
|
|
5844
|
+
);
|
|
5845
|
+
|
|
5846
|
+
return wordSyllables.filter(
|
|
5847
|
+
wordSyllable => !wordSyllable.classList.contains('transliteration'),
|
|
5848
|
+
);
|
|
5849
|
+
}
|
|
5850
|
+
|
|
5851
|
+
private static getWordElementForSyllable(
|
|
5852
|
+
syllable: HTMLElement,
|
|
5853
|
+
): HTMLElement | undefined {
|
|
5854
|
+
return syllable.parentElement?.parentElement as HTMLElement | undefined;
|
|
5855
|
+
}
|
|
5856
|
+
|
|
5857
|
+
private static getWordPreWipeKey(syllable: HTMLElement): string {
|
|
5858
|
+
const wordElement = AmLyrics.getWordElementForSyllable(syllable);
|
|
5859
|
+
return (
|
|
5860
|
+
wordElement?.dataset.virtualWordId ||
|
|
5861
|
+
`${syllable.getAttribute('data-start-time') || ''}:${AmLyrics.getSyllableWordIndex(
|
|
5862
|
+
syllable,
|
|
5863
|
+
)}`
|
|
5864
|
+
);
|
|
5865
|
+
}
|
|
5866
|
+
|
|
5867
|
+
private static isPreWipeArmed(syllable: HTMLElement): boolean {
|
|
5868
|
+
const wordElement = AmLyrics.getWordElementForSyllable(syllable);
|
|
5869
|
+
const target = wordElement as any;
|
|
5870
|
+
return Boolean(
|
|
5871
|
+
target?._wordPreWipeKey === AmLyrics.getWordPreWipeKey(syllable),
|
|
5872
|
+
);
|
|
5873
|
+
}
|
|
5874
|
+
|
|
5875
|
+
private static applyWordPreWipe(
|
|
5876
|
+
nextSyllable: HTMLElement,
|
|
5877
|
+
wordSyllables: HTMLElement[],
|
|
5878
|
+
currentTimeMs: number,
|
|
5879
|
+
preWipeStartMs: number,
|
|
5880
|
+
preWipeDurationMs: number,
|
|
5330
5881
|
): void {
|
|
5331
|
-
if (
|
|
5882
|
+
if (AmLyrics.isPreWipeArmed(nextSyllable)) return;
|
|
5883
|
+
|
|
5884
|
+
const wordElement = AmLyrics.getWordElementForSyllable(nextSyllable);
|
|
5885
|
+
const wordElements = AmLyrics.getCachedVirtualWordElements(wordElement);
|
|
5886
|
+
const charSpans = AmLyrics.getCachedVirtualWordCharSpans(wordElement, []);
|
|
5887
|
+
const elapsedPreWipe = currentTimeMs - preWipeStartMs;
|
|
5888
|
+
const charCount =
|
|
5889
|
+
charSpans.length ||
|
|
5890
|
+
wordSyllables.reduce(
|
|
5891
|
+
(total, wordSyllable) =>
|
|
5892
|
+
total + AmLyrics.getVisibleCharacterCount(wordSyllable),
|
|
5893
|
+
0,
|
|
5894
|
+
) ||
|
|
5895
|
+
AmLyrics.getVisibleCharacterCount(nextSyllable);
|
|
5896
|
+
AmLyrics.ensureWordWipeGeometry(charSpans, charCount);
|
|
5897
|
+
const leadChar = charSpans[0];
|
|
5898
|
+
const leadCharSyllable = leadChar?.closest(
|
|
5899
|
+
'.lyrics-syllable',
|
|
5900
|
+
) as HTMLElement | null;
|
|
5901
|
+
const preWipeSyllable =
|
|
5902
|
+
leadCharSyllable || wordSyllables[0] || nextSyllable;
|
|
5332
5903
|
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
)
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5904
|
+
AmLyrics.applyWipeShape(preWipeSyllable, charCount);
|
|
5905
|
+
preWipeSyllable.style.setProperty(
|
|
5906
|
+
'--pre-wipe-duration',
|
|
5907
|
+
`${preWipeDurationMs}ms`,
|
|
5908
|
+
);
|
|
5909
|
+
preWipeSyllable.style.setProperty(
|
|
5910
|
+
'--pre-wipe-delay',
|
|
5911
|
+
`${-elapsedPreWipe}ms`,
|
|
5912
|
+
);
|
|
5913
|
+
preWipeSyllable.classList.add('pre-highlight');
|
|
5914
|
+
|
|
5915
|
+
// Character-animated words still need a single word-leading gradient.
|
|
5916
|
+
// Giving every glyph this state creates one gradient per character and
|
|
5917
|
+
// makes the whole word enter the pre-wipe continuation simultaneously.
|
|
5918
|
+
if (leadChar) {
|
|
5919
|
+
AmLyrics.applyWipeShape(leadChar, charCount);
|
|
5920
|
+
leadChar.style.setProperty(
|
|
5921
|
+
'--pre-wipe-duration',
|
|
5922
|
+
`${preWipeDurationMs}ms`,
|
|
5923
|
+
);
|
|
5924
|
+
leadChar.style.setProperty('--pre-wipe-delay', `${-elapsedPreWipe}ms`);
|
|
5925
|
+
leadChar.classList.add('pre-wipe-lead');
|
|
5926
|
+
}
|
|
5927
|
+
|
|
5928
|
+
wordElements.forEach(element => {
|
|
5929
|
+
const target = element as any;
|
|
5930
|
+
target._wordPreWipeKey = AmLyrics.getWordPreWipeKey(nextSyllable);
|
|
5931
|
+
});
|
|
5932
|
+
}
|
|
5933
|
+
|
|
5934
|
+
private static maybePreWipeNextWord(
|
|
5935
|
+
syllables: HTMLElement[],
|
|
5936
|
+
index: number,
|
|
5937
|
+
currentTimeMs: number,
|
|
5938
|
+
currentEndTimeMs: number,
|
|
5939
|
+
): void {
|
|
5940
|
+
const syllable = syllables[index];
|
|
5941
|
+
if (
|
|
5942
|
+
syllable.classList.contains('line-synced') ||
|
|
5943
|
+
syllable.classList.contains('transliteration') ||
|
|
5944
|
+
syllable.closest('.lyrics-gap')
|
|
5945
|
+
) {
|
|
5946
|
+
return;
|
|
5947
|
+
}
|
|
5948
|
+
|
|
5949
|
+
const currentWordReady =
|
|
5950
|
+
syllable.classList.contains('finished') ||
|
|
5951
|
+
currentTimeMs >= currentEndTimeMs - WORD_PRE_WIPE_HANDOFF_LEAD_MS;
|
|
5952
|
+
if (!currentWordReady) {
|
|
5953
|
+
return;
|
|
5954
|
+
}
|
|
5955
|
+
|
|
5956
|
+
const nextSyllable = AmLyrics.getNextWordSyllable(syllables, index);
|
|
5957
|
+
if (
|
|
5958
|
+
!nextSyllable ||
|
|
5959
|
+
nextSyllable.classList.contains('line-synced') ||
|
|
5960
|
+
nextSyllable.classList.contains('transliteration') ||
|
|
5961
|
+
nextSyllable.closest('.lyrics-gap') ||
|
|
5962
|
+
nextSyllable.classList.contains('highlight') ||
|
|
5963
|
+
nextSyllable.classList.contains('finished')
|
|
5964
|
+
) {
|
|
5965
|
+
return;
|
|
5966
|
+
}
|
|
5967
|
+
|
|
5968
|
+
const nextStartTimeMs = (nextSyllable as any)._cachedStartTime;
|
|
5969
|
+
if (!Number.isFinite(nextStartTimeMs)) return;
|
|
5970
|
+
|
|
5971
|
+
const gapMs = nextStartTimeMs - currentEndTimeMs;
|
|
5972
|
+
if (gapMs > NEXT_WORD_PRE_WIPE_MAX_GAP_MS || gapMs < -50) {
|
|
5973
|
+
return;
|
|
5974
|
+
}
|
|
5975
|
+
|
|
5976
|
+
const nextWordSyllables = AmLyrics.getRenderedWordSyllables(nextSyllable);
|
|
5977
|
+
const preWipeSyllables =
|
|
5978
|
+
nextWordSyllables.length > 0 ? nextWordSyllables : [nextSyllable];
|
|
5979
|
+
const wordElement = AmLyrics.getWordElementForSyllable(nextSyllable);
|
|
5980
|
+
const wordCharSpans = AmLyrics.getCachedVirtualWordCharSpans(
|
|
5981
|
+
wordElement,
|
|
5982
|
+
[],
|
|
5983
|
+
);
|
|
5984
|
+
const charCount =
|
|
5985
|
+
wordCharSpans.length ||
|
|
5986
|
+
preWipeSyllables.reduce(
|
|
5987
|
+
(total, wordSyllable) =>
|
|
5988
|
+
total + AmLyrics.getVisibleCharacterCount(wordSyllable),
|
|
5989
|
+
0,
|
|
5990
|
+
);
|
|
5991
|
+
if (charCount <= 0) return;
|
|
5992
|
+
|
|
5993
|
+
const preWipeDuration = AmLyrics.clamp(
|
|
5994
|
+
64 + charCount * 9,
|
|
5995
|
+
NEXT_WORD_PRE_WIPE_MIN_DURATION_MS,
|
|
5996
|
+
NEXT_WORD_PRE_WIPE_MAX_DURATION_MS,
|
|
5997
|
+
);
|
|
5998
|
+
const preWipeStart = Math.max(
|
|
5999
|
+
nextStartTimeMs - preWipeDuration,
|
|
6000
|
+
currentEndTimeMs - WORD_PRE_WIPE_HANDOFF_LEAD_MS,
|
|
6001
|
+
);
|
|
6002
|
+
|
|
6003
|
+
if (currentTimeMs < preWipeStart || currentTimeMs >= nextStartTimeMs) {
|
|
6004
|
+
return;
|
|
6005
|
+
}
|
|
6006
|
+
|
|
6007
|
+
AmLyrics.applyWordPreWipe(
|
|
6008
|
+
nextSyllable,
|
|
6009
|
+
preWipeSyllables,
|
|
6010
|
+
currentTimeMs,
|
|
6011
|
+
preWipeStart,
|
|
6012
|
+
preWipeDuration,
|
|
6013
|
+
);
|
|
6014
|
+
}
|
|
6015
|
+
|
|
6016
|
+
private static getCachedCharSpans(element: HTMLElement): HTMLElement[] {
|
|
6017
|
+
const cacheTarget = element as any;
|
|
6018
|
+
if (!cacheTarget._cachedCharSpans) {
|
|
6019
|
+
cacheTarget._cachedCharSpans = Array.from(
|
|
6020
|
+
element.querySelectorAll('span.char'),
|
|
6021
|
+
) as HTMLElement[];
|
|
6022
|
+
}
|
|
6023
|
+
return cacheTarget._cachedCharSpans as HTMLElement[];
|
|
6024
|
+
}
|
|
6025
|
+
|
|
6026
|
+
private static getCachedVirtualWordElements(
|
|
6027
|
+
wordElement: HTMLElement | undefined,
|
|
6028
|
+
): HTMLElement[] {
|
|
6029
|
+
if (!wordElement) return [];
|
|
6030
|
+
|
|
6031
|
+
const cacheTarget = wordElement as any;
|
|
6032
|
+
if (cacheTarget._cachedVirtualWordElements) {
|
|
6033
|
+
return cacheTarget._cachedVirtualWordElements as HTMLElement[];
|
|
6034
|
+
}
|
|
6035
|
+
|
|
6036
|
+
const { virtualWordId } = wordElement.dataset;
|
|
6037
|
+
let wordElements: HTMLElement[] = [wordElement];
|
|
6038
|
+
if (virtualWordId && wordElement.parentElement) {
|
|
5343
6039
|
wordElements = Array.from(
|
|
5344
6040
|
wordElement.parentElement.querySelectorAll('.lyrics-word'),
|
|
5345
6041
|
).filter(
|
|
5346
6042
|
el => (el as HTMLElement).dataset.virtualWordId === virtualWordId,
|
|
5347
6043
|
) as HTMLElement[];
|
|
5348
|
-
} else if (wordElement) {
|
|
5349
|
-
wordElements = [wordElement as HTMLElement];
|
|
5350
6044
|
}
|
|
5351
|
-
|
|
6045
|
+
|
|
6046
|
+
wordElements.forEach(element => {
|
|
6047
|
+
const target = element as any;
|
|
6048
|
+
target._cachedVirtualWordElements = wordElements;
|
|
6049
|
+
});
|
|
6050
|
+
|
|
6051
|
+
return wordElements;
|
|
6052
|
+
}
|
|
6053
|
+
|
|
6054
|
+
private static getCachedVirtualWordCharSpans(
|
|
6055
|
+
wordElement: HTMLElement | undefined,
|
|
6056
|
+
fallbackCharSpans: HTMLElement[],
|
|
6057
|
+
): HTMLElement[] {
|
|
6058
|
+
if (!wordElement) return fallbackCharSpans;
|
|
6059
|
+
|
|
6060
|
+
const cacheTarget = wordElement as any;
|
|
6061
|
+
if (cacheTarget._cachedVirtualWordCharSpans) {
|
|
6062
|
+
return cacheTarget._cachedVirtualWordCharSpans as HTMLElement[];
|
|
6063
|
+
}
|
|
6064
|
+
|
|
6065
|
+
const wordElements = AmLyrics.getCachedVirtualWordElements(wordElement);
|
|
6066
|
+
const charSpans = wordElements.flatMap(
|
|
5352
6067
|
word => Array.from(word.querySelectorAll('span.char')) as HTMLElement[],
|
|
5353
6068
|
);
|
|
5354
|
-
const
|
|
5355
|
-
|
|
6069
|
+
const result = charSpans.length > 0 ? charSpans : fallbackCharSpans;
|
|
6070
|
+
|
|
6071
|
+
wordElements.forEach(element => {
|
|
6072
|
+
const target = element as any;
|
|
6073
|
+
target._cachedVirtualWordCharSpans = result;
|
|
6074
|
+
});
|
|
6075
|
+
|
|
6076
|
+
return result;
|
|
6077
|
+
}
|
|
6078
|
+
|
|
6079
|
+
private static updateSyllableAnimation(
|
|
6080
|
+
syllable: HTMLElement,
|
|
6081
|
+
elapsedTimeMs = 0,
|
|
6082
|
+
): void {
|
|
6083
|
+
if (syllable.classList.contains('highlight')) return;
|
|
6084
|
+
|
|
6085
|
+
const { classList } = syllable;
|
|
6086
|
+
const hadPreHighlight = classList.contains('pre-highlight');
|
|
6087
|
+
const isRTL = classList.contains('rtl-text');
|
|
6088
|
+
const charSpans = AmLyrics.getCachedCharSpans(syllable);
|
|
6089
|
+
const wordElement = syllable.parentElement?.parentElement; // syllable-wrap -> word
|
|
6090
|
+
const typedWordElement = wordElement as HTMLElement | undefined;
|
|
6091
|
+
const allWordElements =
|
|
6092
|
+
AmLyrics.getCachedVirtualWordElements(typedWordElement);
|
|
6093
|
+
const allWordCharSpans = AmLyrics.getCachedVirtualWordCharSpans(
|
|
6094
|
+
typedWordElement,
|
|
6095
|
+
charSpans,
|
|
6096
|
+
);
|
|
6097
|
+
const isGrowable = typedWordElement?.classList.contains('growable');
|
|
6098
|
+
const isCharRise = typedWordElement?.classList.contains('char-rise');
|
|
6099
|
+
const isCharDrag = typedWordElement?.classList.contains('char-drag');
|
|
5356
6100
|
const isFirstSyllable =
|
|
5357
6101
|
syllable.getAttribute('data-syllable-index') === '0';
|
|
5358
6102
|
const syllableStartMs = parseFloat(
|
|
5359
6103
|
syllable.getAttribute('data-start-time') || '0',
|
|
5360
6104
|
);
|
|
5361
6105
|
const virtualWordStartMs = parseFloat(
|
|
5362
|
-
|
|
6106
|
+
typedWordElement?.dataset.virtualWordStart || '',
|
|
5363
6107
|
);
|
|
5364
6108
|
const isFirstInVirtualWord =
|
|
5365
6109
|
isFirstSyllable &&
|
|
@@ -5377,8 +6121,11 @@ export class AmLyrics extends LitElement {
|
|
|
5377
6121
|
syllable.getAttribute('data-duration') ||
|
|
5378
6122
|
'0',
|
|
5379
6123
|
) || syllableDurationMs;
|
|
6124
|
+
const wordElapsedTimeMs = Number.isFinite(virtualWordStartMs)
|
|
6125
|
+
? elapsedTimeMs + (syllableStartMs - virtualWordStartMs)
|
|
6126
|
+
: elapsedTimeMs;
|
|
6127
|
+
const charWipeDurationMs = Math.max(wordDurationMs, syllableDurationMs);
|
|
5380
6128
|
|
|
5381
|
-
// Use a Map to collect animations like YouLyPlus
|
|
5382
6129
|
const charAnimationsMap = new Map<HTMLElement, string>();
|
|
5383
6130
|
const styleUpdates: Array<{
|
|
5384
6131
|
element: HTMLElement;
|
|
@@ -5446,21 +6193,81 @@ export class AmLyrics extends LitElement {
|
|
|
5446
6193
|
});
|
|
5447
6194
|
}
|
|
5448
6195
|
|
|
6196
|
+
if (isCharDrag && isFirstInVirtualWord && allWordCharSpans.length > 0) {
|
|
6197
|
+
const finalDuration = Math.max(wordDurationMs, syllableDurationMs);
|
|
6198
|
+
const baseDelayPerChar = AmLyrics.clamp(finalDuration * 0.15, 64, 118);
|
|
6199
|
+
const dragDurationMs = AmLyrics.clamp(finalDuration * 0.82, 560, 900);
|
|
6200
|
+
|
|
6201
|
+
allWordCharSpans.forEach(span => {
|
|
6202
|
+
const charIndex = parseFloat(span.dataset.syllableCharIndex || '0');
|
|
6203
|
+
const dragDelay = baseDelayPerChar * charIndex;
|
|
6204
|
+
|
|
6205
|
+
charAnimationsMap.set(
|
|
6206
|
+
span,
|
|
6207
|
+
`drag-char ${dragDurationMs}ms ease ${dragDelay}ms forwards`,
|
|
6208
|
+
);
|
|
6209
|
+
});
|
|
6210
|
+
}
|
|
6211
|
+
|
|
5449
6212
|
// Step 2: Wipe Pass
|
|
5450
6213
|
if (charSpans.length > 0) {
|
|
5451
|
-
|
|
6214
|
+
const wipeCharCount =
|
|
6215
|
+
allWordCharSpans.length ||
|
|
6216
|
+
charSpans.length ||
|
|
6217
|
+
AmLyrics.getVisibleCharacterCount(syllable);
|
|
6218
|
+
const wipeScale = AmLyrics.getLongWordWipeScale(wipeCharCount);
|
|
6219
|
+
AmLyrics.applyWipeShape(syllable, wipeCharCount);
|
|
6220
|
+
AmLyrics.ensureWordWipeGeometry(allWordCharSpans, wipeCharCount);
|
|
6221
|
+
allWordCharSpans.forEach(span =>
|
|
6222
|
+
AmLyrics.applyWipeShape(span, wipeCharCount),
|
|
6223
|
+
);
|
|
6224
|
+
|
|
6225
|
+
const hasWordLevelWipe =
|
|
6226
|
+
!isFirstInVirtualWord &&
|
|
6227
|
+
(Boolean((typedWordElement as any)?._wordWipeStarted) ||
|
|
6228
|
+
allWordCharSpans.some(span => span.style.animation.includes('wipe')));
|
|
6229
|
+
let charSpansToAnimate = charSpans;
|
|
6230
|
+
if (isFirstInVirtualWord) {
|
|
6231
|
+
charSpansToAnimate = allWordCharSpans;
|
|
6232
|
+
} else if (hasWordLevelWipe) {
|
|
6233
|
+
charSpansToAnimate = [];
|
|
6234
|
+
}
|
|
6235
|
+
|
|
6236
|
+
if (charSpansToAnimate.length > 0 && allWordElements.length > 0) {
|
|
6237
|
+
allWordElements.forEach(element => {
|
|
6238
|
+
const target = element as any;
|
|
6239
|
+
target._wordWipeStarted = true;
|
|
6240
|
+
target._wordPreWipeKey = undefined;
|
|
6241
|
+
});
|
|
6242
|
+
}
|
|
6243
|
+
|
|
6244
|
+
charSpansToAnimate.forEach((span, charIndex) => {
|
|
5452
6245
|
const startPct = parseFloat(span.dataset.wipeStart || '0');
|
|
5453
6246
|
const durationPct = parseFloat(span.dataset.wipeDuration || '0');
|
|
6247
|
+
const globalCharIndex = parseFloat(
|
|
6248
|
+
span.dataset.syllableCharIndex || `${charIndex}`,
|
|
6249
|
+
);
|
|
5454
6250
|
|
|
5455
|
-
const
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
const
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
6251
|
+
const hadCharPreWipe =
|
|
6252
|
+
span.classList.contains('pre-wipe-lead') ||
|
|
6253
|
+
(hadPreHighlight && globalCharIndex === 0);
|
|
6254
|
+
const charStartMs = charWipeDurationMs * startPct;
|
|
6255
|
+
const remainingWordWipeMs = Math.max(
|
|
6256
|
+
0,
|
|
6257
|
+
charWipeDurationMs - charStartMs,
|
|
6258
|
+
);
|
|
6259
|
+
const wipeDelay = charStartMs - wordElapsedTimeMs;
|
|
6260
|
+
const wipeDuration = Math.min(
|
|
6261
|
+
charWipeDurationMs * durationPct * wipeScale,
|
|
6262
|
+
remainingWordWipeMs,
|
|
6263
|
+
);
|
|
6264
|
+
const useStartAnimation =
|
|
6265
|
+
isFirstInContainer && globalCharIndex === 0 && !hadCharPreWipe;
|
|
6266
|
+
let charWipeAnimation = 'char-wipe';
|
|
6267
|
+
if (hadCharPreWipe) {
|
|
6268
|
+
charWipeAnimation = 'char-wipe';
|
|
6269
|
+
} else if (useStartAnimation) {
|
|
6270
|
+
charWipeAnimation = 'char-start-wipe';
|
|
5464
6271
|
}
|
|
5465
6272
|
|
|
5466
6273
|
const existingAnimation =
|
|
@@ -5471,36 +6278,37 @@ export class AmLyrics extends LitElement {
|
|
|
5471
6278
|
if (
|
|
5472
6279
|
existingAnimation &&
|
|
5473
6280
|
(existingAnimation.includes('grow-dynamic') ||
|
|
5474
|
-
existingAnimation.includes('rise-char')
|
|
6281
|
+
existingAnimation.includes('rise-char') ||
|
|
6282
|
+
existingAnimation.includes('drag-char'))
|
|
5475
6283
|
) {
|
|
5476
6284
|
animationParts.push(existingAnimation.split(',')[0].trim());
|
|
5477
6285
|
}
|
|
5478
|
-
if (
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
6286
|
+
if (
|
|
6287
|
+
globalCharIndex > 0 &&
|
|
6288
|
+
!hadCharPreWipe &&
|
|
6289
|
+
wipeDelay > 0 &&
|
|
6290
|
+
wipeDuration > 0
|
|
6291
|
+
) {
|
|
6292
|
+
const measuredDuration = Number.parseFloat(
|
|
5484
6293
|
span.dataset.preWipeDuration || '100',
|
|
5485
6294
|
);
|
|
5486
6295
|
const preWipeDuration = Math.min(
|
|
5487
|
-
|
|
6296
|
+
measuredDuration,
|
|
5488
6297
|
wipeDuration * 0.9,
|
|
5489
|
-
|
|
5490
|
-
|
|
6298
|
+
charWipeDurationMs * 0.08,
|
|
6299
|
+
wipeDelay,
|
|
5491
6300
|
);
|
|
5492
|
-
const animDelay = arrivalTime - preWipeDuration;
|
|
5493
6301
|
|
|
5494
6302
|
if (preWipeDuration >= 16) {
|
|
5495
6303
|
animationParts.push(
|
|
5496
|
-
`pre-wipe
|
|
6304
|
+
`char-pre-wipe ${preWipeDuration}ms linear ${wipeDelay - preWipeDuration}ms none`,
|
|
5497
6305
|
);
|
|
5498
6306
|
}
|
|
5499
6307
|
}
|
|
5500
|
-
|
|
5501
6308
|
if (wipeDuration > 0) {
|
|
6309
|
+
const wipeFillMode = hadCharPreWipe ? 'both' : 'forwards';
|
|
5502
6310
|
animationParts.push(
|
|
5503
|
-
`${charWipeAnimation} ${wipeDuration}ms linear ${wipeDelay}ms
|
|
6311
|
+
`${charWipeAnimation} ${wipeDuration}ms linear ${wipeDelay}ms ${wipeFillMode}`,
|
|
5504
6312
|
);
|
|
5505
6313
|
}
|
|
5506
6314
|
|
|
@@ -5513,10 +6321,15 @@ export class AmLyrics extends LitElement {
|
|
|
5513
6321
|
const wipeRatio = parseFloat(
|
|
5514
6322
|
syllable.getAttribute('data-wipe-ratio') || '1',
|
|
5515
6323
|
);
|
|
5516
|
-
const
|
|
6324
|
+
const wipeCharCount = AmLyrics.getVisibleCharacterCount(syllable);
|
|
6325
|
+
const wipeScale = AmLyrics.getLongWordWipeScale(wipeCharCount);
|
|
6326
|
+
const visualDuration = syllableDurationMs * wipeRatio * wipeScale;
|
|
6327
|
+
AmLyrics.applyWipeShape(syllable, wipeCharCount);
|
|
5517
6328
|
|
|
5518
6329
|
let wipeAnimation = 'wipe';
|
|
5519
|
-
if (
|
|
6330
|
+
if (hadPreHighlight) {
|
|
6331
|
+
wipeAnimation = isRTL ? 'wipe-from-pre-rtl' : 'wipe-from-pre';
|
|
6332
|
+
} else if (isFirstInContainer) {
|
|
5520
6333
|
wipeAnimation = isRTL ? 'start-wipe-rtl' : 'start-wipe';
|
|
5521
6334
|
} else {
|
|
5522
6335
|
wipeAnimation = isRTL ? 'wipe-rtl' : 'wipe';
|
|
@@ -5530,18 +6343,28 @@ export class AmLyrics extends LitElement {
|
|
|
5530
6343
|
}
|
|
5531
6344
|
|
|
5532
6345
|
// --- WRITE PHASE ---
|
|
6346
|
+
if (allWordElements.length > 0) {
|
|
6347
|
+
allWordElements.forEach(element => {
|
|
6348
|
+
const target = element as any;
|
|
6349
|
+
target._wordPreWipeKey = undefined;
|
|
6350
|
+
});
|
|
6351
|
+
}
|
|
6352
|
+
|
|
5533
6353
|
classList.remove('pre-highlight');
|
|
5534
6354
|
classList.add('highlight');
|
|
6355
|
+
allWordCharSpans.forEach(span => AmLyrics.clearPreWipeLead(span));
|
|
6356
|
+
|
|
6357
|
+
// Apply keyframe variables before assigning animation strings so the
|
|
6358
|
+
// first painted frame never uses fallback transform values.
|
|
6359
|
+
for (const update of styleUpdates) {
|
|
6360
|
+
update.element.style.setProperty(update.property, update.value);
|
|
6361
|
+
}
|
|
5535
6362
|
|
|
5536
6363
|
for (const [span, animationString] of charAnimationsMap.entries()) {
|
|
5537
6364
|
span.style.willChange = 'transform';
|
|
6365
|
+
span.style.removeProperty('background-color');
|
|
5538
6366
|
span.style.animation = animationString;
|
|
5539
6367
|
}
|
|
5540
|
-
|
|
5541
|
-
// Apply style updates
|
|
5542
|
-
for (const update of styleUpdates) {
|
|
5543
|
-
update.element.style.setProperty(update.property, update.value);
|
|
5544
|
-
}
|
|
5545
6368
|
}
|
|
5546
6369
|
|
|
5547
6370
|
/**
|
|
@@ -5566,6 +6389,7 @@ export class AmLyrics extends LitElement {
|
|
|
5566
6389
|
el.style.animation = '';
|
|
5567
6390
|
el.style.transition = 'none';
|
|
5568
6391
|
el.style.backgroundColor = 'var(--lyplus-text-secondary)';
|
|
6392
|
+
AmLyrics.clearPreWipeLead(el);
|
|
5569
6393
|
}
|
|
5570
6394
|
|
|
5571
6395
|
// Immediately remove all state classes
|
|
@@ -5577,12 +6401,22 @@ export class AmLyrics extends LitElement {
|
|
|
5577
6401
|
);
|
|
5578
6402
|
}
|
|
5579
6403
|
|
|
6404
|
+
private static resetWordAnimationState(line: HTMLElement): void {
|
|
6405
|
+
const wordElements = line.querySelectorAll('.lyrics-word');
|
|
6406
|
+
wordElements.forEach(wordElement => {
|
|
6407
|
+
const target = wordElement as any;
|
|
6408
|
+
target._wordPreWipeKey = undefined;
|
|
6409
|
+
target._wordWipeStarted = false;
|
|
6410
|
+
});
|
|
6411
|
+
}
|
|
6412
|
+
|
|
5580
6413
|
/**
|
|
5581
6414
|
* Reset all syllables in a line — batches deferred cleanup into a single rAF
|
|
5582
6415
|
*/
|
|
5583
6416
|
private static resetSyllables(line: HTMLElement): void {
|
|
5584
6417
|
if (!line) return;
|
|
5585
6418
|
line.classList.remove('persist-highlight');
|
|
6419
|
+
AmLyrics.resetWordAnimationState(line);
|
|
5586
6420
|
// eslint-disable-next-line no-param-reassign
|
|
5587
6421
|
(line as any)._cachedSyllableElements = null;
|
|
5588
6422
|
const syllables = line.getElementsByClassName('lyrics-syllable');
|
|
@@ -5614,6 +6448,7 @@ export class AmLyrics extends LitElement {
|
|
|
5614
6448
|
private static unfinishSyllables(line: HTMLElement): void {
|
|
5615
6449
|
if (!line) return;
|
|
5616
6450
|
line.classList.remove('persist-highlight');
|
|
6451
|
+
AmLyrics.resetWordAnimationState(line);
|
|
5617
6452
|
const syllables = line.getElementsByClassName('lyrics-syllable');
|
|
5618
6453
|
for (let i = 0; i < syllables.length; i += 1) {
|
|
5619
6454
|
const s = syllables[i] as HTMLElement;
|
|
@@ -5631,6 +6466,7 @@ export class AmLyrics extends LitElement {
|
|
|
5631
6466
|
el.style.removeProperty('background-color');
|
|
5632
6467
|
el.style.removeProperty('transition');
|
|
5633
6468
|
el.style.removeProperty('filter');
|
|
6469
|
+
AmLyrics.clearPreWipeLead(el);
|
|
5634
6470
|
}
|
|
5635
6471
|
}
|
|
5636
6472
|
}
|
|
@@ -5681,22 +6517,33 @@ export class AmLyrics extends LitElement {
|
|
|
5681
6517
|
syllable.style.animation = '';
|
|
5682
6518
|
syllable.style.removeProperty('--pre-wipe-duration');
|
|
5683
6519
|
syllable.style.removeProperty('--pre-wipe-delay');
|
|
6520
|
+
syllable.style.removeProperty('background-color');
|
|
6521
|
+
AmLyrics.applyWipeShape(
|
|
6522
|
+
syllable,
|
|
6523
|
+
AmLyrics.getVisibleCharacterCount(syllable),
|
|
6524
|
+
);
|
|
5684
6525
|
const chars = syllable.querySelectorAll('span.char');
|
|
5685
6526
|
for (let ci = 0; ci < chars.length; ci += 1) {
|
|
5686
6527
|
const charEl = chars[ci] as HTMLElement;
|
|
5687
6528
|
const currentAnim = charEl.style.animation || '';
|
|
5688
6529
|
if (
|
|
5689
6530
|
currentAnim.includes('grow-dynamic') ||
|
|
5690
|
-
currentAnim.includes('rise-char')
|
|
6531
|
+
currentAnim.includes('rise-char') ||
|
|
6532
|
+
currentAnim.includes('drag-char')
|
|
5691
6533
|
) {
|
|
5692
6534
|
const parts = currentAnim.split(',').map(p => p.trim());
|
|
5693
6535
|
const transformAnim = parts.find(
|
|
5694
|
-
p =>
|
|
6536
|
+
p =>
|
|
6537
|
+
p.includes('grow-dynamic') ||
|
|
6538
|
+
p.includes('rise-char') ||
|
|
6539
|
+
p.includes('drag-char'),
|
|
5695
6540
|
);
|
|
5696
6541
|
charEl.style.animation = transformAnim || '';
|
|
5697
6542
|
} else {
|
|
5698
6543
|
charEl.style.animation = '';
|
|
5699
6544
|
}
|
|
6545
|
+
charEl.style.backgroundColor = 'var(--lyplus-text-primary)';
|
|
6546
|
+
AmLyrics.clearPreWipeLead(charEl);
|
|
5700
6547
|
}
|
|
5701
6548
|
}
|
|
5702
6549
|
}
|
|
@@ -5751,14 +6598,20 @@ export class AmLyrics extends LitElement {
|
|
|
5751
6598
|
if (!(currentTimeMs < startTime - 1000 && !hasActiveState)) {
|
|
5752
6599
|
let preHighlightReset = false;
|
|
5753
6600
|
|
|
5754
|
-
//
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
6601
|
+
// Before the syllable starts, pre-highlight only belongs beside a
|
|
6602
|
+
// previous active word. Once the syllable starts, updateSyllableAnimation
|
|
6603
|
+
// consumes the class so the actual wipe can continue from the pre-wipe
|
|
6604
|
+
// pose instead of restarting from the beginning.
|
|
6605
|
+
if (hasPreHighlight && currentTimeMs < startTime) {
|
|
6606
|
+
const prevSyllable = AmLyrics.getPreviousNonTransliterationSyllable(
|
|
6607
|
+
syllables,
|
|
6608
|
+
i,
|
|
6609
|
+
);
|
|
6610
|
+
const previousCarriesHighlight =
|
|
6611
|
+
prevSyllable?.classList.contains('highlight') ||
|
|
6612
|
+
prevSyllable?.classList.contains('finished');
|
|
6613
|
+
if (!previousCarriesHighlight) {
|
|
6614
|
+
AmLyrics.clearPreHighlight(syllable);
|
|
5762
6615
|
preHighlightReset = true;
|
|
5763
6616
|
}
|
|
5764
6617
|
}
|
|
@@ -5791,6 +6644,8 @@ export class AmLyrics extends LitElement {
|
|
|
5791
6644
|
// Not yet started
|
|
5792
6645
|
AmLyrics.resetSyllable(syllable);
|
|
5793
6646
|
}
|
|
6647
|
+
|
|
6648
|
+
AmLyrics.maybePreWipeNextWord(syllables, i, currentTimeMs, endTime);
|
|
5794
6649
|
}
|
|
5795
6650
|
}
|
|
5796
6651
|
}
|
|
@@ -6151,6 +7006,9 @@ export class AmLyrics extends LitElement {
|
|
|
6151
7006
|
data-end-time="${endTimeMs}"
|
|
6152
7007
|
data-duration="${durationMs}"
|
|
6153
7008
|
data-syllable-index="${syllableIndex}"
|
|
7009
|
+
data-word-index="${syllableIndex}"
|
|
7010
|
+
data-word-length="${syllable.text.replace(/\s/g, '')
|
|
7011
|
+
.length}"
|
|
6154
7012
|
data-wipe-ratio="1"
|
|
6155
7013
|
>${syllable.text}</span
|
|
6156
7014
|
>${bgRomanizedText}</span
|
|
@@ -6175,6 +7033,7 @@ export class AmLyrics extends LitElement {
|
|
|
6175
7033
|
const groupGrowable = lineData?.groupGrowable ?? [];
|
|
6176
7034
|
const groupGlowing = lineData?.groupGlowing ?? [];
|
|
6177
7035
|
const groupCharRise = lineData?.groupCharRise ?? [];
|
|
7036
|
+
const groupCharDrag = lineData?.groupCharDrag ?? [];
|
|
6178
7037
|
const vwFullText = lineData?.vwFullText ?? [];
|
|
6179
7038
|
const vwFullDuration = lineData?.vwFullDuration ?? [];
|
|
6180
7039
|
const vwCharOffset = lineData?.vwCharOffset ?? [];
|
|
@@ -6182,7 +7041,6 @@ export class AmLyrics extends LitElement {
|
|
|
6182
7041
|
const vwEndMs = lineData?.vwEndMs ?? [];
|
|
6183
7042
|
const lineIsRTL = lineData?.lineIsRTL ?? false;
|
|
6184
7043
|
|
|
6185
|
-
// Create main vocals using YouLyPlus syllable structure
|
|
6186
7044
|
const mainVocalElement = html`<p
|
|
6187
7045
|
class="main-vocal-container ${lineIsRTL ? 'rtl-text' : ''}"
|
|
6188
7046
|
>
|
|
@@ -6190,28 +7048,26 @@ export class AmLyrics extends LitElement {
|
|
|
6190
7048
|
const isGrowable = groupGrowable[groupIdx];
|
|
6191
7049
|
const isGlowing = groupGlowing[groupIdx];
|
|
6192
7050
|
const isCharRise = groupCharRise[groupIdx];
|
|
6193
|
-
const
|
|
7051
|
+
const isCharDrag = groupCharDrag[groupIdx];
|
|
7052
|
+
const isAnimatedByChar = isGrowable || isCharRise || isCharDrag;
|
|
6194
7053
|
const groupLineSynced = group.some(s => s.lineSynced);
|
|
6195
7054
|
|
|
6196
7055
|
const wordText = isAnimatedByChar ? vwFullText[groupIdx] : '';
|
|
6197
7056
|
const wordDuration = isAnimatedByChar
|
|
6198
7057
|
? vwFullDuration[groupIdx]
|
|
6199
7058
|
: 0;
|
|
6200
|
-
const wordNumChars = wordText.length;
|
|
7059
|
+
const wordNumChars = wordText.replace(/\s/g, '').length;
|
|
6201
7060
|
const groupCharOffset = isAnimatedByChar
|
|
6202
7061
|
? vwCharOffset[groupIdx]
|
|
6203
7062
|
: 0;
|
|
6204
|
-
const virtualWordId =
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
const virtualWordStart = isAnimatedByChar
|
|
6208
|
-
? vwStartMs[groupIdx]
|
|
6209
|
-
: '';
|
|
6210
|
-
const virtualWordEnd = isAnimatedByChar ? vwEndMs[groupIdx] : '';
|
|
7063
|
+
const virtualWordId = `${lineIndex}:${vwStartMs[groupIdx]}:${vwEndMs[groupIdx]}`;
|
|
7064
|
+
const virtualWordStart = vwStartMs[groupIdx];
|
|
7065
|
+
const virtualWordEnd = vwEndMs[groupIdx];
|
|
6211
7066
|
|
|
6212
7067
|
let sylCharAccumulator = 0;
|
|
6213
7068
|
|
|
6214
7069
|
const groupText = group.map(s => s.text).join('');
|
|
7070
|
+
const visibleWordLength = groupText.replace(/\s/g, '').length;
|
|
6215
7071
|
const shouldAllowBreak =
|
|
6216
7072
|
groupText.trim().length >= 16 ||
|
|
6217
7073
|
/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(
|
|
@@ -6231,9 +7087,9 @@ export class AmLyrics extends LitElement {
|
|
|
6231
7087
|
return html`<span
|
|
6232
7088
|
class="lyrics-word${isGrowable ? ' growable' : ''}${isCharRise
|
|
6233
7089
|
? ' char-rise'
|
|
6234
|
-
: ''}${
|
|
6235
|
-
? '
|
|
6236
|
-
: ''}"
|
|
7090
|
+
: ''}${isCharDrag ? ' char-drag' : ''}${isGlowing
|
|
7091
|
+
? ' glowing'
|
|
7092
|
+
: ''}${shouldAllowBreak ? ' allow-break' : ''}"
|
|
6237
7093
|
data-virtual-word-id="${virtualWordId}"
|
|
6238
7094
|
data-virtual-word-start="${virtualWordStart}"
|
|
6239
7095
|
data-virtual-word-end="${virtualWordEnd}"
|
|
@@ -6264,17 +7120,39 @@ export class AmLyrics extends LitElement {
|
|
|
6264
7120
|
let syllableContent: any = text;
|
|
6265
7121
|
|
|
6266
7122
|
if (isAnimatedByChar) {
|
|
6267
|
-
let charIndexInsideSyllable = 0;
|
|
6268
7123
|
const numCharsInSyllable =
|
|
6269
7124
|
text.replace(/\s/g, '').length || 1;
|
|
7125
|
+
const hasVirtualTiming =
|
|
7126
|
+
wordDuration > 0 && Number.isFinite(virtualWordStart);
|
|
7127
|
+
const syllableStartRatio = hasVirtualTiming
|
|
7128
|
+
? AmLyrics.clamp(
|
|
7129
|
+
(startTimeMs - virtualWordStart) / wordDuration,
|
|
7130
|
+
0,
|
|
7131
|
+
1,
|
|
7132
|
+
)
|
|
7133
|
+
: 0;
|
|
7134
|
+
const syllableDurationRatio = hasVirtualTiming
|
|
7135
|
+
? AmLyrics.clamp(durationMs / wordDuration, 0, 1)
|
|
7136
|
+
: 1;
|
|
7137
|
+
let charIndexInsideSyllable = 0;
|
|
6270
7138
|
|
|
6271
7139
|
syllableContent = html`${text.split('').map(char => {
|
|
6272
7140
|
if (char === ' ') return ' ';
|
|
6273
7141
|
|
|
6274
7142
|
const charIndexInsideWord =
|
|
6275
7143
|
groupCharOffset + sylCharAccumulator;
|
|
6276
|
-
const
|
|
6277
|
-
|
|
7144
|
+
const localCharIndex = charIndexInsideSyllable;
|
|
7145
|
+
const visibleWordChars = Math.max(1, wordNumChars);
|
|
7146
|
+
const charStartPercentVal = AmLyrics.clamp(
|
|
7147
|
+
syllableStartRatio +
|
|
7148
|
+
(localCharIndex / numCharsInSyllable) *
|
|
7149
|
+
syllableDurationRatio,
|
|
7150
|
+
0,
|
|
7151
|
+
1,
|
|
7152
|
+
);
|
|
7153
|
+
const charDurationPercentVal =
|
|
7154
|
+
syllableDurationRatio / numCharsInSyllable ||
|
|
7155
|
+
1 / visibleWordChars;
|
|
6278
7156
|
|
|
6279
7157
|
sylCharAccumulator += 1;
|
|
6280
7158
|
charIndexInsideSyllable += 1;
|
|
@@ -6336,29 +7214,41 @@ export class AmLyrics extends LitElement {
|
|
|
6336
7214
|
1,
|
|
6337
7215
|
Math.max(0.3, effectiveDuration / 2000),
|
|
6338
7216
|
);
|
|
6339
|
-
const
|
|
7217
|
+
const baseTranslateYPeak =
|
|
6340
7218
|
-normalizedGrowth * (2 * peakMultiplier); // Further dampened lift peak
|
|
6341
7219
|
|
|
6342
7220
|
const position = (charIndexInsideWord + 0.5) / wordNumChars;
|
|
6343
7221
|
const horizontalOffset =
|
|
6344
7222
|
(position - 0.5) * 2 * ((charMaxScale - 1.0) * 25);
|
|
7223
|
+
const isDragMotion = isCharDrag;
|
|
7224
|
+
let charTranslateYPeak = baseTranslateYPeak;
|
|
7225
|
+
if (isCharRise) {
|
|
7226
|
+
charTranslateYPeak = 0;
|
|
7227
|
+
} else if (isDragMotion) {
|
|
7228
|
+
charTranslateYPeak = -0.78;
|
|
7229
|
+
}
|
|
6345
7230
|
|
|
7231
|
+
let motionHorizontalOffset = horizontalOffset;
|
|
7232
|
+
if (isCharRise) {
|
|
7233
|
+
motionHorizontalOffset = 0;
|
|
7234
|
+
} else if (isDragMotion) {
|
|
7235
|
+
motionHorizontalOffset = 0;
|
|
7236
|
+
}
|
|
6346
7237
|
return html`<span
|
|
6347
7238
|
class="char"
|
|
6348
7239
|
data-char-index="${charIndexInsideWord}"
|
|
6349
7240
|
data-syllable-char-index="${charIndexInsideWord}"
|
|
6350
7241
|
data-wipe-start="${charStartPercentVal.toFixed(4)}"
|
|
6351
|
-
data-wipe-duration="${
|
|
6352
|
-
4,
|
|
6353
|
-
)}"
|
|
7242
|
+
data-wipe-duration="${charDurationPercentVal.toFixed(4)}"
|
|
6354
7243
|
data-horizontal-offset="${horizontalOffset.toFixed(2)}"
|
|
6355
7244
|
data-max-scale="${charMaxScale.toFixed(3)}"
|
|
6356
7245
|
data-matrix-scale="${(charMaxScale * 0.98).toFixed(3)}"
|
|
6357
|
-
data-char-offset-x="${(
|
|
6358
|
-
|
|
6359
|
-
)}"
|
|
7246
|
+
data-char-offset-x="${(
|
|
7247
|
+
motionHorizontalOffset * 0.98
|
|
7248
|
+
).toFixed(2)}"
|
|
6360
7249
|
data-shadow-intensity="${charShadowIntensity.toFixed(3)}"
|
|
6361
7250
|
data-translate-y-peak="${charTranslateYPeak.toFixed(3)}"
|
|
7251
|
+
style="--word-wipe-width: ${visibleWordChars}ch; --char-wipe-position: -${charIndexInsideWord}ch"
|
|
6362
7252
|
>${char}</span
|
|
6363
7253
|
>`;
|
|
6364
7254
|
})}`;
|
|
@@ -6377,6 +7267,8 @@ export class AmLyrics extends LitElement {
|
|
|
6377
7267
|
data-duration="${durationMs}"
|
|
6378
7268
|
data-word-duration="${wordDuration}"
|
|
6379
7269
|
data-syllable-index="${sylIdx}"
|
|
7270
|
+
data-word-index="${groupIdx}"
|
|
7271
|
+
data-word-length="${visibleWordLength}"
|
|
6380
7272
|
data-wipe-ratio="1"
|
|
6381
7273
|
>${syllableContent}</span
|
|
6382
7274
|
>${romanizedText}</span
|
|
@@ -6617,13 +7509,14 @@ export class AmLyrics extends LitElement {
|
|
|
6617
7509
|
<button
|
|
6618
7510
|
class="download-button source-switch-btn"
|
|
6619
7511
|
title="Switch Lyrics Source"
|
|
6620
|
-
style="font-family: inherit; font-size: 11px; padding: 2px 6px; border-radius: 4px; border: 1px solid rgba(255, 255, 255, 0.2); background: transparent; cursor: pointer; color: #aaa; display: inline-flex; align-items: center;"
|
|
6621
7512
|
@click=${this.switchSource}
|
|
6622
7513
|
?disabled=${this.isFetchingAlternatives}
|
|
6623
7514
|
>
|
|
6624
7515
|
<svg
|
|
6625
|
-
class="source-switch-svg lucide lucide-arrow-down-up-icon lucide-arrow-down-up
|
|
6626
|
-
|
|
7516
|
+
class="source-switch-svg lucide lucide-arrow-down-up-icon lucide-arrow-down-up ${this
|
|
7517
|
+
.isFetchingAlternatives
|
|
7518
|
+
? 'is-loading'
|
|
7519
|
+
: ''}"
|
|
6627
7520
|
xmlns="http://www.w3.org/2000/svg"
|
|
6628
7521
|
width="12"
|
|
6629
7522
|
height="12"
|