@uimaxbai/am-lyrics 1.5.4 → 1.5.5
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 +876 -342
- package/dist/src/am-lyrics.js.map +1 -1
- package/dist/src/react.js +876 -342
- package/dist/src/react.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/AmLyrics.ts +1155 -384
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.5';
|
|
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 = 120;
|
|
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 {
|
|
@@ -444,11 +455,22 @@ export class AmLyrics extends LitElement {
|
|
|
444
455
|
white-space: nowrap;
|
|
445
456
|
}
|
|
446
457
|
|
|
458
|
+
.lyrics-word.char-drag {
|
|
459
|
+
display: inline-block;
|
|
460
|
+
vertical-align: baseline;
|
|
461
|
+
white-space: nowrap;
|
|
462
|
+
}
|
|
463
|
+
|
|
447
464
|
.lyrics-word.char-rise.allow-break {
|
|
448
465
|
display: inline;
|
|
449
466
|
white-space: normal;
|
|
450
467
|
}
|
|
451
468
|
|
|
469
|
+
.lyrics-word.char-drag.allow-break {
|
|
470
|
+
display: inline;
|
|
471
|
+
white-space: normal;
|
|
472
|
+
}
|
|
473
|
+
|
|
452
474
|
.lyrics-syllable-wrap {
|
|
453
475
|
display: inline;
|
|
454
476
|
}
|
|
@@ -503,44 +525,30 @@ export class AmLyrics extends LitElement {
|
|
|
503
525
|
.lyrics-line.active:not(.lyrics-gap)
|
|
504
526
|
.lyrics-syllable.pre-highlight.no-chars {
|
|
505
527
|
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%;
|
|
528
|
+
background-image: linear-gradient(
|
|
529
|
+
90deg,
|
|
530
|
+
var(--lyplus-text-primary, #fff) 0%,
|
|
531
|
+
var(--lyplus-text-primary, #fff)
|
|
532
|
+
calc(100% - var(--wipe-gradient-width, 0.75em)),
|
|
533
|
+
#0000 100%
|
|
534
|
+
);
|
|
535
|
+
background-size: 0% 100%;
|
|
536
|
+
background-position: left;
|
|
524
537
|
}
|
|
525
538
|
|
|
526
539
|
.lyrics-line.active:not(.lyrics-gap) .lyrics-syllable.highlight.rtl-text,
|
|
527
540
|
.lyrics-line.active:not(.lyrics-gap)
|
|
528
541
|
.lyrics-syllable.pre-highlight.rtl-text {
|
|
529
542
|
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%;
|
|
543
|
+
background-image: linear-gradient(
|
|
544
|
+
-90deg,
|
|
545
|
+
var(--lyplus-text-primary) 0%,
|
|
546
|
+
var(--lyplus-text-primary)
|
|
547
|
+
calc(100% - var(--wipe-gradient-width, 0.75em)),
|
|
548
|
+
transparent 100%
|
|
549
|
+
);
|
|
550
|
+
background-size: 0% 100%;
|
|
551
|
+
background-position: right 0%;
|
|
544
552
|
}
|
|
545
553
|
|
|
546
554
|
/* Background vocals: muted gray wipe instead of white.
|
|
@@ -557,18 +565,13 @@ export class AmLyrics extends LitElement {
|
|
|
557
565
|
.lyrics-line.pre-active
|
|
558
566
|
.background-vocal-container
|
|
559
567
|
.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
|
-
);
|
|
568
|
+
background-image: linear-gradient(
|
|
569
|
+
90deg,
|
|
570
|
+
color-mix(in srgb, var(--lyplus-text-primary, #fff) 50%, #888888) 0%,
|
|
571
|
+
color-mix(in srgb, var(--lyplus-text-primary, #fff) 50%, #888888)
|
|
572
|
+
calc(100% - var(--wipe-gradient-width, 0.75em)),
|
|
573
|
+
#0000 100%
|
|
574
|
+
);
|
|
572
575
|
}
|
|
573
576
|
|
|
574
577
|
.lyrics-line.active
|
|
@@ -583,17 +586,13 @@ export class AmLyrics extends LitElement {
|
|
|
583
586
|
.lyrics-line.pre-active
|
|
584
587
|
.background-vocal-container
|
|
585
588
|
.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
|
-
);
|
|
589
|
+
background-image: linear-gradient(
|
|
590
|
+
-90deg,
|
|
591
|
+
color-mix(in srgb, var(--lyplus-text-primary) 50%, #888888) 0%,
|
|
592
|
+
color-mix(in srgb, var(--lyplus-text-primary) 50%, #888888)
|
|
593
|
+
calc(100% - var(--wipe-gradient-width, 0.75em)),
|
|
594
|
+
transparent 100%
|
|
595
|
+
);
|
|
597
596
|
}
|
|
598
597
|
|
|
599
598
|
/* Non-growable words float up with a gentle curve */
|
|
@@ -617,6 +616,10 @@ export class AmLyrics extends LitElement {
|
|
|
617
616
|
transform: translate3d(0, var(--char-rise-y, -1.12px), 0);
|
|
618
617
|
}
|
|
619
618
|
|
|
619
|
+
.lyrics-word.char-drag .lyrics-syllable.cleanup .char {
|
|
620
|
+
transform: translate3d(0, var(--char-rise-y, -1.12px), 0);
|
|
621
|
+
}
|
|
622
|
+
|
|
620
623
|
.lyrics-line.persist-highlight
|
|
621
624
|
.lyrics-word.growable
|
|
622
625
|
.lyrics-syllable.finished
|
|
@@ -624,6 +627,10 @@ export class AmLyrics extends LitElement {
|
|
|
624
627
|
.lyrics-line.persist-highlight
|
|
625
628
|
.lyrics-word.char-rise
|
|
626
629
|
.lyrics-syllable.finished
|
|
630
|
+
.char,
|
|
631
|
+
.lyrics-line.persist-highlight
|
|
632
|
+
.lyrics-word.char-drag
|
|
633
|
+
.lyrics-syllable.finished
|
|
627
634
|
.char {
|
|
628
635
|
transform: translate3d(0, var(--char-rise-y, -1.12px), 0);
|
|
629
636
|
}
|
|
@@ -734,70 +741,56 @@ export class AmLyrics extends LitElement {
|
|
|
734
741
|
transform 0.7s ease;
|
|
735
742
|
}
|
|
736
743
|
|
|
744
|
+
.lyrics-word.char-drag span.char {
|
|
745
|
+
transition: color 0.18s;
|
|
746
|
+
}
|
|
747
|
+
|
|
737
748
|
/* Active char spans: structural only, wipe animation sets gradient */
|
|
738
749
|
.lyrics-line.active .lyrics-syllable span.char {
|
|
739
750
|
background-clip: text;
|
|
740
751
|
-webkit-background-clip: text;
|
|
741
752
|
background-repeat: no-repeat;
|
|
742
|
-
background-image:
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
var(--
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
var(--lyplus-text-primary, #fff) 100%,
|
|
752
|
-
#0000 100%
|
|
753
|
-
);
|
|
754
|
-
background-size:
|
|
755
|
-
0.5em 100%,
|
|
756
|
-
0% 100%;
|
|
757
|
-
background-position:
|
|
758
|
-
-0.5em 0%,
|
|
759
|
-
-0.25em 0%;
|
|
753
|
+
background-image: linear-gradient(
|
|
754
|
+
90deg,
|
|
755
|
+
var(--lyplus-text-primary, #fff) 0%,
|
|
756
|
+
var(--lyplus-text-primary, #fff)
|
|
757
|
+
calc(100% - var(--wipe-gradient-width, 0.75em)),
|
|
758
|
+
#0000 100%
|
|
759
|
+
);
|
|
760
|
+
background-size: 0% 100%;
|
|
761
|
+
background-position: left;
|
|
760
762
|
transition:
|
|
761
763
|
transform 0.7s ease,
|
|
762
764
|
color 0.18s;
|
|
763
765
|
}
|
|
764
766
|
|
|
765
767
|
.lyrics-line.active .lyrics-syllable span.char.highlight {
|
|
766
|
-
background-image:
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
background-image:
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
var(--
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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%;
|
|
768
|
+
background-image: linear-gradient(
|
|
769
|
+
-90deg,
|
|
770
|
+
var(--lyplus-text-primary, #fff) 0%,
|
|
771
|
+
var(--lyplus-text-primary, #fff)
|
|
772
|
+
calc(100% - var(--wipe-gradient-width, 0.75em)),
|
|
773
|
+
#0000 100%
|
|
774
|
+
);
|
|
775
|
+
background-size: 0% 100%;
|
|
776
|
+
background-position: right 0%;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.lyrics-line.active .lyrics-syllable span.char.pre-wipe-lead {
|
|
780
|
+
animation-name: pre-wipe-word-char;
|
|
781
|
+
animation-duration: var(--pre-wipe-duration);
|
|
782
|
+
animation-delay: var(--pre-wipe-delay);
|
|
783
|
+
animation-timing-function: linear;
|
|
784
|
+
animation-fill-mode: forwards;
|
|
785
|
+
background-image: linear-gradient(
|
|
786
|
+
90deg,
|
|
787
|
+
var(--lyplus-text-primary, #fff) 0%,
|
|
788
|
+
var(--lyplus-text-primary, #fff)
|
|
789
|
+
calc(100% - var(--wipe-gradient-width, 0.75em)),
|
|
790
|
+
#0000 100%
|
|
791
|
+
);
|
|
792
|
+
background-size: 0% 100%;
|
|
793
|
+
background-position: var(--char-wipe-position, left) 0%;
|
|
801
794
|
}
|
|
802
795
|
|
|
803
796
|
/* ==========================================================================
|
|
@@ -874,13 +867,7 @@ export class AmLyrics extends LitElement {
|
|
|
874
867
|
color: var(--lyplus-text-primary) !important;
|
|
875
868
|
}
|
|
876
869
|
|
|
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 {
|
|
870
|
+
.lyrics-line.active .lyrics-syllable.line-synced span.char {
|
|
884
871
|
background-image: none !important;
|
|
885
872
|
background-color: var(--lyplus-text-primary) !important;
|
|
886
873
|
transition: background-color 120ms ease-out !important;
|
|
@@ -1053,6 +1040,49 @@ export class AmLyrics extends LitElement {
|
|
|
1053
1040
|
gap: 4px;
|
|
1054
1041
|
}
|
|
1055
1042
|
|
|
1043
|
+
.source-switch-btn {
|
|
1044
|
+
position: relative;
|
|
1045
|
+
display: inline-flex;
|
|
1046
|
+
align-items: center;
|
|
1047
|
+
padding: 2px 8px;
|
|
1048
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
1049
|
+
min-height: 28px;
|
|
1050
|
+
background: transparent;
|
|
1051
|
+
border-radius: 6px;
|
|
1052
|
+
color: #aaa;
|
|
1053
|
+
cursor: pointer;
|
|
1054
|
+
font-family: inherit;
|
|
1055
|
+
font-size: 11px;
|
|
1056
|
+
transition:
|
|
1057
|
+
color 0.2s ease,
|
|
1058
|
+
border-color 0.2s ease,
|
|
1059
|
+
background-color 0.2s ease,
|
|
1060
|
+
transform 0.12s ease;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
.source-switch-btn::before {
|
|
1064
|
+
content: '';
|
|
1065
|
+
position: absolute;
|
|
1066
|
+
inset: -6px;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
.source-switch-btn:active:not(:disabled) {
|
|
1070
|
+
transform: scale(0.96);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
.source-switch-btn:disabled {
|
|
1074
|
+
cursor: default;
|
|
1075
|
+
opacity: 0.7;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
.source-switch-svg {
|
|
1079
|
+
margin-right: 4px;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
.source-switch-svg.is-loading {
|
|
1083
|
+
animation: source-switch-spin 1s linear infinite;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1056
1086
|
.control-button {
|
|
1057
1087
|
background: transparent;
|
|
1058
1088
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
@@ -1061,7 +1091,10 @@ export class AmLyrics extends LitElement {
|
|
|
1061
1091
|
font-size: 0.8em;
|
|
1062
1092
|
color: rgba(255, 255, 255, 0.6);
|
|
1063
1093
|
cursor: pointer;
|
|
1064
|
-
transition:
|
|
1094
|
+
transition:
|
|
1095
|
+
color 0.2s,
|
|
1096
|
+
border-color 0.2s,
|
|
1097
|
+
background-color 0.2s;
|
|
1065
1098
|
font-weight: normal;
|
|
1066
1099
|
}
|
|
1067
1100
|
|
|
@@ -1199,137 +1232,125 @@ export class AmLyrics extends LitElement {
|
|
|
1199
1232
|
KEYFRAME ANIMATIONS
|
|
1200
1233
|
========================================================================== */
|
|
1201
1234
|
|
|
1235
|
+
@keyframes source-switch-spin {
|
|
1236
|
+
to {
|
|
1237
|
+
transform: rotate(360deg);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1202
1241
|
/* Wipe animation for syllables */
|
|
1203
1242
|
@keyframes wipe {
|
|
1204
1243
|
from {
|
|
1205
|
-
background-size:
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1244
|
+
background-size: 0% 100%;
|
|
1245
|
+
background-position: left;
|
|
1246
|
+
}
|
|
1247
|
+
to {
|
|
1248
|
+
background-size: calc(100% + var(--wipe-gradient-width, 0.75em)) 100%;
|
|
1249
|
+
background-position: left;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
@keyframes wipe-from-pre {
|
|
1254
|
+
from {
|
|
1255
|
+
background-size: var(--wipe-gradient-width, 0.75em) 100%;
|
|
1256
|
+
background-position: left;
|
|
1211
1257
|
}
|
|
1212
1258
|
to {
|
|
1213
|
-
background-size:
|
|
1214
|
-
|
|
1215
|
-
100% 100%;
|
|
1216
|
-
background-position:
|
|
1217
|
-
calc(100% + 0.375em) 0%,
|
|
1218
|
-
left;
|
|
1259
|
+
background-size: calc(100% + var(--wipe-gradient-width, 0.75em)) 100%;
|
|
1260
|
+
background-position: left;
|
|
1219
1261
|
}
|
|
1220
1262
|
}
|
|
1221
1263
|
|
|
1222
1264
|
@keyframes start-wipe {
|
|
1223
1265
|
0% {
|
|
1224
|
-
background-size:
|
|
1225
|
-
|
|
1226
|
-
0% 100%;
|
|
1227
|
-
background-position:
|
|
1228
|
-
-0.75em 0%,
|
|
1229
|
-
-0.375em 0%;
|
|
1266
|
+
background-size: 0% 100%;
|
|
1267
|
+
background-position: left;
|
|
1230
1268
|
}
|
|
1231
1269
|
100% {
|
|
1232
|
-
background-size:
|
|
1233
|
-
|
|
1234
|
-
100% 100%;
|
|
1235
|
-
background-position:
|
|
1236
|
-
calc(100% + 0.375em) 0%,
|
|
1237
|
-
left;
|
|
1270
|
+
background-size: calc(100% + var(--wipe-gradient-width, 0.75em)) 100%;
|
|
1271
|
+
background-position: left;
|
|
1238
1272
|
}
|
|
1239
1273
|
}
|
|
1240
1274
|
|
|
1241
1275
|
@keyframes wipe-rtl {
|
|
1242
1276
|
from {
|
|
1243
|
-
background-size:
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1277
|
+
background-size: 0% 100%;
|
|
1278
|
+
background-position: right 0%;
|
|
1279
|
+
}
|
|
1280
|
+
to {
|
|
1281
|
+
background-size: calc(100% + var(--wipe-gradient-width, 0.75em)) 100%;
|
|
1282
|
+
background-position: right 0%;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
@keyframes wipe-from-pre-rtl {
|
|
1287
|
+
from {
|
|
1288
|
+
background-size: var(--wipe-gradient-width, 0.75em) 100%;
|
|
1289
|
+
background-position: right 0%;
|
|
1249
1290
|
}
|
|
1250
1291
|
to {
|
|
1251
|
-
background-size:
|
|
1252
|
-
|
|
1253
|
-
100% 100%;
|
|
1254
|
-
background-position:
|
|
1255
|
-
-0.75em 0%,
|
|
1256
|
-
right 0%;
|
|
1292
|
+
background-size: calc(100% + var(--wipe-gradient-width, 0.75em)) 100%;
|
|
1293
|
+
background-position: right 0%;
|
|
1257
1294
|
}
|
|
1258
1295
|
}
|
|
1259
1296
|
|
|
1260
1297
|
@keyframes start-wipe-rtl {
|
|
1261
1298
|
0% {
|
|
1262
|
-
background-size:
|
|
1263
|
-
|
|
1264
|
-
0% 100%;
|
|
1265
|
-
background-position:
|
|
1266
|
-
calc(100% + 0.75em) 0%,
|
|
1267
|
-
calc(100% + 0.5em) 0%;
|
|
1299
|
+
background-size: 0% 100%;
|
|
1300
|
+
background-position: right 0%;
|
|
1268
1301
|
}
|
|
1269
1302
|
100% {
|
|
1270
|
-
background-size:
|
|
1271
|
-
|
|
1272
|
-
100% 100%;
|
|
1273
|
-
background-position:
|
|
1274
|
-
-0.75em 0%,
|
|
1275
|
-
right 0%;
|
|
1303
|
+
background-size: calc(100% + var(--wipe-gradient-width, 0.75em)) 100%;
|
|
1304
|
+
background-position: right 0%;
|
|
1276
1305
|
}
|
|
1277
1306
|
}
|
|
1278
1307
|
|
|
1279
1308
|
@keyframes pre-wipe-universal {
|
|
1280
1309
|
from {
|
|
1281
|
-
background-size:
|
|
1282
|
-
|
|
1283
|
-
0% 100%;
|
|
1284
|
-
background-position:
|
|
1285
|
-
-0.75em 0%,
|
|
1286
|
-
left;
|
|
1310
|
+
background-size: 0% 100%;
|
|
1311
|
+
background-position: left;
|
|
1287
1312
|
}
|
|
1288
1313
|
to {
|
|
1289
|
-
background-size:
|
|
1290
|
-
|
|
1291
|
-
0% 100%;
|
|
1292
|
-
background-position:
|
|
1293
|
-
-0.375em 0%,
|
|
1294
|
-
left;
|
|
1314
|
+
background-size: var(--wipe-gradient-width, 0.75em) 100%;
|
|
1315
|
+
background-position: left;
|
|
1295
1316
|
}
|
|
1296
1317
|
}
|
|
1297
1318
|
|
|
1298
1319
|
@keyframes pre-wipe-universal-rtl {
|
|
1299
1320
|
from {
|
|
1300
|
-
background-size:
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1321
|
+
background-size: 0% 100%;
|
|
1322
|
+
background-position: right 0%;
|
|
1323
|
+
}
|
|
1324
|
+
to {
|
|
1325
|
+
background-size: var(--wipe-gradient-width, 0.75em) 100%;
|
|
1326
|
+
background-position: right 0%;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
@keyframes pre-wipe-word-char {
|
|
1331
|
+
from {
|
|
1332
|
+
background-size: 0px 100%;
|
|
1333
|
+
background-position: var(--char-wipe-position, left) 0%;
|
|
1306
1334
|
}
|
|
1307
1335
|
to {
|
|
1308
|
-
background-size:
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
background-position:
|
|
1312
|
-
calc(100% + 0.375em) 0%,
|
|
1313
|
-
right 0%;
|
|
1336
|
+
background-size: var(--pre-wipe-word-size, var(--wipe-gradient-width))
|
|
1337
|
+
100%;
|
|
1338
|
+
background-position: var(--char-wipe-position, left) 0%;
|
|
1314
1339
|
}
|
|
1315
1340
|
}
|
|
1316
1341
|
|
|
1317
|
-
@keyframes
|
|
1342
|
+
@keyframes wipe-word-from-pre {
|
|
1318
1343
|
from {
|
|
1319
|
-
background-size:
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
background-position:
|
|
1323
|
-
-0.75em 0%,
|
|
1324
|
-
left;
|
|
1344
|
+
background-size: var(--pre-wipe-word-size, var(--wipe-gradient-width))
|
|
1345
|
+
100%;
|
|
1346
|
+
background-position: var(--char-wipe-position, left) 0%;
|
|
1325
1347
|
}
|
|
1326
1348
|
to {
|
|
1327
|
-
background-size:
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
left;
|
|
1349
|
+
background-size: calc(
|
|
1350
|
+
var(--word-wipe-width, 100%) + var(--wipe-gradient-width, 0.75em)
|
|
1351
|
+
)
|
|
1352
|
+
100%;
|
|
1353
|
+
background-position: var(--char-wipe-position, left) 0%;
|
|
1333
1354
|
}
|
|
1334
1355
|
}
|
|
1335
1356
|
|
|
@@ -1423,6 +1444,15 @@ export class AmLyrics extends LitElement {
|
|
|
1423
1444
|
}
|
|
1424
1445
|
}
|
|
1425
1446
|
|
|
1447
|
+
@keyframes drag-char {
|
|
1448
|
+
0% {
|
|
1449
|
+
transform: translate3d(0, 0, 0);
|
|
1450
|
+
}
|
|
1451
|
+
100% {
|
|
1452
|
+
transform: translate3d(0, var(--char-rise-y, -1.12px), 0);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1426
1456
|
@keyframes grow-static {
|
|
1427
1457
|
0%,
|
|
1428
1458
|
100% {
|
|
@@ -1668,6 +1698,7 @@ export class AmLyrics extends LitElement {
|
|
|
1668
1698
|
this.preActiveLineElements = [];
|
|
1669
1699
|
this.positionedLineElements = [];
|
|
1670
1700
|
this.activeGapLineElements = [];
|
|
1701
|
+
this.clearBackgroundExpandedLine();
|
|
1671
1702
|
|
|
1672
1703
|
// Stop all running animations and clear highlights immediately
|
|
1673
1704
|
if (this.lyricsContainer) {
|
|
@@ -1740,10 +1771,7 @@ export class AmLyrics extends LitElement {
|
|
|
1740
1771
|
(switchBtn as HTMLButtonElement).disabled = this.isFetchingAlternatives;
|
|
1741
1772
|
}
|
|
1742
1773
|
if (svgEl) {
|
|
1743
|
-
svgEl.
|
|
1744
|
-
'style',
|
|
1745
|
-
`margin-right: 4px; ${this.isFetchingAlternatives ? 'animation: spin 1s linear infinite;' : ''}`,
|
|
1746
|
-
);
|
|
1774
|
+
svgEl.classList.toggle('is-loading', this.isFetchingAlternatives);
|
|
1747
1775
|
}
|
|
1748
1776
|
if (labelEl) {
|
|
1749
1777
|
labelEl.textContent = this.isFetchingAlternatives
|
|
@@ -1806,6 +1834,7 @@ export class AmLyrics extends LitElement {
|
|
|
1806
1834
|
groupGrowable: boolean[];
|
|
1807
1835
|
groupGlowing: boolean[];
|
|
1808
1836
|
groupCharRise: boolean[];
|
|
1837
|
+
groupCharDrag: boolean[];
|
|
1809
1838
|
vwFullText: string[];
|
|
1810
1839
|
vwFullDuration: number[];
|
|
1811
1840
|
vwCharOffset: number[];
|
|
@@ -1821,6 +1850,8 @@ export class AmLyrics extends LitElement {
|
|
|
1821
1850
|
|
|
1822
1851
|
private lastPrimaryActiveLine: HTMLElement | null = null;
|
|
1823
1852
|
|
|
1853
|
+
private backgroundExpandedLine: HTMLElement | null = null;
|
|
1854
|
+
|
|
1824
1855
|
// Scroll animation state
|
|
1825
1856
|
private scrollAnimationState: {
|
|
1826
1857
|
isAnimating: boolean;
|
|
@@ -2076,6 +2107,7 @@ export class AmLyrics extends LitElement {
|
|
|
2076
2107
|
this.preActiveLineElements = [];
|
|
2077
2108
|
this.positionedLineElements = [];
|
|
2078
2109
|
this.activeGapLineElements = [];
|
|
2110
|
+
this.clearBackgroundExpandedLine();
|
|
2079
2111
|
|
|
2080
2112
|
if (this.lyricsContainer) {
|
|
2081
2113
|
this.isProgrammaticScroll = true;
|
|
@@ -2140,20 +2172,26 @@ export class AmLyrics extends LitElement {
|
|
|
2140
2172
|
return 30;
|
|
2141
2173
|
}
|
|
2142
2174
|
|
|
2175
|
+
private static getDisplaySourceLabel(sourceLabel: string): string {
|
|
2176
|
+
return sourceLabel.toLowerCase().includes('lyricsplus')
|
|
2177
|
+
? 'QQ'
|
|
2178
|
+
: sourceLabel;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
private static getSourceKey(sourceLabel: string | null | undefined): string {
|
|
2182
|
+
const lower = (sourceLabel || '').trim().toLowerCase();
|
|
2183
|
+
if (!lower) return '';
|
|
2184
|
+
if (lower.includes('lyricsplus') || lower === 'qq') return 'qq';
|
|
2185
|
+
return lower.replace(/\s+/g, ' ');
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2143
2188
|
private static mergeAndSortSources(
|
|
2144
|
-
collectedSources:
|
|
2145
|
-
):
|
|
2146
|
-
const uniqueSourcesMap = new Map<
|
|
2147
|
-
string,
|
|
2148
|
-
{ lines: LyricsLine[]; source: string }
|
|
2149
|
-
>();
|
|
2189
|
+
collectedSources: YouLyPlusLyricsResult[],
|
|
2190
|
+
): YouLyPlusLyricsResult[] {
|
|
2191
|
+
const uniqueSourcesMap = new Map<string, YouLyPlusLyricsResult>();
|
|
2150
2192
|
|
|
2151
2193
|
for (const source of collectedSources) {
|
|
2152
|
-
const normalizedSource = source.source
|
|
2153
|
-
.toLowerCase()
|
|
2154
|
-
.includes('lyricsplus')
|
|
2155
|
-
? 'QQ'
|
|
2156
|
-
: source.source;
|
|
2194
|
+
const normalizedSource = AmLyrics.getDisplaySourceLabel(source.source);
|
|
2157
2195
|
|
|
2158
2196
|
if (!uniqueSourcesMap.has(normalizedSource)) {
|
|
2159
2197
|
uniqueSourcesMap.set(normalizedSource, {
|
|
@@ -2170,9 +2208,63 @@ export class AmLyrics extends LitElement {
|
|
|
2170
2208
|
);
|
|
2171
2209
|
}
|
|
2172
2210
|
|
|
2211
|
+
private findCurrentSourceIndex(
|
|
2212
|
+
sources = this.availableSources,
|
|
2213
|
+
sourceLabel = this.lyricsSource,
|
|
2214
|
+
lines = this.lyrics,
|
|
2215
|
+
): number {
|
|
2216
|
+
const identityIndex = sources.findIndex(source => source.lines === lines);
|
|
2217
|
+
if (identityIndex !== -1) return identityIndex;
|
|
2218
|
+
|
|
2219
|
+
const sourceKey = AmLyrics.getSourceKey(sourceLabel);
|
|
2220
|
+
if (!sourceKey) return -1;
|
|
2221
|
+
|
|
2222
|
+
return sources.findIndex(
|
|
2223
|
+
source => AmLyrics.getSourceKey(source.source) === sourceKey,
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
private static getNextSourceIndex(
|
|
2228
|
+
sources: YouLyPlusLyricsResult[],
|
|
2229
|
+
currentIndex: number,
|
|
2230
|
+
currentSourceLabel: string | null,
|
|
2231
|
+
currentLines: LyricsLine[] | undefined,
|
|
2232
|
+
): number {
|
|
2233
|
+
if (sources.length <= 1) return -1;
|
|
2234
|
+
|
|
2235
|
+
if (currentIndex !== -1) {
|
|
2236
|
+
return (currentIndex + 1) % sources.length;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
const currentKey = AmLyrics.getSourceKey(currentSourceLabel);
|
|
2240
|
+
const fallbackIndex = sources.findIndex(
|
|
2241
|
+
source =>
|
|
2242
|
+
source.lines !== currentLines &&
|
|
2243
|
+
AmLyrics.getSourceKey(source.source) !== currentKey,
|
|
2244
|
+
);
|
|
2245
|
+
|
|
2246
|
+
return fallbackIndex === -1 ? 0 : fallbackIndex;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
private async applySourceAtIndex(index: number) {
|
|
2250
|
+
const sourceResult = this.availableSources[index];
|
|
2251
|
+
if (!sourceResult) return;
|
|
2252
|
+
|
|
2253
|
+
this.currentSourceIndex = index;
|
|
2254
|
+
this.lyrics = sourceResult.lines;
|
|
2255
|
+
this.lyricsSource = sourceResult.source;
|
|
2256
|
+
if (sourceResult.songwriters) {
|
|
2257
|
+
this.songwriters = sourceResult.songwriters;
|
|
2258
|
+
}
|
|
2259
|
+
await this.onLyricsLoaded();
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2173
2262
|
private async switchSource() {
|
|
2174
2263
|
if (this.isFetchingAlternatives) return;
|
|
2175
2264
|
|
|
2265
|
+
const currentSourceLabel = this.lyricsSource;
|
|
2266
|
+
const currentLines = this.lyrics;
|
|
2267
|
+
|
|
2176
2268
|
if (!this.hasFetchedAllProviders) {
|
|
2177
2269
|
this.isFetchingAlternatives = true;
|
|
2178
2270
|
this._updateFooter();
|
|
@@ -2249,11 +2341,13 @@ export class AmLyrics extends LitElement {
|
|
|
2249
2341
|
...this.availableSources,
|
|
2250
2342
|
...newSources,
|
|
2251
2343
|
]);
|
|
2252
|
-
// Re-sync current index since sorting
|
|
2253
|
-
|
|
2254
|
-
|
|
2344
|
+
// Re-sync current index since sorting or label normalization can
|
|
2345
|
+
// shift the currently displayed source underneath the old index.
|
|
2346
|
+
this.currentSourceIndex = this.findCurrentSourceIndex(
|
|
2347
|
+
this.availableSources,
|
|
2348
|
+
currentSourceLabel,
|
|
2349
|
+
currentLines,
|
|
2255
2350
|
);
|
|
2256
|
-
if (this.currentSourceIndex === -1) this.currentSourceIndex = 0;
|
|
2257
2351
|
}
|
|
2258
2352
|
}
|
|
2259
2353
|
} finally {
|
|
@@ -2264,15 +2358,20 @@ export class AmLyrics extends LitElement {
|
|
|
2264
2358
|
}
|
|
2265
2359
|
|
|
2266
2360
|
if (this.availableSources.length > 1) {
|
|
2267
|
-
this.
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
this.
|
|
2361
|
+
const currentIndex = this.findCurrentSourceIndex(
|
|
2362
|
+
this.availableSources,
|
|
2363
|
+
currentSourceLabel,
|
|
2364
|
+
currentLines,
|
|
2365
|
+
);
|
|
2366
|
+
const nextIndex = AmLyrics.getNextSourceIndex(
|
|
2367
|
+
this.availableSources,
|
|
2368
|
+
currentIndex,
|
|
2369
|
+
currentSourceLabel,
|
|
2370
|
+
currentLines,
|
|
2371
|
+
);
|
|
2372
|
+
if (nextIndex !== -1) {
|
|
2373
|
+
await this.applySourceAtIndex(nextIndex);
|
|
2274
2374
|
}
|
|
2275
|
-
await this.onLyricsLoaded();
|
|
2276
2375
|
}
|
|
2277
2376
|
}
|
|
2278
2377
|
|
|
@@ -3538,7 +3637,11 @@ export class AmLyrics extends LitElement {
|
|
|
3538
3637
|
if (!newActiveLines.includes(lineIndex)) {
|
|
3539
3638
|
const lineElement = this._getLineElement(lineIndex);
|
|
3540
3639
|
if (lineElement) {
|
|
3541
|
-
if (
|
|
3640
|
+
if (
|
|
3641
|
+
isSeek ||
|
|
3642
|
+
this.isUserScrolling ||
|
|
3643
|
+
AmLyrics.isLineSyncedLine(this.lyrics?.[lineIndex])
|
|
3644
|
+
) {
|
|
3542
3645
|
AmLyrics.unfinishSyllables(lineElement);
|
|
3543
3646
|
} else {
|
|
3544
3647
|
AmLyrics.finishSyllablesUpToTime(lineElement, newTime);
|
|
@@ -3555,12 +3658,13 @@ export class AmLyrics extends LitElement {
|
|
|
3555
3658
|
}
|
|
3556
3659
|
}
|
|
3557
3660
|
|
|
3558
|
-
// Add 'active'
|
|
3661
|
+
// Add 'active' to newly active lines. Background expansion is driven
|
|
3662
|
+
// separately by the current scroll target.
|
|
3559
3663
|
for (const lineIndex of newActiveLines) {
|
|
3560
3664
|
if (!oldActiveLines.includes(lineIndex)) {
|
|
3561
3665
|
const lineElement = this._getLineElement(lineIndex);
|
|
3562
3666
|
if (lineElement) {
|
|
3563
|
-
lineElement.classList.add('active'
|
|
3667
|
+
lineElement.classList.add('active');
|
|
3564
3668
|
lineElement.classList.remove('pre-active');
|
|
3565
3669
|
const preIdx = this.preActiveLineElements.indexOf(lineElement);
|
|
3566
3670
|
if (preIdx !== -1) this.preActiveLineElements.splice(preIdx, 1);
|
|
@@ -3824,8 +3928,14 @@ export class AmLyrics extends LitElement {
|
|
|
3824
3928
|
const activeLines = this.findActiveLineIndices(this.currentTime);
|
|
3825
3929
|
for (const lineIndex of activeLines) {
|
|
3826
3930
|
const lineEl = this._getLineElement(lineIndex);
|
|
3827
|
-
if (lineEl) lineEl.classList.add('active'
|
|
3931
|
+
if (lineEl) lineEl.classList.add('active');
|
|
3828
3932
|
}
|
|
3933
|
+
const primaryActiveLine = this.getPrimaryActiveLineIndex(activeLines);
|
|
3934
|
+
this.setBackgroundExpandedLine(
|
|
3935
|
+
primaryActiveLine !== null
|
|
3936
|
+
? this._getLineElement(primaryActiveLine)
|
|
3937
|
+
: null,
|
|
3938
|
+
);
|
|
3829
3939
|
|
|
3830
3940
|
// Trigger a faux time-change so that updateSyllablesForLine fires
|
|
3831
3941
|
// to setup inline syllable CSS wipe animations for whatever the current time is
|
|
@@ -3872,6 +3982,7 @@ export class AmLyrics extends LitElement {
|
|
|
3872
3982
|
this.preActiveLineElements = [];
|
|
3873
3983
|
this.positionedLineElements = [];
|
|
3874
3984
|
this.activeGapLineElements = [];
|
|
3985
|
+
this.clearBackgroundExpandedLine();
|
|
3875
3986
|
this.setUserScrolling(false);
|
|
3876
3987
|
|
|
3877
3988
|
// Cancel any running animations
|
|
@@ -3943,6 +4054,7 @@ export class AmLyrics extends LitElement {
|
|
|
3943
4054
|
// Don't override it with a scroll back to the last lyric.
|
|
3944
4055
|
const footer = this.lyricsContainer.querySelector('.lyrics-footer');
|
|
3945
4056
|
if (footer?.classList.contains('active')) {
|
|
4057
|
+
this.setBackgroundExpandedLine(null);
|
|
3946
4058
|
return;
|
|
3947
4059
|
}
|
|
3948
4060
|
|
|
@@ -3994,10 +4106,17 @@ export class AmLyrics extends LitElement {
|
|
|
3994
4106
|
targetElement = this._getLineElement(targetLineIdx);
|
|
3995
4107
|
}
|
|
3996
4108
|
}
|
|
3997
|
-
if (!targetElement)
|
|
4109
|
+
if (!targetElement) {
|
|
4110
|
+
this.setBackgroundExpandedLine(null);
|
|
4111
|
+
return;
|
|
4112
|
+
}
|
|
4113
|
+
|
|
4114
|
+
const scrollDuration = scrollLookAheadMs;
|
|
4115
|
+
targetElement.style.setProperty('--scroll-duration', `${scrollDuration}ms`);
|
|
4116
|
+
this.setBackgroundExpandedLine(targetElement);
|
|
3998
4117
|
|
|
3999
|
-
// Unblur the upcoming target line early
|
|
4000
|
-
//
|
|
4118
|
+
// Unblur the upcoming target line early while the separate bg-expanded
|
|
4119
|
+
// class starts background vocal height/opacity in sync with scroll.
|
|
4001
4120
|
if (!targetElement.classList.contains('active')) {
|
|
4002
4121
|
targetElement.classList.add('pre-active');
|
|
4003
4122
|
if (!this.preActiveLineElements.includes(targetElement)) {
|
|
@@ -4005,8 +4124,6 @@ export class AmLyrics extends LitElement {
|
|
|
4005
4124
|
}
|
|
4006
4125
|
}
|
|
4007
4126
|
|
|
4008
|
-
const scrollDuration = scrollLookAheadMs;
|
|
4009
|
-
|
|
4010
4127
|
this.focusLine(targetElement, forceScroll, scrollDuration);
|
|
4011
4128
|
}
|
|
4012
4129
|
|
|
@@ -4096,6 +4213,7 @@ export class AmLyrics extends LitElement {
|
|
|
4096
4213
|
this.preActiveLineElements = [];
|
|
4097
4214
|
this.positionedLineElements = [];
|
|
4098
4215
|
this.activeGapLineElements = [];
|
|
4216
|
+
this.clearBackgroundExpandedLine();
|
|
4099
4217
|
this.visibilityObserver?.disconnect();
|
|
4100
4218
|
this.visibilityObserver = undefined;
|
|
4101
4219
|
}
|
|
@@ -4139,6 +4257,7 @@ export class AmLyrics extends LitElement {
|
|
|
4139
4257
|
const groupGrowable: boolean[] = new Array(wordGroups.length).fill(false);
|
|
4140
4258
|
const groupGlowing: boolean[] = new Array(wordGroups.length).fill(false);
|
|
4141
4259
|
const groupCharRise: boolean[] = new Array(wordGroups.length).fill(false);
|
|
4260
|
+
const groupCharDrag: boolean[] = new Array(wordGroups.length).fill(false);
|
|
4142
4261
|
const vwFullText: string[] = new Array(wordGroups.length).fill('');
|
|
4143
4262
|
const vwFullDuration: number[] = new Array(wordGroups.length).fill(0);
|
|
4144
4263
|
const vwCharOffset: number[] = new Array(wordGroups.length).fill(0);
|
|
@@ -4183,9 +4302,13 @@ export class AmLyrics extends LitElement {
|
|
|
4183
4302
|
line.isWordSynced === false || line.text.some(s => s.lineSynced);
|
|
4184
4303
|
let isGrowableVW = canAnimateByChar && wordLen > 0 && wordLen <= 7;
|
|
4185
4304
|
if (isGrowableVW) {
|
|
4186
|
-
if (wordLen
|
|
4305
|
+
if (wordLen <= 1) {
|
|
4187
4306
|
isGrowableVW =
|
|
4188
4307
|
combinedDuration >= 1050 && combinedDuration >= wordLen * 525;
|
|
4308
|
+
} else if (wordLen <= 3) {
|
|
4309
|
+
isGrowableVW =
|
|
4310
|
+
combinedDuration >=
|
|
4311
|
+
SHORT_WORD_GLOW_MIN_DURATION_MS + (wordLen - 2) * 140;
|
|
4189
4312
|
} else {
|
|
4190
4313
|
isGrowableVW =
|
|
4191
4314
|
combinedDuration >= 850 && combinedDuration >= wordLen * 190;
|
|
@@ -4194,6 +4317,11 @@ export class AmLyrics extends LitElement {
|
|
|
4194
4317
|
|
|
4195
4318
|
const hasCharRiseDuration =
|
|
4196
4319
|
combinedDuration >= Math.max(700, wordLen * 85);
|
|
4320
|
+
const hasTinyWordDragDuration =
|
|
4321
|
+
wordLen >= 2 &&
|
|
4322
|
+
wordLen <= 3 &&
|
|
4323
|
+
combinedDuration >=
|
|
4324
|
+
Math.max(SHORT_WORD_DRAG_MIN_DURATION_MS, wordLen * 150);
|
|
4197
4325
|
const hasLongShortWordDuration =
|
|
4198
4326
|
wordLen >= 4 && combinedDuration >= Math.max(1300, wordLen * 260);
|
|
4199
4327
|
const isCharRiseVW =
|
|
@@ -4202,6 +4330,11 @@ export class AmLyrics extends LitElement {
|
|
|
4202
4330
|
!isGrowableVW &&
|
|
4203
4331
|
((wordLen >= 8 && hasCharRiseDuration) ||
|
|
4204
4332
|
(wordLen < 8 && hasLongShortWordDuration));
|
|
4333
|
+
const isCharDragVW =
|
|
4334
|
+
canAnimateByChar &&
|
|
4335
|
+
!isLineSynced &&
|
|
4336
|
+
!isGrowableVW &&
|
|
4337
|
+
hasTinyWordDragDuration;
|
|
4205
4338
|
|
|
4206
4339
|
const isGlowingVW = isGrowableVW && !isLineSynced;
|
|
4207
4340
|
|
|
@@ -4210,6 +4343,7 @@ export class AmLyrics extends LitElement {
|
|
|
4210
4343
|
groupGrowable[gi] = isGrowableVW;
|
|
4211
4344
|
groupGlowing[gi] = isGlowingVW;
|
|
4212
4345
|
groupCharRise[gi] = isCharRiseVW;
|
|
4346
|
+
groupCharDrag[gi] = isCharDragVW;
|
|
4213
4347
|
vwFullText[gi] = combinedText;
|
|
4214
4348
|
vwFullDuration[gi] = combinedDuration;
|
|
4215
4349
|
vwCharOffset[gi] = charOff;
|
|
@@ -4227,6 +4361,7 @@ export class AmLyrics extends LitElement {
|
|
|
4227
4361
|
groupGrowable,
|
|
4228
4362
|
groupGlowing,
|
|
4229
4363
|
groupCharRise,
|
|
4364
|
+
groupCharDrag,
|
|
4230
4365
|
vwFullText,
|
|
4231
4366
|
vwFullDuration,
|
|
4232
4367
|
vwCharOffset,
|
|
@@ -4248,62 +4383,135 @@ export class AmLyrics extends LitElement {
|
|
|
4248
4383
|
|
|
4249
4384
|
const computedStyle = getComputedStyle(referenceSyllable);
|
|
4250
4385
|
const { font } = computedStyle; // Full font string
|
|
4251
|
-
const fontSize = parseFloat(computedStyle.fontSize);
|
|
4252
4386
|
|
|
4253
|
-
const charTimedWords =
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
charTimedWords.
|
|
4259
|
-
|
|
4387
|
+
const charTimedWords = Array.from(
|
|
4388
|
+
this.shadowRoot.querySelectorAll(
|
|
4389
|
+
'.lyrics-word.growable, .lyrics-word.char-rise, .lyrics-word.char-drag',
|
|
4390
|
+
),
|
|
4391
|
+
) as HTMLElement[];
|
|
4392
|
+
if (charTimedWords.length === 0) return;
|
|
4393
|
+
|
|
4394
|
+
const wordsByVirtualId = new Map<string, HTMLElement[]>();
|
|
4395
|
+
charTimedWords.forEach((wordSpan, index) => {
|
|
4396
|
+
const virtualWordId = wordSpan.dataset.virtualWordId || `word-${index}`;
|
|
4397
|
+
const words = wordsByVirtualId.get(virtualWordId);
|
|
4398
|
+
if (words) {
|
|
4399
|
+
words.push(wordSpan);
|
|
4400
|
+
} else {
|
|
4401
|
+
wordsByVirtualId.set(virtualWordId, [wordSpan]);
|
|
4402
|
+
}
|
|
4403
|
+
});
|
|
4260
4404
|
|
|
4261
|
-
|
|
4405
|
+
wordsByVirtualId.forEach(wordSpans => {
|
|
4262
4406
|
const syllables: HTMLElement[] = [];
|
|
4263
|
-
|
|
4264
|
-
const
|
|
4265
|
-
|
|
4407
|
+
wordSpans.forEach(wordSpan => {
|
|
4408
|
+
const syllableWraps = wordSpan.querySelectorAll(
|
|
4409
|
+
'.lyrics-syllable-wrap',
|
|
4410
|
+
);
|
|
4411
|
+
syllableWraps.forEach(wrap => {
|
|
4412
|
+
const syl = wrap.querySelector('.lyrics-syllable');
|
|
4413
|
+
if (syl) syllables.push(syl as HTMLElement);
|
|
4414
|
+
});
|
|
4266
4415
|
});
|
|
4267
4416
|
|
|
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;
|
|
4417
|
+
const charSpans = syllables.flatMap(syl => {
|
|
4418
|
+
const spans = Array.from(
|
|
4419
|
+
syl.querySelectorAll('.char'),
|
|
4420
|
+
) as HTMLElement[];
|
|
4421
|
+
const target = syl as any;
|
|
4422
|
+
target._cachedCharSpans = spans;
|
|
4423
|
+
return spans;
|
|
4424
|
+
});
|
|
4425
|
+
if (charSpans.length === 0) return;
|
|
4293
4426
|
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4427
|
+
wordSpans.forEach(wordSpan => {
|
|
4428
|
+
const target = wordSpan as any;
|
|
4429
|
+
target._cachedVirtualWordElements = wordSpans;
|
|
4430
|
+
target._cachedVirtualWordCharSpans = charSpans;
|
|
4431
|
+
});
|
|
4297
4432
|
|
|
4298
|
-
|
|
4299
|
-
|
|
4433
|
+
const syllableEntries = syllables.map(syl => {
|
|
4434
|
+
const spans = (syl as any)._cachedCharSpans as HTMLElement[];
|
|
4435
|
+
const charWidths = spans.map(span =>
|
|
4436
|
+
this._getTextWidth(span.textContent || '', font),
|
|
4437
|
+
);
|
|
4438
|
+
const totalWidth = charWidths.reduce((a, b) => a + b, 0);
|
|
4439
|
+
return {
|
|
4440
|
+
syl,
|
|
4441
|
+
spans,
|
|
4442
|
+
charWidths,
|
|
4443
|
+
totalWidth,
|
|
4444
|
+
start: parseFloat(syl.getAttribute('data-start-time') || ''),
|
|
4445
|
+
end: parseFloat(syl.getAttribute('data-end-time') || ''),
|
|
4446
|
+
};
|
|
4447
|
+
});
|
|
4448
|
+
const totalWordWidth = syllableEntries.reduce(
|
|
4449
|
+
(total, entry) => total + entry.totalWidth,
|
|
4450
|
+
0,
|
|
4451
|
+
);
|
|
4452
|
+
if (totalWordWidth <= 0) return;
|
|
4300
4453
|
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4454
|
+
const virtualWordStart = Math.min(
|
|
4455
|
+
...syllableEntries
|
|
4456
|
+
.map(entry => entry.start)
|
|
4457
|
+
.filter(start => Number.isFinite(start)),
|
|
4458
|
+
);
|
|
4459
|
+
const virtualWordEnd = Math.max(
|
|
4460
|
+
...syllableEntries
|
|
4461
|
+
.map(entry => entry.end)
|
|
4462
|
+
.filter(end => Number.isFinite(end)),
|
|
4463
|
+
);
|
|
4464
|
+
const virtualWordDuration = virtualWordEnd - virtualWordStart;
|
|
4465
|
+
const hasTimedSyllables =
|
|
4466
|
+
Number.isFinite(virtualWordStart) &&
|
|
4467
|
+
Number.isFinite(virtualWordEnd) &&
|
|
4468
|
+
virtualWordDuration > 0;
|
|
4469
|
+
|
|
4470
|
+
let cumulativeCharWidth = 0;
|
|
4471
|
+
syllableEntries.forEach(entry => {
|
|
4472
|
+
let cumulativeSyllableWidth = 0;
|
|
4473
|
+
const syllableDuration = entry.end - entry.start;
|
|
4474
|
+
const useSyllableTiming =
|
|
4475
|
+
hasTimedSyllables &&
|
|
4476
|
+
Number.isFinite(entry.start) &&
|
|
4477
|
+
Number.isFinite(entry.end) &&
|
|
4478
|
+
syllableDuration > 0 &&
|
|
4479
|
+
entry.totalWidth > 0;
|
|
4480
|
+
|
|
4481
|
+
entry.spans.forEach((span, index) => {
|
|
4482
|
+
const charWidth = entry.charWidths[index];
|
|
4483
|
+
let startPercent = cumulativeCharWidth / totalWordWidth;
|
|
4484
|
+
let durationPercent = charWidth / totalWordWidth;
|
|
4485
|
+
if (useSyllableTiming) {
|
|
4486
|
+
const charStartMs =
|
|
4487
|
+
entry.start -
|
|
4488
|
+
virtualWordStart +
|
|
4489
|
+
(cumulativeSyllableWidth / entry.totalWidth) * syllableDuration;
|
|
4490
|
+
const charDurationMs =
|
|
4491
|
+
(charWidth / entry.totalWidth) * syllableDuration;
|
|
4492
|
+
startPercent = AmLyrics.clamp(
|
|
4493
|
+
charStartMs / virtualWordDuration,
|
|
4494
|
+
0,
|
|
4495
|
+
1,
|
|
4496
|
+
);
|
|
4497
|
+
durationPercent = AmLyrics.clamp(
|
|
4498
|
+
charDurationMs / virtualWordDuration,
|
|
4499
|
+
0,
|
|
4500
|
+
1,
|
|
4501
|
+
);
|
|
4304
4502
|
}
|
|
4503
|
+
const target = span;
|
|
4504
|
+
|
|
4505
|
+
target.dataset.wipeStart = startPercent.toFixed(4);
|
|
4506
|
+
target.dataset.wipeDuration = durationPercent.toFixed(4);
|
|
4507
|
+
target.style.setProperty('--word-wipe-width', `${totalWordWidth}px`);
|
|
4508
|
+
target.style.setProperty(
|
|
4509
|
+
'--char-wipe-position',
|
|
4510
|
+
`${-cumulativeCharWidth}px`,
|
|
4511
|
+
);
|
|
4305
4512
|
|
|
4306
4513
|
cumulativeCharWidth += charWidth;
|
|
4514
|
+
cumulativeSyllableWidth += charWidth;
|
|
4307
4515
|
});
|
|
4308
4516
|
});
|
|
4309
4517
|
});
|
|
@@ -4313,6 +4521,37 @@ export class AmLyrics extends LitElement {
|
|
|
4313
4521
|
return a.length === b.length && a.every((val, i) => val === b[i]);
|
|
4314
4522
|
}
|
|
4315
4523
|
|
|
4524
|
+
private static isLineSyncedLine(line: LyricsLine | undefined): boolean {
|
|
4525
|
+
if (!line) return false;
|
|
4526
|
+
return line.isWordSynced === false || line.text.some(s => s.lineSynced);
|
|
4527
|
+
}
|
|
4528
|
+
|
|
4529
|
+
private getLineHighlightEndTime(index: number): number {
|
|
4530
|
+
if (!this.lyrics) return 0;
|
|
4531
|
+
const line = this.lyrics[index];
|
|
4532
|
+
if (!line) return 0;
|
|
4533
|
+
|
|
4534
|
+
const rawEnd = Math.max(line.endtime, line.timestamp);
|
|
4535
|
+
|
|
4536
|
+
const nextLine = this.lyrics[index + 1];
|
|
4537
|
+
if (!nextLine || nextLine.timestamp <= line.timestamp) {
|
|
4538
|
+
return rawEnd > line.timestamp ? rawEnd + 200 : rawEnd;
|
|
4539
|
+
}
|
|
4540
|
+
|
|
4541
|
+
if (rawEnd > line.timestamp) {
|
|
4542
|
+
if (nextLine.timestamp < rawEnd) {
|
|
4543
|
+
return rawEnd;
|
|
4544
|
+
}
|
|
4545
|
+
|
|
4546
|
+
const gapToNext = nextLine.timestamp - rawEnd;
|
|
4547
|
+
if (gapToNext >= INSTRUMENTAL_THRESHOLD_MS) {
|
|
4548
|
+
return rawEnd;
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4551
|
+
|
|
4552
|
+
return nextLine.timestamp;
|
|
4553
|
+
}
|
|
4554
|
+
|
|
4316
4555
|
private static getLineIndexFromElement(
|
|
4317
4556
|
lineElement: HTMLElement | null,
|
|
4318
4557
|
): number | null {
|
|
@@ -4350,6 +4589,31 @@ export class AmLyrics extends LitElement {
|
|
|
4350
4589
|
this.preActiveLineElements = keptLines;
|
|
4351
4590
|
}
|
|
4352
4591
|
|
|
4592
|
+
private setBackgroundExpandedLine(lineElement: HTMLElement | null): void {
|
|
4593
|
+
const target =
|
|
4594
|
+
lineElement &&
|
|
4595
|
+
!lineElement.classList.contains('lyrics-gap') &&
|
|
4596
|
+
lineElement.querySelector('.background-vocal-container')
|
|
4597
|
+
? lineElement
|
|
4598
|
+
: null;
|
|
4599
|
+
|
|
4600
|
+
if (this.backgroundExpandedLine === target) {
|
|
4601
|
+
if (target && !target.classList.contains('bg-expanded')) {
|
|
4602
|
+
target.classList.add('bg-expanded');
|
|
4603
|
+
}
|
|
4604
|
+
return;
|
|
4605
|
+
}
|
|
4606
|
+
|
|
4607
|
+
this.backgroundExpandedLine?.classList.remove('bg-expanded');
|
|
4608
|
+
this.backgroundExpandedLine = target;
|
|
4609
|
+
target?.classList.add('bg-expanded');
|
|
4610
|
+
}
|
|
4611
|
+
|
|
4612
|
+
private clearBackgroundExpandedLine(): void {
|
|
4613
|
+
this.backgroundExpandedLine?.classList.remove('bg-expanded');
|
|
4614
|
+
this.backgroundExpandedLine = null;
|
|
4615
|
+
}
|
|
4616
|
+
|
|
4353
4617
|
private getPrimaryActiveLineIndex(activeIndices: number[]): number | null {
|
|
4354
4618
|
if (activeIndices.length === 0) return null;
|
|
4355
4619
|
|
|
@@ -4593,9 +4857,10 @@ export class AmLyrics extends LitElement {
|
|
|
4593
4857
|
|
|
4594
4858
|
for (let i = 0; i < this.lyrics.length; i += 1) {
|
|
4595
4859
|
const line = this.lyrics[i];
|
|
4860
|
+
const highlightEndTime = this.getLineHighlightEndTime(i);
|
|
4596
4861
|
|
|
4597
4862
|
if (line.timestamp > time) break;
|
|
4598
|
-
if (time >= line.timestamp && time <
|
|
4863
|
+
if (time >= line.timestamp && time < highlightEndTime) {
|
|
4599
4864
|
activeLines.push(i);
|
|
4600
4865
|
}
|
|
4601
4866
|
}
|
|
@@ -4862,6 +5127,7 @@ export class AmLyrics extends LitElement {
|
|
|
4862
5127
|
this.lastPrimaryActiveLine = null;
|
|
4863
5128
|
this.activeLineIds.clear();
|
|
4864
5129
|
this.animatingLines = [];
|
|
5130
|
+
this.setBackgroundExpandedLine(null);
|
|
4865
5131
|
|
|
4866
5132
|
// Find the clicked line element and scroll to it with forceScroll (like YouLyPlus)
|
|
4867
5133
|
// Timestamps are already in milliseconds — match the data-start-time attribute directly
|
|
@@ -4884,6 +5150,7 @@ export class AmLyrics extends LitElement {
|
|
|
4884
5150
|
}, 800);
|
|
4885
5151
|
|
|
4886
5152
|
this.scrollToActiveLineYouLy(clickedLineElement, true);
|
|
5153
|
+
this.setBackgroundExpandedLine(clickedLineElement);
|
|
4887
5154
|
}
|
|
4888
5155
|
|
|
4889
5156
|
const event = new CustomEvent('line-click', {
|
|
@@ -5322,44 +5589,417 @@ export class AmLyrics extends LitElement {
|
|
|
5322
5589
|
|
|
5323
5590
|
/**
|
|
5324
5591
|
* Update syllable highlight animation - apply CSS wipe animation
|
|
5325
|
-
* (Exact copy from YouLyPlus _updateSyllableAnimation)
|
|
5326
5592
|
*/
|
|
5327
|
-
private static
|
|
5593
|
+
private static clamp(value: number, min: number, max: number): number {
|
|
5594
|
+
return Math.min(max, Math.max(min, value));
|
|
5595
|
+
}
|
|
5596
|
+
|
|
5597
|
+
private static getVisibleCharacterCount(element: HTMLElement): number {
|
|
5598
|
+
const attrLength = parseFloat(
|
|
5599
|
+
element.getAttribute('data-word-length') || '',
|
|
5600
|
+
);
|
|
5601
|
+
if (Number.isFinite(attrLength) && attrLength > 0) return attrLength;
|
|
5602
|
+
return (element.textContent || '').replace(/\s/g, '').length;
|
|
5603
|
+
}
|
|
5604
|
+
|
|
5605
|
+
private static getLongWordWipeScale(charCount: number): number {
|
|
5606
|
+
if (charCount <= 6) return 1;
|
|
5607
|
+
return (
|
|
5608
|
+
1 +
|
|
5609
|
+
AmLyrics.clamp((charCount - 6) / 10, 0, 1) * LONG_WORD_WIPE_EXTRA_RATIO
|
|
5610
|
+
);
|
|
5611
|
+
}
|
|
5612
|
+
|
|
5613
|
+
private static applyWipeShape(element: HTMLElement, charCount: number): void {
|
|
5614
|
+
const extra =
|
|
5615
|
+
AmLyrics.clamp((charCount - 6) / 10, 0, 1) * LONG_WORD_WIPE_EXTRA_EM;
|
|
5616
|
+
const width = BASE_WIPE_GRADIENT_EM + extra;
|
|
5617
|
+
element.style.setProperty('--wipe-gradient-width', `${width.toFixed(3)}em`);
|
|
5618
|
+
element.style.setProperty(
|
|
5619
|
+
'--wipe-gradient-half',
|
|
5620
|
+
`${(width / 2).toFixed(3)}em`,
|
|
5621
|
+
);
|
|
5622
|
+
}
|
|
5623
|
+
|
|
5624
|
+
private static ensureWordWipeGeometry(
|
|
5625
|
+
charSpans: HTMLElement[],
|
|
5626
|
+
charCount: number,
|
|
5627
|
+
): void {
|
|
5628
|
+
if (charSpans.length === 0) return;
|
|
5629
|
+
|
|
5630
|
+
const approxWidthCh = Math.max(1, charCount || charSpans.length);
|
|
5631
|
+
charSpans.forEach((span, index) => {
|
|
5632
|
+
if (!span.style.getPropertyValue('--word-wipe-width')) {
|
|
5633
|
+
span.style.setProperty('--word-wipe-width', `${approxWidthCh}ch`);
|
|
5634
|
+
}
|
|
5635
|
+
|
|
5636
|
+
if (!span.style.getPropertyValue('--char-wipe-position')) {
|
|
5637
|
+
const startPct = Number.parseFloat(
|
|
5638
|
+
span.dataset.wipeStart || `${index / Math.max(1, charSpans.length)}`,
|
|
5639
|
+
);
|
|
5640
|
+
span.style.setProperty(
|
|
5641
|
+
'--char-wipe-position',
|
|
5642
|
+
`${-(AmLyrics.clamp(startPct, 0, 1) * approxWidthCh)}ch`,
|
|
5643
|
+
);
|
|
5644
|
+
}
|
|
5645
|
+
});
|
|
5646
|
+
}
|
|
5647
|
+
|
|
5648
|
+
private static clearPreHighlight(syllable: HTMLElement): void {
|
|
5649
|
+
const target = syllable;
|
|
5650
|
+
target.classList.remove('pre-highlight');
|
|
5651
|
+
target.style.removeProperty('--pre-wipe-duration');
|
|
5652
|
+
target.style.removeProperty('--pre-wipe-delay');
|
|
5653
|
+
target.style.animation = '';
|
|
5654
|
+
target
|
|
5655
|
+
.querySelectorAll('.pre-wipe-lead')
|
|
5656
|
+
.forEach(element => AmLyrics.clearPreWipeLead(element as HTMLElement));
|
|
5657
|
+
}
|
|
5658
|
+
|
|
5659
|
+
private static clearPreWipeLead(element: HTMLElement): void {
|
|
5660
|
+
element.classList.remove('pre-wipe-lead');
|
|
5661
|
+
element.style.removeProperty('--pre-wipe-duration');
|
|
5662
|
+
element.style.removeProperty('--pre-wipe-delay');
|
|
5663
|
+
}
|
|
5664
|
+
|
|
5665
|
+
private static hasTextBoundaryAfter(syllable: HTMLElement): boolean {
|
|
5666
|
+
return /\s$/.test(syllable.textContent || '');
|
|
5667
|
+
}
|
|
5668
|
+
|
|
5669
|
+
private static getSyllableWordIndex(syllable: HTMLElement): string {
|
|
5670
|
+
const wordElement = AmLyrics.getWordElementForSyllable(syllable);
|
|
5671
|
+
const virtualWordId = wordElement?.dataset.virtualWordId;
|
|
5672
|
+
if (virtualWordId) {
|
|
5673
|
+
return `virtual:${virtualWordId}`;
|
|
5674
|
+
}
|
|
5675
|
+
|
|
5676
|
+
const virtualWordStart = wordElement?.dataset.virtualWordStart;
|
|
5677
|
+
const virtualWordEnd = wordElement?.dataset.virtualWordEnd;
|
|
5678
|
+
if (virtualWordStart || virtualWordEnd) {
|
|
5679
|
+
return `virtual:${virtualWordStart || ''}:${virtualWordEnd || ''}`;
|
|
5680
|
+
}
|
|
5681
|
+
|
|
5682
|
+
return (
|
|
5683
|
+
syllable.getAttribute('data-word-index') ||
|
|
5684
|
+
syllable.getAttribute('data-syllable-index') ||
|
|
5685
|
+
''
|
|
5686
|
+
);
|
|
5687
|
+
}
|
|
5688
|
+
|
|
5689
|
+
private static getNextWordSyllable(
|
|
5690
|
+
syllables: HTMLElement[],
|
|
5691
|
+
index: number,
|
|
5692
|
+
): HTMLElement | null {
|
|
5693
|
+
const current = syllables[index];
|
|
5694
|
+
const currentWordIndex = AmLyrics.getSyllableWordIndex(current);
|
|
5695
|
+
const previousSyllable = current;
|
|
5696
|
+
|
|
5697
|
+
for (let i = index + 1; i < syllables.length; i += 1) {
|
|
5698
|
+
const candidate = syllables[i];
|
|
5699
|
+
if (candidate.classList.contains('transliteration')) {
|
|
5700
|
+
// eslint-disable-next-line no-continue
|
|
5701
|
+
continue;
|
|
5702
|
+
}
|
|
5703
|
+
|
|
5704
|
+
const candidateWordIndex = AmLyrics.getSyllableWordIndex(candidate);
|
|
5705
|
+
if (
|
|
5706
|
+
candidateWordIndex === currentWordIndex ||
|
|
5707
|
+
!AmLyrics.hasTextBoundaryAfter(previousSyllable)
|
|
5708
|
+
) {
|
|
5709
|
+
return null;
|
|
5710
|
+
}
|
|
5711
|
+
|
|
5712
|
+
return candidate;
|
|
5713
|
+
}
|
|
5714
|
+
|
|
5715
|
+
return null;
|
|
5716
|
+
}
|
|
5717
|
+
|
|
5718
|
+
private static getPreviousNonTransliterationSyllable(
|
|
5719
|
+
syllables: HTMLElement[],
|
|
5720
|
+
index: number,
|
|
5721
|
+
): HTMLElement | null {
|
|
5722
|
+
for (let i = index - 1; i >= 0; i -= 1) {
|
|
5723
|
+
const candidate = syllables[i];
|
|
5724
|
+
if (!candidate.classList.contains('transliteration')) {
|
|
5725
|
+
return candidate;
|
|
5726
|
+
}
|
|
5727
|
+
}
|
|
5728
|
+
|
|
5729
|
+
return null;
|
|
5730
|
+
}
|
|
5731
|
+
|
|
5732
|
+
private static getRenderedWordSyllables(
|
|
5328
5733
|
syllable: HTMLElement,
|
|
5329
|
-
|
|
5734
|
+
): HTMLElement[] {
|
|
5735
|
+
const wordElement = AmLyrics.getWordElementForSyllable(syllable);
|
|
5736
|
+
const wordElements = AmLyrics.getCachedVirtualWordElements(wordElement);
|
|
5737
|
+
const wordSyllables = wordElements.flatMap(
|
|
5738
|
+
element =>
|
|
5739
|
+
Array.from(
|
|
5740
|
+
element.querySelectorAll('.lyrics-syllable'),
|
|
5741
|
+
) as HTMLElement[],
|
|
5742
|
+
);
|
|
5743
|
+
|
|
5744
|
+
return wordSyllables.filter(
|
|
5745
|
+
wordSyllable => !wordSyllable.classList.contains('transliteration'),
|
|
5746
|
+
);
|
|
5747
|
+
}
|
|
5748
|
+
|
|
5749
|
+
private static getWordElementForSyllable(
|
|
5750
|
+
syllable: HTMLElement,
|
|
5751
|
+
): HTMLElement | undefined {
|
|
5752
|
+
return syllable.parentElement?.parentElement as HTMLElement | undefined;
|
|
5753
|
+
}
|
|
5754
|
+
|
|
5755
|
+
private static getWordPreWipeKey(syllable: HTMLElement): string {
|
|
5756
|
+
const wordElement = AmLyrics.getWordElementForSyllable(syllable);
|
|
5757
|
+
return (
|
|
5758
|
+
wordElement?.dataset.virtualWordId ||
|
|
5759
|
+
`${syllable.getAttribute('data-start-time') || ''}:${AmLyrics.getSyllableWordIndex(
|
|
5760
|
+
syllable,
|
|
5761
|
+
)}`
|
|
5762
|
+
);
|
|
5763
|
+
}
|
|
5764
|
+
|
|
5765
|
+
private static isPreWipeArmed(syllable: HTMLElement): boolean {
|
|
5766
|
+
const wordElement = AmLyrics.getWordElementForSyllable(syllable);
|
|
5767
|
+
const target = wordElement as any;
|
|
5768
|
+
return Boolean(
|
|
5769
|
+
target?._wordPreWipeKey === AmLyrics.getWordPreWipeKey(syllable),
|
|
5770
|
+
);
|
|
5771
|
+
}
|
|
5772
|
+
|
|
5773
|
+
private static applyWordPreWipe(
|
|
5774
|
+
nextSyllable: HTMLElement,
|
|
5775
|
+
wordSyllables: HTMLElement[],
|
|
5776
|
+
currentTimeMs: number,
|
|
5777
|
+
preWipeStartMs: number,
|
|
5778
|
+
preWipeDurationMs: number,
|
|
5330
5779
|
): void {
|
|
5331
|
-
if (
|
|
5780
|
+
if (AmLyrics.isPreWipeArmed(nextSyllable)) return;
|
|
5781
|
+
|
|
5782
|
+
const wordElement = AmLyrics.getWordElementForSyllable(nextSyllable);
|
|
5783
|
+
const wordElements = AmLyrics.getCachedVirtualWordElements(wordElement);
|
|
5784
|
+
const charSpans = AmLyrics.getCachedVirtualWordCharSpans(wordElement, []);
|
|
5785
|
+
const elapsedPreWipe = currentTimeMs - preWipeStartMs;
|
|
5786
|
+
const charCount =
|
|
5787
|
+
charSpans.length ||
|
|
5788
|
+
wordSyllables.reduce(
|
|
5789
|
+
(total, wordSyllable) =>
|
|
5790
|
+
total + AmLyrics.getVisibleCharacterCount(wordSyllable),
|
|
5791
|
+
0,
|
|
5792
|
+
) ||
|
|
5793
|
+
AmLyrics.getVisibleCharacterCount(nextSyllable);
|
|
5794
|
+
AmLyrics.ensureWordWipeGeometry(charSpans, charCount);
|
|
5795
|
+
const leadChar = charSpans[0];
|
|
5796
|
+
const leadCharSyllable = leadChar?.closest(
|
|
5797
|
+
'.lyrics-syllable',
|
|
5798
|
+
) as HTMLElement | null;
|
|
5799
|
+
const preWipeSyllable =
|
|
5800
|
+
leadCharSyllable || wordSyllables[0] || nextSyllable;
|
|
5332
5801
|
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
)
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5802
|
+
AmLyrics.applyWipeShape(preWipeSyllable, charCount);
|
|
5803
|
+
preWipeSyllable.style.setProperty(
|
|
5804
|
+
'--pre-wipe-duration',
|
|
5805
|
+
`${preWipeDurationMs}ms`,
|
|
5806
|
+
);
|
|
5807
|
+
preWipeSyllable.style.setProperty(
|
|
5808
|
+
'--pre-wipe-delay',
|
|
5809
|
+
`${-elapsedPreWipe}ms`,
|
|
5810
|
+
);
|
|
5811
|
+
preWipeSyllable.classList.add('pre-highlight');
|
|
5812
|
+
|
|
5813
|
+
let leadSpans = charSpans;
|
|
5814
|
+
if (leadSpans.length === 0 && leadChar) {
|
|
5815
|
+
leadSpans = [leadChar];
|
|
5816
|
+
}
|
|
5817
|
+
leadSpans.forEach(span => {
|
|
5818
|
+
AmLyrics.applyWipeShape(span, charCount);
|
|
5819
|
+
span.style.setProperty('--pre-wipe-duration', `${preWipeDurationMs}ms`);
|
|
5820
|
+
span.style.setProperty('--pre-wipe-delay', `${-elapsedPreWipe}ms`);
|
|
5821
|
+
span.classList.add('pre-wipe-lead');
|
|
5822
|
+
});
|
|
5823
|
+
|
|
5824
|
+
wordElements.forEach(element => {
|
|
5825
|
+
const target = element as any;
|
|
5826
|
+
target._wordPreWipeKey = AmLyrics.getWordPreWipeKey(nextSyllable);
|
|
5827
|
+
});
|
|
5828
|
+
}
|
|
5829
|
+
|
|
5830
|
+
private static maybePreWipeNextWord(
|
|
5831
|
+
syllables: HTMLElement[],
|
|
5832
|
+
index: number,
|
|
5833
|
+
currentTimeMs: number,
|
|
5834
|
+
currentEndTimeMs: number,
|
|
5835
|
+
): void {
|
|
5836
|
+
const syllable = syllables[index];
|
|
5837
|
+
if (
|
|
5838
|
+
syllable.classList.contains('line-synced') ||
|
|
5839
|
+
syllable.classList.contains('transliteration') ||
|
|
5840
|
+
syllable.closest('.lyrics-gap')
|
|
5841
|
+
) {
|
|
5842
|
+
return;
|
|
5843
|
+
}
|
|
5844
|
+
|
|
5845
|
+
const currentWordReady =
|
|
5846
|
+
syllable.classList.contains('finished') ||
|
|
5847
|
+
currentTimeMs >= currentEndTimeMs - WORD_PRE_WIPE_HANDOFF_LEAD_MS;
|
|
5848
|
+
if (!currentWordReady) {
|
|
5849
|
+
return;
|
|
5850
|
+
}
|
|
5851
|
+
|
|
5852
|
+
const nextSyllable = AmLyrics.getNextWordSyllable(syllables, index);
|
|
5853
|
+
if (
|
|
5854
|
+
!nextSyllable ||
|
|
5855
|
+
nextSyllable.classList.contains('line-synced') ||
|
|
5856
|
+
nextSyllable.classList.contains('transliteration') ||
|
|
5857
|
+
nextSyllable.closest('.lyrics-gap') ||
|
|
5858
|
+
nextSyllable.classList.contains('highlight') ||
|
|
5859
|
+
nextSyllable.classList.contains('finished')
|
|
5860
|
+
) {
|
|
5861
|
+
return;
|
|
5862
|
+
}
|
|
5863
|
+
|
|
5864
|
+
const nextStartTimeMs = (nextSyllable as any)._cachedStartTime;
|
|
5865
|
+
if (!Number.isFinite(nextStartTimeMs)) return;
|
|
5866
|
+
|
|
5867
|
+
const gapMs = nextStartTimeMs - currentEndTimeMs;
|
|
5868
|
+
if (gapMs > NEXT_WORD_PRE_WIPE_MAX_GAP_MS || gapMs < -50) {
|
|
5869
|
+
return;
|
|
5870
|
+
}
|
|
5871
|
+
|
|
5872
|
+
const nextWordSyllables = AmLyrics.getRenderedWordSyllables(nextSyllable);
|
|
5873
|
+
const preWipeSyllables =
|
|
5874
|
+
nextWordSyllables.length > 0 ? nextWordSyllables : [nextSyllable];
|
|
5875
|
+
const wordElement = AmLyrics.getWordElementForSyllable(nextSyllable);
|
|
5876
|
+
const wordCharSpans = AmLyrics.getCachedVirtualWordCharSpans(
|
|
5877
|
+
wordElement,
|
|
5878
|
+
[],
|
|
5879
|
+
);
|
|
5880
|
+
const charCount =
|
|
5881
|
+
wordCharSpans.length ||
|
|
5882
|
+
preWipeSyllables.reduce(
|
|
5883
|
+
(total, wordSyllable) =>
|
|
5884
|
+
total + AmLyrics.getVisibleCharacterCount(wordSyllable),
|
|
5885
|
+
0,
|
|
5886
|
+
);
|
|
5887
|
+
if (charCount <= 0) return;
|
|
5888
|
+
|
|
5889
|
+
const preWipeDuration = AmLyrics.clamp(
|
|
5890
|
+
64 + charCount * 9,
|
|
5891
|
+
NEXT_WORD_PRE_WIPE_MIN_DURATION_MS,
|
|
5892
|
+
NEXT_WORD_PRE_WIPE_MAX_DURATION_MS,
|
|
5893
|
+
);
|
|
5894
|
+
const preWipeStart = Math.max(
|
|
5895
|
+
nextStartTimeMs - preWipeDuration,
|
|
5896
|
+
currentEndTimeMs - WORD_PRE_WIPE_HANDOFF_LEAD_MS,
|
|
5897
|
+
);
|
|
5898
|
+
|
|
5899
|
+
if (currentTimeMs < preWipeStart || currentTimeMs >= nextStartTimeMs) {
|
|
5900
|
+
return;
|
|
5901
|
+
}
|
|
5902
|
+
|
|
5903
|
+
AmLyrics.applyWordPreWipe(
|
|
5904
|
+
nextSyllable,
|
|
5905
|
+
preWipeSyllables,
|
|
5906
|
+
currentTimeMs,
|
|
5907
|
+
preWipeStart,
|
|
5908
|
+
preWipeDuration,
|
|
5909
|
+
);
|
|
5910
|
+
}
|
|
5911
|
+
|
|
5912
|
+
private static getCachedCharSpans(element: HTMLElement): HTMLElement[] {
|
|
5913
|
+
const cacheTarget = element as any;
|
|
5914
|
+
if (!cacheTarget._cachedCharSpans) {
|
|
5915
|
+
cacheTarget._cachedCharSpans = Array.from(
|
|
5916
|
+
element.querySelectorAll('span.char'),
|
|
5917
|
+
) as HTMLElement[];
|
|
5918
|
+
}
|
|
5919
|
+
return cacheTarget._cachedCharSpans as HTMLElement[];
|
|
5920
|
+
}
|
|
5921
|
+
|
|
5922
|
+
private static getCachedVirtualWordElements(
|
|
5923
|
+
wordElement: HTMLElement | undefined,
|
|
5924
|
+
): HTMLElement[] {
|
|
5925
|
+
if (!wordElement) return [];
|
|
5926
|
+
|
|
5927
|
+
const cacheTarget = wordElement as any;
|
|
5928
|
+
if (cacheTarget._cachedVirtualWordElements) {
|
|
5929
|
+
return cacheTarget._cachedVirtualWordElements as HTMLElement[];
|
|
5930
|
+
}
|
|
5931
|
+
|
|
5932
|
+
const { virtualWordId } = wordElement.dataset;
|
|
5933
|
+
let wordElements: HTMLElement[] = [wordElement];
|
|
5934
|
+
if (virtualWordId && wordElement.parentElement) {
|
|
5343
5935
|
wordElements = Array.from(
|
|
5344
5936
|
wordElement.parentElement.querySelectorAll('.lyrics-word'),
|
|
5345
5937
|
).filter(
|
|
5346
5938
|
el => (el as HTMLElement).dataset.virtualWordId === virtualWordId,
|
|
5347
5939
|
) as HTMLElement[];
|
|
5348
|
-
} else if (wordElement) {
|
|
5349
|
-
wordElements = [wordElement as HTMLElement];
|
|
5350
5940
|
}
|
|
5351
|
-
|
|
5941
|
+
|
|
5942
|
+
wordElements.forEach(element => {
|
|
5943
|
+
const target = element as any;
|
|
5944
|
+
target._cachedVirtualWordElements = wordElements;
|
|
5945
|
+
});
|
|
5946
|
+
|
|
5947
|
+
return wordElements;
|
|
5948
|
+
}
|
|
5949
|
+
|
|
5950
|
+
private static getCachedVirtualWordCharSpans(
|
|
5951
|
+
wordElement: HTMLElement | undefined,
|
|
5952
|
+
fallbackCharSpans: HTMLElement[],
|
|
5953
|
+
): HTMLElement[] {
|
|
5954
|
+
if (!wordElement) return fallbackCharSpans;
|
|
5955
|
+
|
|
5956
|
+
const cacheTarget = wordElement as any;
|
|
5957
|
+
if (cacheTarget._cachedVirtualWordCharSpans) {
|
|
5958
|
+
return cacheTarget._cachedVirtualWordCharSpans as HTMLElement[];
|
|
5959
|
+
}
|
|
5960
|
+
|
|
5961
|
+
const wordElements = AmLyrics.getCachedVirtualWordElements(wordElement);
|
|
5962
|
+
const charSpans = wordElements.flatMap(
|
|
5352
5963
|
word => Array.from(word.querySelectorAll('span.char')) as HTMLElement[],
|
|
5353
5964
|
);
|
|
5354
|
-
const
|
|
5355
|
-
|
|
5965
|
+
const result = charSpans.length > 0 ? charSpans : fallbackCharSpans;
|
|
5966
|
+
|
|
5967
|
+
wordElements.forEach(element => {
|
|
5968
|
+
const target = element as any;
|
|
5969
|
+
target._cachedVirtualWordCharSpans = result;
|
|
5970
|
+
});
|
|
5971
|
+
|
|
5972
|
+
return result;
|
|
5973
|
+
}
|
|
5974
|
+
|
|
5975
|
+
private static updateSyllableAnimation(
|
|
5976
|
+
syllable: HTMLElement,
|
|
5977
|
+
elapsedTimeMs = 0,
|
|
5978
|
+
): void {
|
|
5979
|
+
if (syllable.classList.contains('highlight')) return;
|
|
5980
|
+
|
|
5981
|
+
const { classList } = syllable;
|
|
5982
|
+
const hadPreHighlight = classList.contains('pre-highlight');
|
|
5983
|
+
const isRTL = classList.contains('rtl-text');
|
|
5984
|
+
const charSpans = AmLyrics.getCachedCharSpans(syllable);
|
|
5985
|
+
const wordElement = syllable.parentElement?.parentElement; // syllable-wrap -> word
|
|
5986
|
+
const typedWordElement = wordElement as HTMLElement | undefined;
|
|
5987
|
+
const allWordElements =
|
|
5988
|
+
AmLyrics.getCachedVirtualWordElements(typedWordElement);
|
|
5989
|
+
const allWordCharSpans = AmLyrics.getCachedVirtualWordCharSpans(
|
|
5990
|
+
typedWordElement,
|
|
5991
|
+
charSpans,
|
|
5992
|
+
);
|
|
5993
|
+
const isGrowable = typedWordElement?.classList.contains('growable');
|
|
5994
|
+
const isCharRise = typedWordElement?.classList.contains('char-rise');
|
|
5995
|
+
const isCharDrag = typedWordElement?.classList.contains('char-drag');
|
|
5356
5996
|
const isFirstSyllable =
|
|
5357
5997
|
syllable.getAttribute('data-syllable-index') === '0';
|
|
5358
5998
|
const syllableStartMs = parseFloat(
|
|
5359
5999
|
syllable.getAttribute('data-start-time') || '0',
|
|
5360
6000
|
);
|
|
5361
6001
|
const virtualWordStartMs = parseFloat(
|
|
5362
|
-
|
|
6002
|
+
typedWordElement?.dataset.virtualWordStart || '',
|
|
5363
6003
|
);
|
|
5364
6004
|
const isFirstInVirtualWord =
|
|
5365
6005
|
isFirstSyllable &&
|
|
@@ -5377,8 +6017,11 @@ export class AmLyrics extends LitElement {
|
|
|
5377
6017
|
syllable.getAttribute('data-duration') ||
|
|
5378
6018
|
'0',
|
|
5379
6019
|
) || syllableDurationMs;
|
|
6020
|
+
const wordElapsedTimeMs = Number.isFinite(virtualWordStartMs)
|
|
6021
|
+
? elapsedTimeMs + (syllableStartMs - virtualWordStartMs)
|
|
6022
|
+
: elapsedTimeMs;
|
|
6023
|
+
const charWipeDurationMs = Math.max(wordDurationMs, syllableDurationMs);
|
|
5380
6024
|
|
|
5381
|
-
// Use a Map to collect animations like YouLyPlus
|
|
5382
6025
|
const charAnimationsMap = new Map<HTMLElement, string>();
|
|
5383
6026
|
const styleUpdates: Array<{
|
|
5384
6027
|
element: HTMLElement;
|
|
@@ -5446,18 +6089,82 @@ export class AmLyrics extends LitElement {
|
|
|
5446
6089
|
});
|
|
5447
6090
|
}
|
|
5448
6091
|
|
|
6092
|
+
if (isCharDrag && isFirstInVirtualWord && allWordCharSpans.length > 0) {
|
|
6093
|
+
const finalDuration = Math.max(wordDurationMs, syllableDurationMs);
|
|
6094
|
+
const baseDelayPerChar = AmLyrics.clamp(finalDuration * 0.15, 64, 118);
|
|
6095
|
+
const dragDurationMs = AmLyrics.clamp(finalDuration * 0.82, 560, 900);
|
|
6096
|
+
|
|
6097
|
+
allWordCharSpans.forEach(span => {
|
|
6098
|
+
const charIndex = parseFloat(span.dataset.syllableCharIndex || '0');
|
|
6099
|
+
const dragDelay = baseDelayPerChar * charIndex;
|
|
6100
|
+
|
|
6101
|
+
charAnimationsMap.set(
|
|
6102
|
+
span,
|
|
6103
|
+
`drag-char ${dragDurationMs}ms ease ${dragDelay}ms forwards`,
|
|
6104
|
+
);
|
|
6105
|
+
});
|
|
6106
|
+
}
|
|
6107
|
+
|
|
5449
6108
|
// Step 2: Wipe Pass
|
|
5450
6109
|
if (charSpans.length > 0) {
|
|
5451
|
-
|
|
6110
|
+
const wipeCharCount =
|
|
6111
|
+
allWordCharSpans.length ||
|
|
6112
|
+
charSpans.length ||
|
|
6113
|
+
AmLyrics.getVisibleCharacterCount(syllable);
|
|
6114
|
+
const wipeScale = AmLyrics.getLongWordWipeScale(wipeCharCount);
|
|
6115
|
+
AmLyrics.applyWipeShape(syllable, wipeCharCount);
|
|
6116
|
+
AmLyrics.ensureWordWipeGeometry(allWordCharSpans, wipeCharCount);
|
|
6117
|
+
allWordCharSpans.forEach(span =>
|
|
6118
|
+
AmLyrics.applyWipeShape(span, wipeCharCount),
|
|
6119
|
+
);
|
|
6120
|
+
|
|
6121
|
+
const hasWordLevelWipe =
|
|
6122
|
+
!isFirstInVirtualWord &&
|
|
6123
|
+
(Boolean((typedWordElement as any)?._wordWipeStarted) ||
|
|
6124
|
+
allWordCharSpans.some(span => span.style.animation.includes('wipe')));
|
|
6125
|
+
let charSpansToAnimate = charSpans;
|
|
6126
|
+
if (isFirstInVirtualWord) {
|
|
6127
|
+
charSpansToAnimate = allWordCharSpans;
|
|
6128
|
+
} else if (hasWordLevelWipe) {
|
|
6129
|
+
charSpansToAnimate = [];
|
|
6130
|
+
}
|
|
6131
|
+
|
|
6132
|
+
if (charSpansToAnimate.length > 0 && allWordElements.length > 0) {
|
|
6133
|
+
allWordElements.forEach(element => {
|
|
6134
|
+
const target = element as any;
|
|
6135
|
+
target._wordWipeStarted = true;
|
|
6136
|
+
target._wordPreWipeKey = undefined;
|
|
6137
|
+
});
|
|
6138
|
+
}
|
|
6139
|
+
|
|
6140
|
+
charSpansToAnimate.forEach((span, charIndex) => {
|
|
5452
6141
|
const startPct = parseFloat(span.dataset.wipeStart || '0');
|
|
5453
6142
|
const durationPct = parseFloat(span.dataset.wipeDuration || '0');
|
|
6143
|
+
const globalCharIndex = parseFloat(
|
|
6144
|
+
span.dataset.syllableCharIndex || `${charIndex}`,
|
|
6145
|
+
);
|
|
5454
6146
|
|
|
5455
|
-
const
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
const
|
|
6147
|
+
const hadCharPreWipe =
|
|
6148
|
+
span.classList.contains('pre-wipe-lead') ||
|
|
6149
|
+
(hadPreHighlight && globalCharIndex === 0);
|
|
6150
|
+
const charStartMs = charWipeDurationMs * startPct;
|
|
6151
|
+
const remainingWordWipeMs = Math.max(
|
|
6152
|
+
0,
|
|
6153
|
+
charWipeDurationMs - charStartMs,
|
|
6154
|
+
);
|
|
6155
|
+
let wipeDelay = charStartMs - wordElapsedTimeMs;
|
|
6156
|
+
let wipeDuration = Math.min(
|
|
6157
|
+
charWipeDurationMs * durationPct * wipeScale,
|
|
6158
|
+
remainingWordWipeMs,
|
|
6159
|
+
);
|
|
6160
|
+
const useStartAnimation =
|
|
6161
|
+
isFirstInContainer && globalCharIndex === 0 && !hadCharPreWipe;
|
|
5459
6162
|
let charWipeAnimation = 'wipe';
|
|
5460
|
-
if (
|
|
6163
|
+
if (hadCharPreWipe) {
|
|
6164
|
+
charWipeAnimation = 'wipe-word-from-pre';
|
|
6165
|
+
wipeDelay = -wordElapsedTimeMs;
|
|
6166
|
+
wipeDuration = charWipeDurationMs;
|
|
6167
|
+
} else if (useStartAnimation) {
|
|
5461
6168
|
charWipeAnimation = isRTL ? 'start-wipe-rtl' : 'start-wipe';
|
|
5462
6169
|
} else {
|
|
5463
6170
|
charWipeAnimation = isRTL ? 'wipe-rtl' : 'wipe';
|
|
@@ -5471,36 +6178,15 @@ export class AmLyrics extends LitElement {
|
|
|
5471
6178
|
if (
|
|
5472
6179
|
existingAnimation &&
|
|
5473
6180
|
(existingAnimation.includes('grow-dynamic') ||
|
|
5474
|
-
existingAnimation.includes('rise-char')
|
|
6181
|
+
existingAnimation.includes('rise-char') ||
|
|
6182
|
+
existingAnimation.includes('drag-char'))
|
|
5475
6183
|
) {
|
|
5476
6184
|
animationParts.push(existingAnimation.split(',')[0].trim());
|
|
5477
6185
|
}
|
|
5478
|
-
if (charIndex > 0 && wipeDelay > 0 && wipeDuration > 0) {
|
|
5479
|
-
const arrivalTime =
|
|
5480
|
-
(span.dataset.preWipeArrival
|
|
5481
|
-
? parseFloat(span.dataset.preWipeArrival)
|
|
5482
|
-
: syllableDurationMs * startPct) - elapsedTimeMs;
|
|
5483
|
-
const measuredPreWipeDuration = parseFloat(
|
|
5484
|
-
span.dataset.preWipeDuration || '100',
|
|
5485
|
-
);
|
|
5486
|
-
const preWipeDuration = Math.min(
|
|
5487
|
-
measuredPreWipeDuration,
|
|
5488
|
-
wipeDuration * 0.9,
|
|
5489
|
-
syllableDurationMs * 0.08,
|
|
5490
|
-
arrivalTime,
|
|
5491
|
-
);
|
|
5492
|
-
const animDelay = arrivalTime - preWipeDuration;
|
|
5493
|
-
|
|
5494
|
-
if (preWipeDuration >= 16) {
|
|
5495
|
-
animationParts.push(
|
|
5496
|
-
`pre-wipe-char ${preWipeDuration}ms linear ${animDelay}ms none`,
|
|
5497
|
-
);
|
|
5498
|
-
}
|
|
5499
|
-
}
|
|
5500
|
-
|
|
5501
6186
|
if (wipeDuration > 0) {
|
|
6187
|
+
const wipeFillMode = hadCharPreWipe ? 'both' : 'forwards';
|
|
5502
6188
|
animationParts.push(
|
|
5503
|
-
`${charWipeAnimation} ${wipeDuration}ms linear ${wipeDelay}ms
|
|
6189
|
+
`${charWipeAnimation} ${wipeDuration}ms linear ${wipeDelay}ms ${wipeFillMode}`,
|
|
5504
6190
|
);
|
|
5505
6191
|
}
|
|
5506
6192
|
|
|
@@ -5513,10 +6199,15 @@ export class AmLyrics extends LitElement {
|
|
|
5513
6199
|
const wipeRatio = parseFloat(
|
|
5514
6200
|
syllable.getAttribute('data-wipe-ratio') || '1',
|
|
5515
6201
|
);
|
|
5516
|
-
const
|
|
6202
|
+
const wipeCharCount = AmLyrics.getVisibleCharacterCount(syllable);
|
|
6203
|
+
const wipeScale = AmLyrics.getLongWordWipeScale(wipeCharCount);
|
|
6204
|
+
const visualDuration = syllableDurationMs * wipeRatio * wipeScale;
|
|
6205
|
+
AmLyrics.applyWipeShape(syllable, wipeCharCount);
|
|
5517
6206
|
|
|
5518
6207
|
let wipeAnimation = 'wipe';
|
|
5519
|
-
if (
|
|
6208
|
+
if (hadPreHighlight) {
|
|
6209
|
+
wipeAnimation = isRTL ? 'wipe-from-pre-rtl' : 'wipe-from-pre';
|
|
6210
|
+
} else if (isFirstInContainer) {
|
|
5520
6211
|
wipeAnimation = isRTL ? 'start-wipe-rtl' : 'start-wipe';
|
|
5521
6212
|
} else {
|
|
5522
6213
|
wipeAnimation = isRTL ? 'wipe-rtl' : 'wipe';
|
|
@@ -5530,18 +6221,28 @@ export class AmLyrics extends LitElement {
|
|
|
5530
6221
|
}
|
|
5531
6222
|
|
|
5532
6223
|
// --- WRITE PHASE ---
|
|
6224
|
+
if (allWordElements.length > 0) {
|
|
6225
|
+
allWordElements.forEach(element => {
|
|
6226
|
+
const target = element as any;
|
|
6227
|
+
target._wordPreWipeKey = undefined;
|
|
6228
|
+
});
|
|
6229
|
+
}
|
|
6230
|
+
|
|
5533
6231
|
classList.remove('pre-highlight');
|
|
5534
6232
|
classList.add('highlight');
|
|
6233
|
+
allWordCharSpans.forEach(span => AmLyrics.clearPreWipeLead(span));
|
|
6234
|
+
|
|
6235
|
+
// Apply keyframe variables before assigning animation strings so the
|
|
6236
|
+
// first painted frame never uses fallback transform values.
|
|
6237
|
+
for (const update of styleUpdates) {
|
|
6238
|
+
update.element.style.setProperty(update.property, update.value);
|
|
6239
|
+
}
|
|
5535
6240
|
|
|
5536
6241
|
for (const [span, animationString] of charAnimationsMap.entries()) {
|
|
5537
6242
|
span.style.willChange = 'transform';
|
|
6243
|
+
span.style.removeProperty('background-color');
|
|
5538
6244
|
span.style.animation = animationString;
|
|
5539
6245
|
}
|
|
5540
|
-
|
|
5541
|
-
// Apply style updates
|
|
5542
|
-
for (const update of styleUpdates) {
|
|
5543
|
-
update.element.style.setProperty(update.property, update.value);
|
|
5544
|
-
}
|
|
5545
6246
|
}
|
|
5546
6247
|
|
|
5547
6248
|
/**
|
|
@@ -5566,6 +6267,7 @@ export class AmLyrics extends LitElement {
|
|
|
5566
6267
|
el.style.animation = '';
|
|
5567
6268
|
el.style.transition = 'none';
|
|
5568
6269
|
el.style.backgroundColor = 'var(--lyplus-text-secondary)';
|
|
6270
|
+
AmLyrics.clearPreWipeLead(el);
|
|
5569
6271
|
}
|
|
5570
6272
|
|
|
5571
6273
|
// Immediately remove all state classes
|
|
@@ -5577,12 +6279,22 @@ export class AmLyrics extends LitElement {
|
|
|
5577
6279
|
);
|
|
5578
6280
|
}
|
|
5579
6281
|
|
|
6282
|
+
private static resetWordAnimationState(line: HTMLElement): void {
|
|
6283
|
+
const wordElements = line.querySelectorAll('.lyrics-word');
|
|
6284
|
+
wordElements.forEach(wordElement => {
|
|
6285
|
+
const target = wordElement as any;
|
|
6286
|
+
target._wordPreWipeKey = undefined;
|
|
6287
|
+
target._wordWipeStarted = false;
|
|
6288
|
+
});
|
|
6289
|
+
}
|
|
6290
|
+
|
|
5580
6291
|
/**
|
|
5581
6292
|
* Reset all syllables in a line — batches deferred cleanup into a single rAF
|
|
5582
6293
|
*/
|
|
5583
6294
|
private static resetSyllables(line: HTMLElement): void {
|
|
5584
6295
|
if (!line) return;
|
|
5585
6296
|
line.classList.remove('persist-highlight');
|
|
6297
|
+
AmLyrics.resetWordAnimationState(line);
|
|
5586
6298
|
// eslint-disable-next-line no-param-reassign
|
|
5587
6299
|
(line as any)._cachedSyllableElements = null;
|
|
5588
6300
|
const syllables = line.getElementsByClassName('lyrics-syllable');
|
|
@@ -5614,6 +6326,7 @@ export class AmLyrics extends LitElement {
|
|
|
5614
6326
|
private static unfinishSyllables(line: HTMLElement): void {
|
|
5615
6327
|
if (!line) return;
|
|
5616
6328
|
line.classList.remove('persist-highlight');
|
|
6329
|
+
AmLyrics.resetWordAnimationState(line);
|
|
5617
6330
|
const syllables = line.getElementsByClassName('lyrics-syllable');
|
|
5618
6331
|
for (let i = 0; i < syllables.length; i += 1) {
|
|
5619
6332
|
const s = syllables[i] as HTMLElement;
|
|
@@ -5631,6 +6344,7 @@ export class AmLyrics extends LitElement {
|
|
|
5631
6344
|
el.style.removeProperty('background-color');
|
|
5632
6345
|
el.style.removeProperty('transition');
|
|
5633
6346
|
el.style.removeProperty('filter');
|
|
6347
|
+
AmLyrics.clearPreWipeLead(el);
|
|
5634
6348
|
}
|
|
5635
6349
|
}
|
|
5636
6350
|
}
|
|
@@ -5681,22 +6395,33 @@ export class AmLyrics extends LitElement {
|
|
|
5681
6395
|
syllable.style.animation = '';
|
|
5682
6396
|
syllable.style.removeProperty('--pre-wipe-duration');
|
|
5683
6397
|
syllable.style.removeProperty('--pre-wipe-delay');
|
|
6398
|
+
syllable.style.removeProperty('background-color');
|
|
6399
|
+
AmLyrics.applyWipeShape(
|
|
6400
|
+
syllable,
|
|
6401
|
+
AmLyrics.getVisibleCharacterCount(syllable),
|
|
6402
|
+
);
|
|
5684
6403
|
const chars = syllable.querySelectorAll('span.char');
|
|
5685
6404
|
for (let ci = 0; ci < chars.length; ci += 1) {
|
|
5686
6405
|
const charEl = chars[ci] as HTMLElement;
|
|
5687
6406
|
const currentAnim = charEl.style.animation || '';
|
|
5688
6407
|
if (
|
|
5689
6408
|
currentAnim.includes('grow-dynamic') ||
|
|
5690
|
-
currentAnim.includes('rise-char')
|
|
6409
|
+
currentAnim.includes('rise-char') ||
|
|
6410
|
+
currentAnim.includes('drag-char')
|
|
5691
6411
|
) {
|
|
5692
6412
|
const parts = currentAnim.split(',').map(p => p.trim());
|
|
5693
6413
|
const transformAnim = parts.find(
|
|
5694
|
-
p =>
|
|
6414
|
+
p =>
|
|
6415
|
+
p.includes('grow-dynamic') ||
|
|
6416
|
+
p.includes('rise-char') ||
|
|
6417
|
+
p.includes('drag-char'),
|
|
5695
6418
|
);
|
|
5696
6419
|
charEl.style.animation = transformAnim || '';
|
|
5697
6420
|
} else {
|
|
5698
6421
|
charEl.style.animation = '';
|
|
5699
6422
|
}
|
|
6423
|
+
charEl.style.backgroundColor = 'var(--lyplus-text-primary)';
|
|
6424
|
+
AmLyrics.clearPreWipeLead(charEl);
|
|
5700
6425
|
}
|
|
5701
6426
|
}
|
|
5702
6427
|
}
|
|
@@ -5751,14 +6476,20 @@ export class AmLyrics extends LitElement {
|
|
|
5751
6476
|
if (!(currentTimeMs < startTime - 1000 && !hasActiveState)) {
|
|
5752
6477
|
let preHighlightReset = false;
|
|
5753
6478
|
|
|
5754
|
-
//
|
|
5755
|
-
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
6479
|
+
// Before the syllable starts, pre-highlight only belongs beside a
|
|
6480
|
+
// previous active word. Once the syllable starts, updateSyllableAnimation
|
|
6481
|
+
// consumes the class so the actual wipe can continue from the pre-wipe
|
|
6482
|
+
// pose instead of restarting from the beginning.
|
|
6483
|
+
if (hasPreHighlight && currentTimeMs < startTime) {
|
|
6484
|
+
const prevSyllable = AmLyrics.getPreviousNonTransliterationSyllable(
|
|
6485
|
+
syllables,
|
|
6486
|
+
i,
|
|
6487
|
+
);
|
|
6488
|
+
const previousCarriesHighlight =
|
|
6489
|
+
prevSyllable?.classList.contains('highlight') ||
|
|
6490
|
+
prevSyllable?.classList.contains('finished');
|
|
6491
|
+
if (!previousCarriesHighlight) {
|
|
6492
|
+
AmLyrics.clearPreHighlight(syllable);
|
|
5762
6493
|
preHighlightReset = true;
|
|
5763
6494
|
}
|
|
5764
6495
|
}
|
|
@@ -5791,6 +6522,8 @@ export class AmLyrics extends LitElement {
|
|
|
5791
6522
|
// Not yet started
|
|
5792
6523
|
AmLyrics.resetSyllable(syllable);
|
|
5793
6524
|
}
|
|
6525
|
+
|
|
6526
|
+
AmLyrics.maybePreWipeNextWord(syllables, i, currentTimeMs, endTime);
|
|
5794
6527
|
}
|
|
5795
6528
|
}
|
|
5796
6529
|
}
|
|
@@ -6151,6 +6884,9 @@ export class AmLyrics extends LitElement {
|
|
|
6151
6884
|
data-end-time="${endTimeMs}"
|
|
6152
6885
|
data-duration="${durationMs}"
|
|
6153
6886
|
data-syllable-index="${syllableIndex}"
|
|
6887
|
+
data-word-index="${syllableIndex}"
|
|
6888
|
+
data-word-length="${syllable.text.replace(/\s/g, '')
|
|
6889
|
+
.length}"
|
|
6154
6890
|
data-wipe-ratio="1"
|
|
6155
6891
|
>${syllable.text}</span
|
|
6156
6892
|
>${bgRomanizedText}</span
|
|
@@ -6175,6 +6911,7 @@ export class AmLyrics extends LitElement {
|
|
|
6175
6911
|
const groupGrowable = lineData?.groupGrowable ?? [];
|
|
6176
6912
|
const groupGlowing = lineData?.groupGlowing ?? [];
|
|
6177
6913
|
const groupCharRise = lineData?.groupCharRise ?? [];
|
|
6914
|
+
const groupCharDrag = lineData?.groupCharDrag ?? [];
|
|
6178
6915
|
const vwFullText = lineData?.vwFullText ?? [];
|
|
6179
6916
|
const vwFullDuration = lineData?.vwFullDuration ?? [];
|
|
6180
6917
|
const vwCharOffset = lineData?.vwCharOffset ?? [];
|
|
@@ -6182,7 +6919,6 @@ export class AmLyrics extends LitElement {
|
|
|
6182
6919
|
const vwEndMs = lineData?.vwEndMs ?? [];
|
|
6183
6920
|
const lineIsRTL = lineData?.lineIsRTL ?? false;
|
|
6184
6921
|
|
|
6185
|
-
// Create main vocals using YouLyPlus syllable structure
|
|
6186
6922
|
const mainVocalElement = html`<p
|
|
6187
6923
|
class="main-vocal-container ${lineIsRTL ? 'rtl-text' : ''}"
|
|
6188
6924
|
>
|
|
@@ -6190,28 +6926,26 @@ export class AmLyrics extends LitElement {
|
|
|
6190
6926
|
const isGrowable = groupGrowable[groupIdx];
|
|
6191
6927
|
const isGlowing = groupGlowing[groupIdx];
|
|
6192
6928
|
const isCharRise = groupCharRise[groupIdx];
|
|
6193
|
-
const
|
|
6929
|
+
const isCharDrag = groupCharDrag[groupIdx];
|
|
6930
|
+
const isAnimatedByChar = isGrowable || isCharRise || isCharDrag;
|
|
6194
6931
|
const groupLineSynced = group.some(s => s.lineSynced);
|
|
6195
6932
|
|
|
6196
6933
|
const wordText = isAnimatedByChar ? vwFullText[groupIdx] : '';
|
|
6197
6934
|
const wordDuration = isAnimatedByChar
|
|
6198
6935
|
? vwFullDuration[groupIdx]
|
|
6199
6936
|
: 0;
|
|
6200
|
-
const wordNumChars = wordText.length;
|
|
6937
|
+
const wordNumChars = wordText.replace(/\s/g, '').length;
|
|
6201
6938
|
const groupCharOffset = isAnimatedByChar
|
|
6202
6939
|
? vwCharOffset[groupIdx]
|
|
6203
6940
|
: 0;
|
|
6204
|
-
const virtualWordId =
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
const virtualWordStart = isAnimatedByChar
|
|
6208
|
-
? vwStartMs[groupIdx]
|
|
6209
|
-
: '';
|
|
6210
|
-
const virtualWordEnd = isAnimatedByChar ? vwEndMs[groupIdx] : '';
|
|
6941
|
+
const virtualWordId = `${lineIndex}:${vwStartMs[groupIdx]}:${vwEndMs[groupIdx]}`;
|
|
6942
|
+
const virtualWordStart = vwStartMs[groupIdx];
|
|
6943
|
+
const virtualWordEnd = vwEndMs[groupIdx];
|
|
6211
6944
|
|
|
6212
6945
|
let sylCharAccumulator = 0;
|
|
6213
6946
|
|
|
6214
6947
|
const groupText = group.map(s => s.text).join('');
|
|
6948
|
+
const visibleWordLength = groupText.replace(/\s/g, '').length;
|
|
6215
6949
|
const shouldAllowBreak =
|
|
6216
6950
|
groupText.trim().length >= 16 ||
|
|
6217
6951
|
/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(
|
|
@@ -6231,9 +6965,9 @@ export class AmLyrics extends LitElement {
|
|
|
6231
6965
|
return html`<span
|
|
6232
6966
|
class="lyrics-word${isGrowable ? ' growable' : ''}${isCharRise
|
|
6233
6967
|
? ' char-rise'
|
|
6234
|
-
: ''}${
|
|
6235
|
-
? '
|
|
6236
|
-
: ''}"
|
|
6968
|
+
: ''}${isCharDrag ? ' char-drag' : ''}${isGlowing
|
|
6969
|
+
? ' glowing'
|
|
6970
|
+
: ''}${shouldAllowBreak ? ' allow-break' : ''}"
|
|
6237
6971
|
data-virtual-word-id="${virtualWordId}"
|
|
6238
6972
|
data-virtual-word-start="${virtualWordStart}"
|
|
6239
6973
|
data-virtual-word-end="${virtualWordEnd}"
|
|
@@ -6264,17 +6998,39 @@ export class AmLyrics extends LitElement {
|
|
|
6264
6998
|
let syllableContent: any = text;
|
|
6265
6999
|
|
|
6266
7000
|
if (isAnimatedByChar) {
|
|
6267
|
-
let charIndexInsideSyllable = 0;
|
|
6268
7001
|
const numCharsInSyllable =
|
|
6269
7002
|
text.replace(/\s/g, '').length || 1;
|
|
7003
|
+
const hasVirtualTiming =
|
|
7004
|
+
wordDuration > 0 && Number.isFinite(virtualWordStart);
|
|
7005
|
+
const syllableStartRatio = hasVirtualTiming
|
|
7006
|
+
? AmLyrics.clamp(
|
|
7007
|
+
(startTimeMs - virtualWordStart) / wordDuration,
|
|
7008
|
+
0,
|
|
7009
|
+
1,
|
|
7010
|
+
)
|
|
7011
|
+
: 0;
|
|
7012
|
+
const syllableDurationRatio = hasVirtualTiming
|
|
7013
|
+
? AmLyrics.clamp(durationMs / wordDuration, 0, 1)
|
|
7014
|
+
: 1;
|
|
7015
|
+
let charIndexInsideSyllable = 0;
|
|
6270
7016
|
|
|
6271
7017
|
syllableContent = html`${text.split('').map(char => {
|
|
6272
7018
|
if (char === ' ') return ' ';
|
|
6273
7019
|
|
|
6274
7020
|
const charIndexInsideWord =
|
|
6275
7021
|
groupCharOffset + sylCharAccumulator;
|
|
6276
|
-
const
|
|
6277
|
-
|
|
7022
|
+
const localCharIndex = charIndexInsideSyllable;
|
|
7023
|
+
const visibleWordChars = Math.max(1, wordNumChars);
|
|
7024
|
+
const charStartPercentVal = AmLyrics.clamp(
|
|
7025
|
+
syllableStartRatio +
|
|
7026
|
+
(localCharIndex / numCharsInSyllable) *
|
|
7027
|
+
syllableDurationRatio,
|
|
7028
|
+
0,
|
|
7029
|
+
1,
|
|
7030
|
+
);
|
|
7031
|
+
const charDurationPercentVal =
|
|
7032
|
+
syllableDurationRatio / numCharsInSyllable ||
|
|
7033
|
+
1 / visibleWordChars;
|
|
6278
7034
|
|
|
6279
7035
|
sylCharAccumulator += 1;
|
|
6280
7036
|
charIndexInsideSyllable += 1;
|
|
@@ -6336,29 +7092,41 @@ export class AmLyrics extends LitElement {
|
|
|
6336
7092
|
1,
|
|
6337
7093
|
Math.max(0.3, effectiveDuration / 2000),
|
|
6338
7094
|
);
|
|
6339
|
-
const
|
|
7095
|
+
const baseTranslateYPeak =
|
|
6340
7096
|
-normalizedGrowth * (2 * peakMultiplier); // Further dampened lift peak
|
|
6341
7097
|
|
|
6342
7098
|
const position = (charIndexInsideWord + 0.5) / wordNumChars;
|
|
6343
7099
|
const horizontalOffset =
|
|
6344
7100
|
(position - 0.5) * 2 * ((charMaxScale - 1.0) * 25);
|
|
7101
|
+
const isDragMotion = isCharDrag;
|
|
7102
|
+
let charTranslateYPeak = baseTranslateYPeak;
|
|
7103
|
+
if (isCharRise) {
|
|
7104
|
+
charTranslateYPeak = 0;
|
|
7105
|
+
} else if (isDragMotion) {
|
|
7106
|
+
charTranslateYPeak = -0.78;
|
|
7107
|
+
}
|
|
6345
7108
|
|
|
7109
|
+
let motionHorizontalOffset = horizontalOffset;
|
|
7110
|
+
if (isCharRise) {
|
|
7111
|
+
motionHorizontalOffset = 0;
|
|
7112
|
+
} else if (isDragMotion) {
|
|
7113
|
+
motionHorizontalOffset = 0;
|
|
7114
|
+
}
|
|
6346
7115
|
return html`<span
|
|
6347
7116
|
class="char"
|
|
6348
7117
|
data-char-index="${charIndexInsideWord}"
|
|
6349
7118
|
data-syllable-char-index="${charIndexInsideWord}"
|
|
6350
7119
|
data-wipe-start="${charStartPercentVal.toFixed(4)}"
|
|
6351
|
-
data-wipe-duration="${
|
|
6352
|
-
4,
|
|
6353
|
-
)}"
|
|
7120
|
+
data-wipe-duration="${charDurationPercentVal.toFixed(4)}"
|
|
6354
7121
|
data-horizontal-offset="${horizontalOffset.toFixed(2)}"
|
|
6355
7122
|
data-max-scale="${charMaxScale.toFixed(3)}"
|
|
6356
7123
|
data-matrix-scale="${(charMaxScale * 0.98).toFixed(3)}"
|
|
6357
|
-
data-char-offset-x="${(
|
|
6358
|
-
|
|
6359
|
-
)}"
|
|
7124
|
+
data-char-offset-x="${(
|
|
7125
|
+
motionHorizontalOffset * 0.98
|
|
7126
|
+
).toFixed(2)}"
|
|
6360
7127
|
data-shadow-intensity="${charShadowIntensity.toFixed(3)}"
|
|
6361
7128
|
data-translate-y-peak="${charTranslateYPeak.toFixed(3)}"
|
|
7129
|
+
style="--word-wipe-width: ${visibleWordChars}ch; --char-wipe-position: -${charIndexInsideWord}ch"
|
|
6362
7130
|
>${char}</span
|
|
6363
7131
|
>`;
|
|
6364
7132
|
})}`;
|
|
@@ -6377,6 +7145,8 @@ export class AmLyrics extends LitElement {
|
|
|
6377
7145
|
data-duration="${durationMs}"
|
|
6378
7146
|
data-word-duration="${wordDuration}"
|
|
6379
7147
|
data-syllable-index="${sylIdx}"
|
|
7148
|
+
data-word-index="${groupIdx}"
|
|
7149
|
+
data-word-length="${visibleWordLength}"
|
|
6380
7150
|
data-wipe-ratio="1"
|
|
6381
7151
|
>${syllableContent}</span
|
|
6382
7152
|
>${romanizedText}</span
|
|
@@ -6617,13 +7387,14 @@ export class AmLyrics extends LitElement {
|
|
|
6617
7387
|
<button
|
|
6618
7388
|
class="download-button source-switch-btn"
|
|
6619
7389
|
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
7390
|
@click=${this.switchSource}
|
|
6622
7391
|
?disabled=${this.isFetchingAlternatives}
|
|
6623
7392
|
>
|
|
6624
7393
|
<svg
|
|
6625
|
-
class="source-switch-svg lucide lucide-arrow-down-up-icon lucide-arrow-down-up
|
|
6626
|
-
|
|
7394
|
+
class="source-switch-svg lucide lucide-arrow-down-up-icon lucide-arrow-down-up ${this
|
|
7395
|
+
.isFetchingAlternatives
|
|
7396
|
+
? 'is-loading'
|
|
7397
|
+
: ''}"
|
|
6627
7398
|
xmlns="http://www.w3.org/2000/svg"
|
|
6628
7399
|
width="12"
|
|
6629
7400
|
height="12"
|