@uimaxbai/am-lyrics 1.5.3 → 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 +903 -343
- package/dist/src/am-lyrics.js.map +1 -1
- package/dist/src/react.js +903 -343
- package/dist/src/react.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/AmLyrics.ts +1194 -380
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;
|
|
@@ -259,17 +270,15 @@ export class AmLyrics extends LitElement {
|
|
|
259
270
|
|
|
260
271
|
.background-vocal-container {
|
|
261
272
|
max-height: 0;
|
|
262
|
-
overflow:
|
|
273
|
+
overflow: hidden;
|
|
263
274
|
opacity: 0;
|
|
264
275
|
font-size: var(--lyplus-font-size-subtext);
|
|
265
276
|
line-height: 1.15;
|
|
266
277
|
color: color-mix(in srgb, var(--lyplus-text-secondary) 80%, transparent);
|
|
267
|
-
/* Fast exit (0.25 s) so bg vocals collapse quickly and feel snappy.
|
|
268
|
-
The scroll takes ~0.4 s; finishing the collapse first prevents
|
|
269
|
-
the container from trailing behind the motion. */
|
|
270
278
|
transition:
|
|
271
|
-
max-height
|
|
272
|
-
|
|
279
|
+
max-height var(--scroll-duration, 400ms)
|
|
280
|
+
cubic-bezier(0.41, 0, 0.12, 0.99),
|
|
281
|
+
opacity var(--scroll-duration, 400ms) cubic-bezier(0.41, 0, 0.12, 0.99);
|
|
273
282
|
margin: 0;
|
|
274
283
|
pointer-events: none;
|
|
275
284
|
}
|
|
@@ -278,7 +287,8 @@ export class AmLyrics extends LitElement {
|
|
|
278
287
|
display: block;
|
|
279
288
|
padding-top: 0;
|
|
280
289
|
padding-bottom: 0;
|
|
281
|
-
transition: padding-top
|
|
290
|
+
transition: padding-top var(--scroll-duration, 400ms)
|
|
291
|
+
cubic-bezier(0.41, 0, 0.12, 0.99);
|
|
282
292
|
}
|
|
283
293
|
|
|
284
294
|
.lyrics-line.singer-right .background-vocal-container,
|
|
@@ -293,16 +303,11 @@ export class AmLyrics extends LitElement {
|
|
|
293
303
|
.lyrics-line.bg-expanded .background-vocal-container {
|
|
294
304
|
max-height: 4em;
|
|
295
305
|
opacity: 1;
|
|
296
|
-
|
|
297
|
-
transition:
|
|
298
|
-
max-height 0.6s ease,
|
|
299
|
-
opacity 0.6s ease;
|
|
300
|
-
will-change: max-height, opacity;
|
|
306
|
+
will-change: opacity;
|
|
301
307
|
}
|
|
302
308
|
|
|
303
309
|
.lyrics-line.bg-expanded .background-vocal-wrap {
|
|
304
310
|
padding-top: 0.26em;
|
|
305
|
-
transition: padding-top 0.6s ease;
|
|
306
311
|
}
|
|
307
312
|
|
|
308
313
|
/* --- Line States & Modifiers --- */
|
|
@@ -450,11 +455,22 @@ export class AmLyrics extends LitElement {
|
|
|
450
455
|
white-space: nowrap;
|
|
451
456
|
}
|
|
452
457
|
|
|
458
|
+
.lyrics-word.char-drag {
|
|
459
|
+
display: inline-block;
|
|
460
|
+
vertical-align: baseline;
|
|
461
|
+
white-space: nowrap;
|
|
462
|
+
}
|
|
463
|
+
|
|
453
464
|
.lyrics-word.char-rise.allow-break {
|
|
454
465
|
display: inline;
|
|
455
466
|
white-space: normal;
|
|
456
467
|
}
|
|
457
468
|
|
|
469
|
+
.lyrics-word.char-drag.allow-break {
|
|
470
|
+
display: inline;
|
|
471
|
+
white-space: normal;
|
|
472
|
+
}
|
|
473
|
+
|
|
458
474
|
.lyrics-syllable-wrap {
|
|
459
475
|
display: inline;
|
|
460
476
|
}
|
|
@@ -509,44 +525,30 @@ export class AmLyrics extends LitElement {
|
|
|
509
525
|
.lyrics-line.active:not(.lyrics-gap)
|
|
510
526
|
.lyrics-syllable.pre-highlight.no-chars {
|
|
511
527
|
background-repeat: no-repeat;
|
|
512
|
-
background-image:
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
var(--
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
var(--lyplus-text-primary, #fff) 100%,
|
|
522
|
-
#0000 100%
|
|
523
|
-
);
|
|
524
|
-
background-size:
|
|
525
|
-
0.5em 100%,
|
|
526
|
-
0% 100%;
|
|
527
|
-
background-position:
|
|
528
|
-
-0.5em 0%,
|
|
529
|
-
-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;
|
|
530
537
|
}
|
|
531
538
|
|
|
532
539
|
.lyrics-line.active:not(.lyrics-gap) .lyrics-syllable.highlight.rtl-text,
|
|
533
540
|
.lyrics-line.active:not(.lyrics-gap)
|
|
534
541
|
.lyrics-syllable.pre-highlight.rtl-text {
|
|
535
542
|
direction: rtl;
|
|
536
|
-
background-image:
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
transparent 100%
|
|
546
|
-
);
|
|
547
|
-
background-position:
|
|
548
|
-
calc(100% + 0.5em) 0%,
|
|
549
|
-
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%;
|
|
550
552
|
}
|
|
551
553
|
|
|
552
554
|
/* Background vocals: muted gray wipe instead of white.
|
|
@@ -563,18 +565,13 @@ export class AmLyrics extends LitElement {
|
|
|
563
565
|
.lyrics-line.pre-active
|
|
564
566
|
.background-vocal-container
|
|
565
567
|
.lyrics-syllable.pre-highlight.no-chars {
|
|
566
|
-
background-image:
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
linear-gradient(
|
|
574
|
-
90deg,
|
|
575
|
-
color-mix(in srgb, var(--lyplus-text-primary, #fff) 50%, #888888) 100%,
|
|
576
|
-
#0000 100%
|
|
577
|
-
);
|
|
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
|
+
);
|
|
578
575
|
}
|
|
579
576
|
|
|
580
577
|
.lyrics-line.active
|
|
@@ -589,17 +586,13 @@ export class AmLyrics extends LitElement {
|
|
|
589
586
|
.lyrics-line.pre-active
|
|
590
587
|
.background-vocal-container
|
|
591
588
|
.lyrics-syllable.pre-highlight.rtl-text {
|
|
592
|
-
background-image:
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
-90deg,
|
|
600
|
-
color-mix(in srgb, var(--lyplus-text-primary) 50%, #888888) 100%,
|
|
601
|
-
transparent 100%
|
|
602
|
-
);
|
|
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
|
+
);
|
|
603
596
|
}
|
|
604
597
|
|
|
605
598
|
/* Non-growable words float up with a gentle curve */
|
|
@@ -623,6 +616,10 @@ export class AmLyrics extends LitElement {
|
|
|
623
616
|
transform: translate3d(0, var(--char-rise-y, -1.12px), 0);
|
|
624
617
|
}
|
|
625
618
|
|
|
619
|
+
.lyrics-word.char-drag .lyrics-syllable.cleanup .char {
|
|
620
|
+
transform: translate3d(0, var(--char-rise-y, -1.12px), 0);
|
|
621
|
+
}
|
|
622
|
+
|
|
626
623
|
.lyrics-line.persist-highlight
|
|
627
624
|
.lyrics-word.growable
|
|
628
625
|
.lyrics-syllable.finished
|
|
@@ -630,6 +627,10 @@ export class AmLyrics extends LitElement {
|
|
|
630
627
|
.lyrics-line.persist-highlight
|
|
631
628
|
.lyrics-word.char-rise
|
|
632
629
|
.lyrics-syllable.finished
|
|
630
|
+
.char,
|
|
631
|
+
.lyrics-line.persist-highlight
|
|
632
|
+
.lyrics-word.char-drag
|
|
633
|
+
.lyrics-syllable.finished
|
|
633
634
|
.char {
|
|
634
635
|
transform: translate3d(0, var(--char-rise-y, -1.12px), 0);
|
|
635
636
|
}
|
|
@@ -740,70 +741,56 @@ export class AmLyrics extends LitElement {
|
|
|
740
741
|
transform 0.7s ease;
|
|
741
742
|
}
|
|
742
743
|
|
|
744
|
+
.lyrics-word.char-drag span.char {
|
|
745
|
+
transition: color 0.18s;
|
|
746
|
+
}
|
|
747
|
+
|
|
743
748
|
/* Active char spans: structural only, wipe animation sets gradient */
|
|
744
749
|
.lyrics-line.active .lyrics-syllable span.char {
|
|
745
750
|
background-clip: text;
|
|
746
751
|
-webkit-background-clip: text;
|
|
747
752
|
background-repeat: no-repeat;
|
|
748
|
-
background-image:
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
var(--
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
var(--lyplus-text-primary, #fff) 100%,
|
|
758
|
-
#0000 100%
|
|
759
|
-
);
|
|
760
|
-
background-size:
|
|
761
|
-
0.5em 100%,
|
|
762
|
-
0% 100%;
|
|
763
|
-
background-position:
|
|
764
|
-
-0.5em 0%,
|
|
765
|
-
-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;
|
|
766
762
|
transition:
|
|
767
763
|
transform 0.7s ease,
|
|
768
764
|
color 0.18s;
|
|
769
765
|
}
|
|
770
766
|
|
|
771
767
|
.lyrics-line.active .lyrics-syllable span.char.highlight {
|
|
772
|
-
background-image:
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
background-image:
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
var(--
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
var(--lyplus-text-primary, #fff) 100%,
|
|
799
|
-
#0000 100%
|
|
800
|
-
);
|
|
801
|
-
background-size:
|
|
802
|
-
0.75em 100%,
|
|
803
|
-
0% 100%;
|
|
804
|
-
background-position:
|
|
805
|
-
-0.85em 0%,
|
|
806
|
-
-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%;
|
|
807
794
|
}
|
|
808
795
|
|
|
809
796
|
/* ==========================================================================
|
|
@@ -880,13 +867,7 @@ export class AmLyrics extends LitElement {
|
|
|
880
867
|
color: var(--lyplus-text-primary) !important;
|
|
881
868
|
}
|
|
882
869
|
|
|
883
|
-
.lyrics-line.
|
|
884
|
-
animation: fade-in-line 0.14s ease-out forwards !important;
|
|
885
|
-
color: var(--lyplus-text-primary) !important;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
.lyrics-line.active .lyrics-syllable.line-synced span.char,
|
|
889
|
-
.lyrics-line.pre-active .lyrics-syllable.line-synced span.char {
|
|
870
|
+
.lyrics-line.active .lyrics-syllable.line-synced span.char {
|
|
890
871
|
background-image: none !important;
|
|
891
872
|
background-color: var(--lyplus-text-primary) !important;
|
|
892
873
|
transition: background-color 120ms ease-out !important;
|
|
@@ -1059,6 +1040,49 @@ export class AmLyrics extends LitElement {
|
|
|
1059
1040
|
gap: 4px;
|
|
1060
1041
|
}
|
|
1061
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
|
+
|
|
1062
1086
|
.control-button {
|
|
1063
1087
|
background: transparent;
|
|
1064
1088
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
@@ -1067,7 +1091,10 @@ export class AmLyrics extends LitElement {
|
|
|
1067
1091
|
font-size: 0.8em;
|
|
1068
1092
|
color: rgba(255, 255, 255, 0.6);
|
|
1069
1093
|
cursor: pointer;
|
|
1070
|
-
transition:
|
|
1094
|
+
transition:
|
|
1095
|
+
color 0.2s,
|
|
1096
|
+
border-color 0.2s,
|
|
1097
|
+
background-color 0.2s;
|
|
1071
1098
|
font-weight: normal;
|
|
1072
1099
|
}
|
|
1073
1100
|
|
|
@@ -1205,137 +1232,125 @@ export class AmLyrics extends LitElement {
|
|
|
1205
1232
|
KEYFRAME ANIMATIONS
|
|
1206
1233
|
========================================================================== */
|
|
1207
1234
|
|
|
1235
|
+
@keyframes source-switch-spin {
|
|
1236
|
+
to {
|
|
1237
|
+
transform: rotate(360deg);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1208
1241
|
/* Wipe animation for syllables */
|
|
1209
1242
|
@keyframes wipe {
|
|
1210
1243
|
from {
|
|
1211
|
-
background-size:
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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;
|
|
1217
1257
|
}
|
|
1218
1258
|
to {
|
|
1219
|
-
background-size:
|
|
1220
|
-
|
|
1221
|
-
100% 100%;
|
|
1222
|
-
background-position:
|
|
1223
|
-
calc(100% + 0.375em) 0%,
|
|
1224
|
-
left;
|
|
1259
|
+
background-size: calc(100% + var(--wipe-gradient-width, 0.75em)) 100%;
|
|
1260
|
+
background-position: left;
|
|
1225
1261
|
}
|
|
1226
1262
|
}
|
|
1227
1263
|
|
|
1228
1264
|
@keyframes start-wipe {
|
|
1229
1265
|
0% {
|
|
1230
|
-
background-size:
|
|
1231
|
-
|
|
1232
|
-
0% 100%;
|
|
1233
|
-
background-position:
|
|
1234
|
-
-0.75em 0%,
|
|
1235
|
-
-0.375em 0%;
|
|
1266
|
+
background-size: 0% 100%;
|
|
1267
|
+
background-position: left;
|
|
1236
1268
|
}
|
|
1237
1269
|
100% {
|
|
1238
|
-
background-size:
|
|
1239
|
-
|
|
1240
|
-
100% 100%;
|
|
1241
|
-
background-position:
|
|
1242
|
-
calc(100% + 0.375em) 0%,
|
|
1243
|
-
left;
|
|
1270
|
+
background-size: calc(100% + var(--wipe-gradient-width, 0.75em)) 100%;
|
|
1271
|
+
background-position: left;
|
|
1244
1272
|
}
|
|
1245
1273
|
}
|
|
1246
1274
|
|
|
1247
1275
|
@keyframes wipe-rtl {
|
|
1248
1276
|
from {
|
|
1249
|
-
background-size:
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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%;
|
|
1255
1290
|
}
|
|
1256
1291
|
to {
|
|
1257
|
-
background-size:
|
|
1258
|
-
|
|
1259
|
-
100% 100%;
|
|
1260
|
-
background-position:
|
|
1261
|
-
-0.75em 0%,
|
|
1262
|
-
right 0%;
|
|
1292
|
+
background-size: calc(100% + var(--wipe-gradient-width, 0.75em)) 100%;
|
|
1293
|
+
background-position: right 0%;
|
|
1263
1294
|
}
|
|
1264
1295
|
}
|
|
1265
1296
|
|
|
1266
1297
|
@keyframes start-wipe-rtl {
|
|
1267
1298
|
0% {
|
|
1268
|
-
background-size:
|
|
1269
|
-
|
|
1270
|
-
0% 100%;
|
|
1271
|
-
background-position:
|
|
1272
|
-
calc(100% + 0.75em) 0%,
|
|
1273
|
-
calc(100% + 0.5em) 0%;
|
|
1299
|
+
background-size: 0% 100%;
|
|
1300
|
+
background-position: right 0%;
|
|
1274
1301
|
}
|
|
1275
1302
|
100% {
|
|
1276
|
-
background-size:
|
|
1277
|
-
|
|
1278
|
-
100% 100%;
|
|
1279
|
-
background-position:
|
|
1280
|
-
-0.75em 0%,
|
|
1281
|
-
right 0%;
|
|
1303
|
+
background-size: calc(100% + var(--wipe-gradient-width, 0.75em)) 100%;
|
|
1304
|
+
background-position: right 0%;
|
|
1282
1305
|
}
|
|
1283
1306
|
}
|
|
1284
1307
|
|
|
1285
1308
|
@keyframes pre-wipe-universal {
|
|
1286
1309
|
from {
|
|
1287
|
-
background-size:
|
|
1288
|
-
|
|
1289
|
-
0% 100%;
|
|
1290
|
-
background-position:
|
|
1291
|
-
-0.75em 0%,
|
|
1292
|
-
left;
|
|
1310
|
+
background-size: 0% 100%;
|
|
1311
|
+
background-position: left;
|
|
1293
1312
|
}
|
|
1294
1313
|
to {
|
|
1295
|
-
background-size:
|
|
1296
|
-
|
|
1297
|
-
0% 100%;
|
|
1298
|
-
background-position:
|
|
1299
|
-
-0.375em 0%,
|
|
1300
|
-
left;
|
|
1314
|
+
background-size: var(--wipe-gradient-width, 0.75em) 100%;
|
|
1315
|
+
background-position: left;
|
|
1301
1316
|
}
|
|
1302
1317
|
}
|
|
1303
1318
|
|
|
1304
1319
|
@keyframes pre-wipe-universal-rtl {
|
|
1305
1320
|
from {
|
|
1306
|
-
background-size:
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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%;
|
|
1312
1334
|
}
|
|
1313
1335
|
to {
|
|
1314
|
-
background-size:
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
background-position:
|
|
1318
|
-
calc(100% + 0.375em) 0%,
|
|
1319
|
-
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%;
|
|
1320
1339
|
}
|
|
1321
1340
|
}
|
|
1322
1341
|
|
|
1323
|
-
@keyframes
|
|
1342
|
+
@keyframes wipe-word-from-pre {
|
|
1324
1343
|
from {
|
|
1325
|
-
background-size:
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
background-position:
|
|
1329
|
-
-0.75em 0%,
|
|
1330
|
-
left;
|
|
1344
|
+
background-size: var(--pre-wipe-word-size, var(--wipe-gradient-width))
|
|
1345
|
+
100%;
|
|
1346
|
+
background-position: var(--char-wipe-position, left) 0%;
|
|
1331
1347
|
}
|
|
1332
1348
|
to {
|
|
1333
|
-
background-size:
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
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%;
|
|
1339
1354
|
}
|
|
1340
1355
|
}
|
|
1341
1356
|
|
|
@@ -1429,6 +1444,15 @@ export class AmLyrics extends LitElement {
|
|
|
1429
1444
|
}
|
|
1430
1445
|
}
|
|
1431
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
|
+
|
|
1432
1456
|
@keyframes grow-static {
|
|
1433
1457
|
0%,
|
|
1434
1458
|
100% {
|
|
@@ -1674,6 +1698,7 @@ export class AmLyrics extends LitElement {
|
|
|
1674
1698
|
this.preActiveLineElements = [];
|
|
1675
1699
|
this.positionedLineElements = [];
|
|
1676
1700
|
this.activeGapLineElements = [];
|
|
1701
|
+
this.clearBackgroundExpandedLine();
|
|
1677
1702
|
|
|
1678
1703
|
// Stop all running animations and clear highlights immediately
|
|
1679
1704
|
if (this.lyricsContainer) {
|
|
@@ -1746,10 +1771,7 @@ export class AmLyrics extends LitElement {
|
|
|
1746
1771
|
(switchBtn as HTMLButtonElement).disabled = this.isFetchingAlternatives;
|
|
1747
1772
|
}
|
|
1748
1773
|
if (svgEl) {
|
|
1749
|
-
svgEl.
|
|
1750
|
-
'style',
|
|
1751
|
-
`margin-right: 4px; ${this.isFetchingAlternatives ? 'animation: spin 1s linear infinite;' : ''}`,
|
|
1752
|
-
);
|
|
1774
|
+
svgEl.classList.toggle('is-loading', this.isFetchingAlternatives);
|
|
1753
1775
|
}
|
|
1754
1776
|
if (labelEl) {
|
|
1755
1777
|
labelEl.textContent = this.isFetchingAlternatives
|
|
@@ -1812,6 +1834,7 @@ export class AmLyrics extends LitElement {
|
|
|
1812
1834
|
groupGrowable: boolean[];
|
|
1813
1835
|
groupGlowing: boolean[];
|
|
1814
1836
|
groupCharRise: boolean[];
|
|
1837
|
+
groupCharDrag: boolean[];
|
|
1815
1838
|
vwFullText: string[];
|
|
1816
1839
|
vwFullDuration: number[];
|
|
1817
1840
|
vwCharOffset: number[];
|
|
@@ -1827,6 +1850,8 @@ export class AmLyrics extends LitElement {
|
|
|
1827
1850
|
|
|
1828
1851
|
private lastPrimaryActiveLine: HTMLElement | null = null;
|
|
1829
1852
|
|
|
1853
|
+
private backgroundExpandedLine: HTMLElement | null = null;
|
|
1854
|
+
|
|
1830
1855
|
// Scroll animation state
|
|
1831
1856
|
private scrollAnimationState: {
|
|
1832
1857
|
isAnimating: boolean;
|
|
@@ -2082,6 +2107,7 @@ export class AmLyrics extends LitElement {
|
|
|
2082
2107
|
this.preActiveLineElements = [];
|
|
2083
2108
|
this.positionedLineElements = [];
|
|
2084
2109
|
this.activeGapLineElements = [];
|
|
2110
|
+
this.clearBackgroundExpandedLine();
|
|
2085
2111
|
|
|
2086
2112
|
if (this.lyricsContainer) {
|
|
2087
2113
|
this.isProgrammaticScroll = true;
|
|
@@ -2146,20 +2172,26 @@ export class AmLyrics extends LitElement {
|
|
|
2146
2172
|
return 30;
|
|
2147
2173
|
}
|
|
2148
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
|
+
|
|
2149
2188
|
private static mergeAndSortSources(
|
|
2150
|
-
collectedSources:
|
|
2151
|
-
):
|
|
2152
|
-
const uniqueSourcesMap = new Map<
|
|
2153
|
-
string,
|
|
2154
|
-
{ lines: LyricsLine[]; source: string }
|
|
2155
|
-
>();
|
|
2189
|
+
collectedSources: YouLyPlusLyricsResult[],
|
|
2190
|
+
): YouLyPlusLyricsResult[] {
|
|
2191
|
+
const uniqueSourcesMap = new Map<string, YouLyPlusLyricsResult>();
|
|
2156
2192
|
|
|
2157
2193
|
for (const source of collectedSources) {
|
|
2158
|
-
const normalizedSource = source.source
|
|
2159
|
-
.toLowerCase()
|
|
2160
|
-
.includes('lyricsplus')
|
|
2161
|
-
? 'QQ'
|
|
2162
|
-
: source.source;
|
|
2194
|
+
const normalizedSource = AmLyrics.getDisplaySourceLabel(source.source);
|
|
2163
2195
|
|
|
2164
2196
|
if (!uniqueSourcesMap.has(normalizedSource)) {
|
|
2165
2197
|
uniqueSourcesMap.set(normalizedSource, {
|
|
@@ -2176,9 +2208,63 @@ export class AmLyrics extends LitElement {
|
|
|
2176
2208
|
);
|
|
2177
2209
|
}
|
|
2178
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
|
+
|
|
2179
2262
|
private async switchSource() {
|
|
2180
2263
|
if (this.isFetchingAlternatives) return;
|
|
2181
2264
|
|
|
2265
|
+
const currentSourceLabel = this.lyricsSource;
|
|
2266
|
+
const currentLines = this.lyrics;
|
|
2267
|
+
|
|
2182
2268
|
if (!this.hasFetchedAllProviders) {
|
|
2183
2269
|
this.isFetchingAlternatives = true;
|
|
2184
2270
|
this._updateFooter();
|
|
@@ -2255,11 +2341,13 @@ export class AmLyrics extends LitElement {
|
|
|
2255
2341
|
...this.availableSources,
|
|
2256
2342
|
...newSources,
|
|
2257
2343
|
]);
|
|
2258
|
-
// Re-sync current index since sorting
|
|
2259
|
-
|
|
2260
|
-
|
|
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,
|
|
2261
2350
|
);
|
|
2262
|
-
if (this.currentSourceIndex === -1) this.currentSourceIndex = 0;
|
|
2263
2351
|
}
|
|
2264
2352
|
}
|
|
2265
2353
|
} finally {
|
|
@@ -2270,15 +2358,20 @@ export class AmLyrics extends LitElement {
|
|
|
2270
2358
|
}
|
|
2271
2359
|
|
|
2272
2360
|
if (this.availableSources.length > 1) {
|
|
2273
|
-
this.
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
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);
|
|
2280
2374
|
}
|
|
2281
|
-
await this.onLyricsLoaded();
|
|
2282
2375
|
}
|
|
2283
2376
|
}
|
|
2284
2377
|
|
|
@@ -3544,7 +3637,11 @@ export class AmLyrics extends LitElement {
|
|
|
3544
3637
|
if (!newActiveLines.includes(lineIndex)) {
|
|
3545
3638
|
const lineElement = this._getLineElement(lineIndex);
|
|
3546
3639
|
if (lineElement) {
|
|
3547
|
-
if (
|
|
3640
|
+
if (
|
|
3641
|
+
isSeek ||
|
|
3642
|
+
this.isUserScrolling ||
|
|
3643
|
+
AmLyrics.isLineSyncedLine(this.lyrics?.[lineIndex])
|
|
3644
|
+
) {
|
|
3548
3645
|
AmLyrics.unfinishSyllables(lineElement);
|
|
3549
3646
|
} else {
|
|
3550
3647
|
AmLyrics.finishSyllablesUpToTime(lineElement, newTime);
|
|
@@ -3561,12 +3658,13 @@ export class AmLyrics extends LitElement {
|
|
|
3561
3658
|
}
|
|
3562
3659
|
}
|
|
3563
3660
|
|
|
3564
|
-
// Add 'active'
|
|
3661
|
+
// Add 'active' to newly active lines. Background expansion is driven
|
|
3662
|
+
// separately by the current scroll target.
|
|
3565
3663
|
for (const lineIndex of newActiveLines) {
|
|
3566
3664
|
if (!oldActiveLines.includes(lineIndex)) {
|
|
3567
3665
|
const lineElement = this._getLineElement(lineIndex);
|
|
3568
3666
|
if (lineElement) {
|
|
3569
|
-
lineElement.classList.add('active'
|
|
3667
|
+
lineElement.classList.add('active');
|
|
3570
3668
|
lineElement.classList.remove('pre-active');
|
|
3571
3669
|
const preIdx = this.preActiveLineElements.indexOf(lineElement);
|
|
3572
3670
|
if (preIdx !== -1) this.preActiveLineElements.splice(preIdx, 1);
|
|
@@ -3830,8 +3928,14 @@ export class AmLyrics extends LitElement {
|
|
|
3830
3928
|
const activeLines = this.findActiveLineIndices(this.currentTime);
|
|
3831
3929
|
for (const lineIndex of activeLines) {
|
|
3832
3930
|
const lineEl = this._getLineElement(lineIndex);
|
|
3833
|
-
if (lineEl) lineEl.classList.add('active'
|
|
3931
|
+
if (lineEl) lineEl.classList.add('active');
|
|
3834
3932
|
}
|
|
3933
|
+
const primaryActiveLine = this.getPrimaryActiveLineIndex(activeLines);
|
|
3934
|
+
this.setBackgroundExpandedLine(
|
|
3935
|
+
primaryActiveLine !== null
|
|
3936
|
+
? this._getLineElement(primaryActiveLine)
|
|
3937
|
+
: null,
|
|
3938
|
+
);
|
|
3835
3939
|
|
|
3836
3940
|
// Trigger a faux time-change so that updateSyllablesForLine fires
|
|
3837
3941
|
// to setup inline syllable CSS wipe animations for whatever the current time is
|
|
@@ -3878,6 +3982,7 @@ export class AmLyrics extends LitElement {
|
|
|
3878
3982
|
this.preActiveLineElements = [];
|
|
3879
3983
|
this.positionedLineElements = [];
|
|
3880
3984
|
this.activeGapLineElements = [];
|
|
3985
|
+
this.clearBackgroundExpandedLine();
|
|
3881
3986
|
this.setUserScrolling(false);
|
|
3882
3987
|
|
|
3883
3988
|
// Cancel any running animations
|
|
@@ -3949,6 +4054,7 @@ export class AmLyrics extends LitElement {
|
|
|
3949
4054
|
// Don't override it with a scroll back to the last lyric.
|
|
3950
4055
|
const footer = this.lyricsContainer.querySelector('.lyrics-footer');
|
|
3951
4056
|
if (footer?.classList.contains('active')) {
|
|
4057
|
+
this.setBackgroundExpandedLine(null);
|
|
3952
4058
|
return;
|
|
3953
4059
|
}
|
|
3954
4060
|
|
|
@@ -4000,10 +4106,17 @@ export class AmLyrics extends LitElement {
|
|
|
4000
4106
|
targetElement = this._getLineElement(targetLineIdx);
|
|
4001
4107
|
}
|
|
4002
4108
|
}
|
|
4003
|
-
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);
|
|
4004
4117
|
|
|
4005
|
-
// Unblur the upcoming target line early
|
|
4006
|
-
//
|
|
4118
|
+
// Unblur the upcoming target line early while the separate bg-expanded
|
|
4119
|
+
// class starts background vocal height/opacity in sync with scroll.
|
|
4007
4120
|
if (!targetElement.classList.contains('active')) {
|
|
4008
4121
|
targetElement.classList.add('pre-active');
|
|
4009
4122
|
if (!this.preActiveLineElements.includes(targetElement)) {
|
|
@@ -4011,8 +4124,6 @@ export class AmLyrics extends LitElement {
|
|
|
4011
4124
|
}
|
|
4012
4125
|
}
|
|
4013
4126
|
|
|
4014
|
-
const scrollDuration = scrollLookAheadMs;
|
|
4015
|
-
|
|
4016
4127
|
this.focusLine(targetElement, forceScroll, scrollDuration);
|
|
4017
4128
|
}
|
|
4018
4129
|
|
|
@@ -4102,6 +4213,7 @@ export class AmLyrics extends LitElement {
|
|
|
4102
4213
|
this.preActiveLineElements = [];
|
|
4103
4214
|
this.positionedLineElements = [];
|
|
4104
4215
|
this.activeGapLineElements = [];
|
|
4216
|
+
this.clearBackgroundExpandedLine();
|
|
4105
4217
|
this.visibilityObserver?.disconnect();
|
|
4106
4218
|
this.visibilityObserver = undefined;
|
|
4107
4219
|
}
|
|
@@ -4145,6 +4257,7 @@ export class AmLyrics extends LitElement {
|
|
|
4145
4257
|
const groupGrowable: boolean[] = new Array(wordGroups.length).fill(false);
|
|
4146
4258
|
const groupGlowing: boolean[] = new Array(wordGroups.length).fill(false);
|
|
4147
4259
|
const groupCharRise: boolean[] = new Array(wordGroups.length).fill(false);
|
|
4260
|
+
const groupCharDrag: boolean[] = new Array(wordGroups.length).fill(false);
|
|
4148
4261
|
const vwFullText: string[] = new Array(wordGroups.length).fill('');
|
|
4149
4262
|
const vwFullDuration: number[] = new Array(wordGroups.length).fill(0);
|
|
4150
4263
|
const vwCharOffset: number[] = new Array(wordGroups.length).fill(0);
|
|
@@ -4184,29 +4297,53 @@ export class AmLyrics extends LitElement {
|
|
|
4184
4297
|
const hasHyphen = combinedText.includes('-');
|
|
4185
4298
|
|
|
4186
4299
|
const wordLen = combinedText.length;
|
|
4187
|
-
|
|
4188
|
-
|
|
4300
|
+
const canAnimateByChar = !isCJK && !isRTL && !hasHyphen && wordLen > 0;
|
|
4301
|
+
const isLineSynced =
|
|
4302
|
+
line.isWordSynced === false || line.text.some(s => s.lineSynced);
|
|
4303
|
+
let isGrowableVW = canAnimateByChar && wordLen > 0 && wordLen <= 7;
|
|
4189
4304
|
if (isGrowableVW) {
|
|
4190
|
-
if (wordLen
|
|
4305
|
+
if (wordLen <= 1) {
|
|
4191
4306
|
isGrowableVW =
|
|
4192
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;
|
|
4193
4312
|
} else {
|
|
4194
4313
|
isGrowableVW =
|
|
4195
4314
|
combinedDuration >= 850 && combinedDuration >= wordLen * 190;
|
|
4196
4315
|
}
|
|
4197
4316
|
}
|
|
4198
4317
|
|
|
4199
|
-
const
|
|
4200
|
-
|
|
4201
|
-
const
|
|
4318
|
+
const hasCharRiseDuration =
|
|
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);
|
|
4325
|
+
const hasLongShortWordDuration =
|
|
4326
|
+
wordLen >= 4 && combinedDuration >= Math.max(1300, wordLen * 260);
|
|
4202
4327
|
const isCharRiseVW =
|
|
4203
|
-
|
|
4328
|
+
canAnimateByChar &&
|
|
4329
|
+
!isLineSynced &&
|
|
4330
|
+
!isGrowableVW &&
|
|
4331
|
+
((wordLen >= 8 && hasCharRiseDuration) ||
|
|
4332
|
+
(wordLen < 8 && hasLongShortWordDuration));
|
|
4333
|
+
const isCharDragVW =
|
|
4334
|
+
canAnimateByChar &&
|
|
4335
|
+
!isLineSynced &&
|
|
4336
|
+
!isGrowableVW &&
|
|
4337
|
+
hasTinyWordDragDuration;
|
|
4338
|
+
|
|
4339
|
+
const isGlowingVW = isGrowableVW && !isLineSynced;
|
|
4204
4340
|
|
|
4205
4341
|
let charOff = 0;
|
|
4206
4342
|
for (let gi = vwStart; gi <= vwEnd; gi += 1) {
|
|
4207
4343
|
groupGrowable[gi] = isGrowableVW;
|
|
4208
4344
|
groupGlowing[gi] = isGlowingVW;
|
|
4209
4345
|
groupCharRise[gi] = isCharRiseVW;
|
|
4346
|
+
groupCharDrag[gi] = isCharDragVW;
|
|
4210
4347
|
vwFullText[gi] = combinedText;
|
|
4211
4348
|
vwFullDuration[gi] = combinedDuration;
|
|
4212
4349
|
vwCharOffset[gi] = charOff;
|
|
@@ -4224,6 +4361,7 @@ export class AmLyrics extends LitElement {
|
|
|
4224
4361
|
groupGrowable,
|
|
4225
4362
|
groupGlowing,
|
|
4226
4363
|
groupCharRise,
|
|
4364
|
+
groupCharDrag,
|
|
4227
4365
|
vwFullText,
|
|
4228
4366
|
vwFullDuration,
|
|
4229
4367
|
vwCharOffset,
|
|
@@ -4245,62 +4383,135 @@ export class AmLyrics extends LitElement {
|
|
|
4245
4383
|
|
|
4246
4384
|
const computedStyle = getComputedStyle(referenceSyllable);
|
|
4247
4385
|
const { font } = computedStyle; // Full font string
|
|
4248
|
-
const fontSize = parseFloat(computedStyle.fontSize);
|
|
4249
|
-
|
|
4250
|
-
const charTimedWords = this.shadowRoot.querySelectorAll(
|
|
4251
|
-
'.lyrics-word.growable, .lyrics-word.char-rise',
|
|
4252
|
-
);
|
|
4253
|
-
if (!charTimedWords) return;
|
|
4254
4386
|
|
|
4255
|
-
charTimedWords.
|
|
4256
|
-
|
|
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
|
+
});
|
|
4257
4404
|
|
|
4258
|
-
|
|
4405
|
+
wordsByVirtualId.forEach(wordSpans => {
|
|
4259
4406
|
const syllables: HTMLElement[] = [];
|
|
4260
|
-
|
|
4261
|
-
const
|
|
4262
|
-
|
|
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
|
+
});
|
|
4263
4415
|
});
|
|
4264
4416
|
|
|
4265
|
-
syllables.
|
|
4266
|
-
const
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
const duration = parseFloat(sylSpan.dataset.duration || '0');
|
|
4276
|
-
const velocityPxPerMs =
|
|
4277
|
-
duration > 0 ? totalSyllableWidth / duration : 0;
|
|
4278
|
-
|
|
4279
|
-
// Gradient width in pixels = 0.375 * fontSize
|
|
4280
|
-
// This matches YouLyPlus visual gradient size
|
|
4281
|
-
const gradientWidthPx = 0.375 * fontSize;
|
|
4282
|
-
const gradientDurationMs =
|
|
4283
|
-
velocityPxPerMs > 0 ? gradientWidthPx / velocityPxPerMs : 100;
|
|
4284
|
-
|
|
4285
|
-
let cumulativeCharWidth = 0;
|
|
4286
|
-
|
|
4287
|
-
charSpans.forEach((spanArg: any, i: number) => {
|
|
4288
|
-
const charWidth = charWidths[i];
|
|
4289
|
-
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;
|
|
4290
4426
|
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4427
|
+
wordSpans.forEach(wordSpan => {
|
|
4428
|
+
const target = wordSpan as any;
|
|
4429
|
+
target._cachedVirtualWordElements = wordSpans;
|
|
4430
|
+
target._cachedVirtualWordCharSpans = charSpans;
|
|
4431
|
+
});
|
|
4294
4432
|
|
|
4295
|
-
|
|
4296
|
-
|
|
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;
|
|
4297
4453
|
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
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
|
+
);
|
|
4301
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
|
+
);
|
|
4302
4512
|
|
|
4303
4513
|
cumulativeCharWidth += charWidth;
|
|
4514
|
+
cumulativeSyllableWidth += charWidth;
|
|
4304
4515
|
});
|
|
4305
4516
|
});
|
|
4306
4517
|
});
|
|
@@ -4310,6 +4521,37 @@ export class AmLyrics extends LitElement {
|
|
|
4310
4521
|
return a.length === b.length && a.every((val, i) => val === b[i]);
|
|
4311
4522
|
}
|
|
4312
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
|
+
|
|
4313
4555
|
private static getLineIndexFromElement(
|
|
4314
4556
|
lineElement: HTMLElement | null,
|
|
4315
4557
|
): number | null {
|
|
@@ -4347,6 +4589,31 @@ export class AmLyrics extends LitElement {
|
|
|
4347
4589
|
this.preActiveLineElements = keptLines;
|
|
4348
4590
|
}
|
|
4349
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
|
+
|
|
4350
4617
|
private getPrimaryActiveLineIndex(activeIndices: number[]): number | null {
|
|
4351
4618
|
if (activeIndices.length === 0) return null;
|
|
4352
4619
|
|
|
@@ -4590,9 +4857,10 @@ export class AmLyrics extends LitElement {
|
|
|
4590
4857
|
|
|
4591
4858
|
for (let i = 0; i < this.lyrics.length; i += 1) {
|
|
4592
4859
|
const line = this.lyrics[i];
|
|
4860
|
+
const highlightEndTime = this.getLineHighlightEndTime(i);
|
|
4593
4861
|
|
|
4594
4862
|
if (line.timestamp > time) break;
|
|
4595
|
-
if (time >= line.timestamp && time <
|
|
4863
|
+
if (time >= line.timestamp && time < highlightEndTime) {
|
|
4596
4864
|
activeLines.push(i);
|
|
4597
4865
|
}
|
|
4598
4866
|
}
|
|
@@ -4859,6 +5127,7 @@ export class AmLyrics extends LitElement {
|
|
|
4859
5127
|
this.lastPrimaryActiveLine = null;
|
|
4860
5128
|
this.activeLineIds.clear();
|
|
4861
5129
|
this.animatingLines = [];
|
|
5130
|
+
this.setBackgroundExpandedLine(null);
|
|
4862
5131
|
|
|
4863
5132
|
// Find the clicked line element and scroll to it with forceScroll (like YouLyPlus)
|
|
4864
5133
|
// Timestamps are already in milliseconds — match the data-start-time attribute directly
|
|
@@ -4881,6 +5150,7 @@ export class AmLyrics extends LitElement {
|
|
|
4881
5150
|
}, 800);
|
|
4882
5151
|
|
|
4883
5152
|
this.scrollToActiveLineYouLy(clickedLineElement, true);
|
|
5153
|
+
this.setBackgroundExpandedLine(clickedLineElement);
|
|
4884
5154
|
}
|
|
4885
5155
|
|
|
4886
5156
|
const event = new CustomEvent('line-click', {
|
|
@@ -5319,8 +5589,389 @@ export class AmLyrics extends LitElement {
|
|
|
5319
5589
|
|
|
5320
5590
|
/**
|
|
5321
5591
|
* Update syllable highlight animation - apply CSS wipe animation
|
|
5322
|
-
* (Exact copy from YouLyPlus _updateSyllableAnimation)
|
|
5323
5592
|
*/
|
|
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(
|
|
5733
|
+
syllable: HTMLElement,
|
|
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,
|
|
5779
|
+
): void {
|
|
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;
|
|
5801
|
+
|
|
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) {
|
|
5935
|
+
wordElements = Array.from(
|
|
5936
|
+
wordElement.parentElement.querySelectorAll('.lyrics-word'),
|
|
5937
|
+
).filter(
|
|
5938
|
+
el => (el as HTMLElement).dataset.virtualWordId === virtualWordId,
|
|
5939
|
+
) as HTMLElement[];
|
|
5940
|
+
}
|
|
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(
|
|
5963
|
+
word => Array.from(word.querySelectorAll('span.char')) as HTMLElement[],
|
|
5964
|
+
);
|
|
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
|
+
|
|
5324
5975
|
private static updateSyllableAnimation(
|
|
5325
5976
|
syllable: HTMLElement,
|
|
5326
5977
|
elapsedTimeMs = 0,
|
|
@@ -5328,18 +5979,32 @@ export class AmLyrics extends LitElement {
|
|
|
5328
5979
|
if (syllable.classList.contains('highlight')) return;
|
|
5329
5980
|
|
|
5330
5981
|
const { classList } = syllable;
|
|
5982
|
+
const hadPreHighlight = classList.contains('pre-highlight');
|
|
5331
5983
|
const isRTL = classList.contains('rtl-text');
|
|
5332
|
-
const charSpans =
|
|
5333
|
-
syllable.querySelectorAll('span.char'),
|
|
5334
|
-
) as HTMLElement[];
|
|
5984
|
+
const charSpans = AmLyrics.getCachedCharSpans(syllable);
|
|
5335
5985
|
const wordElement = syllable.parentElement?.parentElement; // syllable-wrap -> word
|
|
5336
|
-
const
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
const
|
|
5340
|
-
|
|
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');
|
|
5341
5996
|
const isFirstSyllable =
|
|
5342
5997
|
syllable.getAttribute('data-syllable-index') === '0';
|
|
5998
|
+
const syllableStartMs = parseFloat(
|
|
5999
|
+
syllable.getAttribute('data-start-time') || '0',
|
|
6000
|
+
);
|
|
6001
|
+
const virtualWordStartMs = parseFloat(
|
|
6002
|
+
typedWordElement?.dataset.virtualWordStart || '',
|
|
6003
|
+
);
|
|
6004
|
+
const isFirstInVirtualWord =
|
|
6005
|
+
isFirstSyllable &&
|
|
6006
|
+
(!Number.isFinite(virtualWordStartMs) ||
|
|
6007
|
+
Math.abs(syllableStartMs - virtualWordStartMs) < 0.5);
|
|
5343
6008
|
const isFirstInContainer = isFirstSyllable; // Simplified
|
|
5344
6009
|
const isGap = syllable.closest('.lyrics-gap') !== null;
|
|
5345
6010
|
|
|
@@ -5352,8 +6017,11 @@ export class AmLyrics extends LitElement {
|
|
|
5352
6017
|
syllable.getAttribute('data-duration') ||
|
|
5353
6018
|
'0',
|
|
5354
6019
|
) || syllableDurationMs;
|
|
6020
|
+
const wordElapsedTimeMs = Number.isFinite(virtualWordStartMs)
|
|
6021
|
+
? elapsedTimeMs + (syllableStartMs - virtualWordStartMs)
|
|
6022
|
+
: elapsedTimeMs;
|
|
6023
|
+
const charWipeDurationMs = Math.max(wordDurationMs, syllableDurationMs);
|
|
5355
6024
|
|
|
5356
|
-
// Use a Map to collect animations like YouLyPlus
|
|
5357
6025
|
const charAnimationsMap = new Map<HTMLElement, string>();
|
|
5358
6026
|
const styleUpdates: Array<{
|
|
5359
6027
|
element: HTMLElement;
|
|
@@ -5362,7 +6030,7 @@ export class AmLyrics extends LitElement {
|
|
|
5362
6030
|
}> = [];
|
|
5363
6031
|
|
|
5364
6032
|
// Step 1: Grow Pass
|
|
5365
|
-
if (isGrowable &&
|
|
6033
|
+
if (isGrowable && isFirstInVirtualWord && allWordCharSpans.length > 0) {
|
|
5366
6034
|
const finalDuration = wordDurationMs;
|
|
5367
6035
|
const baseDelayPerChar = finalDuration * 0.09;
|
|
5368
6036
|
const growDurationMs = finalDuration * 1.5;
|
|
@@ -5406,7 +6074,7 @@ export class AmLyrics extends LitElement {
|
|
|
5406
6074
|
});
|
|
5407
6075
|
}
|
|
5408
6076
|
|
|
5409
|
-
if (isCharRise &&
|
|
6077
|
+
if (isCharRise && isFirstInVirtualWord && allWordCharSpans.length > 0) {
|
|
5410
6078
|
const finalDuration = Math.max(wordDurationMs, syllableDurationMs);
|
|
5411
6079
|
const baseDelayPerChar = finalDuration * 0.09;
|
|
5412
6080
|
const riseDurationMs = finalDuration * 1.5;
|
|
@@ -5421,18 +6089,82 @@ export class AmLyrics extends LitElement {
|
|
|
5421
6089
|
});
|
|
5422
6090
|
}
|
|
5423
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
|
+
|
|
5424
6108
|
// Step 2: Wipe Pass
|
|
5425
6109
|
if (charSpans.length > 0) {
|
|
5426
|
-
|
|
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) => {
|
|
5427
6141
|
const startPct = parseFloat(span.dataset.wipeStart || '0');
|
|
5428
6142
|
const durationPct = parseFloat(span.dataset.wipeDuration || '0');
|
|
6143
|
+
const globalCharIndex = parseFloat(
|
|
6144
|
+
span.dataset.syllableCharIndex || `${charIndex}`,
|
|
6145
|
+
);
|
|
5429
6146
|
|
|
5430
|
-
const
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
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;
|
|
5434
6162
|
let charWipeAnimation = 'wipe';
|
|
5435
|
-
if (
|
|
6163
|
+
if (hadCharPreWipe) {
|
|
6164
|
+
charWipeAnimation = 'wipe-word-from-pre';
|
|
6165
|
+
wipeDelay = -wordElapsedTimeMs;
|
|
6166
|
+
wipeDuration = charWipeDurationMs;
|
|
6167
|
+
} else if (useStartAnimation) {
|
|
5436
6168
|
charWipeAnimation = isRTL ? 'start-wipe-rtl' : 'start-wipe';
|
|
5437
6169
|
} else {
|
|
5438
6170
|
charWipeAnimation = isRTL ? 'wipe-rtl' : 'wipe';
|
|
@@ -5446,30 +6178,15 @@ export class AmLyrics extends LitElement {
|
|
|
5446
6178
|
if (
|
|
5447
6179
|
existingAnimation &&
|
|
5448
6180
|
(existingAnimation.includes('grow-dynamic') ||
|
|
5449
|
-
existingAnimation.includes('rise-char')
|
|
6181
|
+
existingAnimation.includes('rise-char') ||
|
|
6182
|
+
existingAnimation.includes('drag-char'))
|
|
5450
6183
|
) {
|
|
5451
6184
|
animationParts.push(existingAnimation.split(',')[0].trim());
|
|
5452
6185
|
}
|
|
5453
|
-
if (charIndex > 0) {
|
|
5454
|
-
const arrivalTime =
|
|
5455
|
-
(span.dataset.preWipeArrival
|
|
5456
|
-
? parseFloat(span.dataset.preWipeArrival)
|
|
5457
|
-
: syllableDurationMs * startPct) - elapsedTimeMs;
|
|
5458
|
-
const constantDuration = parseFloat(
|
|
5459
|
-
span.dataset.preWipeDuration || '100',
|
|
5460
|
-
);
|
|
5461
|
-
const animDelay = arrivalTime - constantDuration;
|
|
5462
|
-
|
|
5463
|
-
if (constantDuration > 0) {
|
|
5464
|
-
animationParts.push(
|
|
5465
|
-
`pre-wipe-char ${constantDuration}ms linear ${animDelay}ms forwards`,
|
|
5466
|
-
);
|
|
5467
|
-
}
|
|
5468
|
-
}
|
|
5469
|
-
|
|
5470
6186
|
if (wipeDuration > 0) {
|
|
6187
|
+
const wipeFillMode = hadCharPreWipe ? 'both' : 'forwards';
|
|
5471
6188
|
animationParts.push(
|
|
5472
|
-
`${charWipeAnimation} ${wipeDuration}ms linear ${wipeDelay}ms
|
|
6189
|
+
`${charWipeAnimation} ${wipeDuration}ms linear ${wipeDelay}ms ${wipeFillMode}`,
|
|
5473
6190
|
);
|
|
5474
6191
|
}
|
|
5475
6192
|
|
|
@@ -5482,10 +6199,15 @@ export class AmLyrics extends LitElement {
|
|
|
5482
6199
|
const wipeRatio = parseFloat(
|
|
5483
6200
|
syllable.getAttribute('data-wipe-ratio') || '1',
|
|
5484
6201
|
);
|
|
5485
|
-
const
|
|
6202
|
+
const wipeCharCount = AmLyrics.getVisibleCharacterCount(syllable);
|
|
6203
|
+
const wipeScale = AmLyrics.getLongWordWipeScale(wipeCharCount);
|
|
6204
|
+
const visualDuration = syllableDurationMs * wipeRatio * wipeScale;
|
|
6205
|
+
AmLyrics.applyWipeShape(syllable, wipeCharCount);
|
|
5486
6206
|
|
|
5487
6207
|
let wipeAnimation = 'wipe';
|
|
5488
|
-
if (
|
|
6208
|
+
if (hadPreHighlight) {
|
|
6209
|
+
wipeAnimation = isRTL ? 'wipe-from-pre-rtl' : 'wipe-from-pre';
|
|
6210
|
+
} else if (isFirstInContainer) {
|
|
5489
6211
|
wipeAnimation = isRTL ? 'start-wipe-rtl' : 'start-wipe';
|
|
5490
6212
|
} else {
|
|
5491
6213
|
wipeAnimation = isRTL ? 'wipe-rtl' : 'wipe';
|
|
@@ -5499,18 +6221,28 @@ export class AmLyrics extends LitElement {
|
|
|
5499
6221
|
}
|
|
5500
6222
|
|
|
5501
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
|
+
|
|
5502
6231
|
classList.remove('pre-highlight');
|
|
5503
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
|
+
}
|
|
5504
6240
|
|
|
5505
6241
|
for (const [span, animationString] of charAnimationsMap.entries()) {
|
|
5506
6242
|
span.style.willChange = 'transform';
|
|
6243
|
+
span.style.removeProperty('background-color');
|
|
5507
6244
|
span.style.animation = animationString;
|
|
5508
6245
|
}
|
|
5509
|
-
|
|
5510
|
-
// Apply style updates
|
|
5511
|
-
for (const update of styleUpdates) {
|
|
5512
|
-
update.element.style.setProperty(update.property, update.value);
|
|
5513
|
-
}
|
|
5514
6246
|
}
|
|
5515
6247
|
|
|
5516
6248
|
/**
|
|
@@ -5535,6 +6267,7 @@ export class AmLyrics extends LitElement {
|
|
|
5535
6267
|
el.style.animation = '';
|
|
5536
6268
|
el.style.transition = 'none';
|
|
5537
6269
|
el.style.backgroundColor = 'var(--lyplus-text-secondary)';
|
|
6270
|
+
AmLyrics.clearPreWipeLead(el);
|
|
5538
6271
|
}
|
|
5539
6272
|
|
|
5540
6273
|
// Immediately remove all state classes
|
|
@@ -5546,12 +6279,22 @@ export class AmLyrics extends LitElement {
|
|
|
5546
6279
|
);
|
|
5547
6280
|
}
|
|
5548
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
|
+
|
|
5549
6291
|
/**
|
|
5550
6292
|
* Reset all syllables in a line — batches deferred cleanup into a single rAF
|
|
5551
6293
|
*/
|
|
5552
6294
|
private static resetSyllables(line: HTMLElement): void {
|
|
5553
6295
|
if (!line) return;
|
|
5554
6296
|
line.classList.remove('persist-highlight');
|
|
6297
|
+
AmLyrics.resetWordAnimationState(line);
|
|
5555
6298
|
// eslint-disable-next-line no-param-reassign
|
|
5556
6299
|
(line as any)._cachedSyllableElements = null;
|
|
5557
6300
|
const syllables = line.getElementsByClassName('lyrics-syllable');
|
|
@@ -5583,6 +6326,7 @@ export class AmLyrics extends LitElement {
|
|
|
5583
6326
|
private static unfinishSyllables(line: HTMLElement): void {
|
|
5584
6327
|
if (!line) return;
|
|
5585
6328
|
line.classList.remove('persist-highlight');
|
|
6329
|
+
AmLyrics.resetWordAnimationState(line);
|
|
5586
6330
|
const syllables = line.getElementsByClassName('lyrics-syllable');
|
|
5587
6331
|
for (let i = 0; i < syllables.length; i += 1) {
|
|
5588
6332
|
const s = syllables[i] as HTMLElement;
|
|
@@ -5600,6 +6344,7 @@ export class AmLyrics extends LitElement {
|
|
|
5600
6344
|
el.style.removeProperty('background-color');
|
|
5601
6345
|
el.style.removeProperty('transition');
|
|
5602
6346
|
el.style.removeProperty('filter');
|
|
6347
|
+
AmLyrics.clearPreWipeLead(el);
|
|
5603
6348
|
}
|
|
5604
6349
|
}
|
|
5605
6350
|
}
|
|
@@ -5650,22 +6395,33 @@ export class AmLyrics extends LitElement {
|
|
|
5650
6395
|
syllable.style.animation = '';
|
|
5651
6396
|
syllable.style.removeProperty('--pre-wipe-duration');
|
|
5652
6397
|
syllable.style.removeProperty('--pre-wipe-delay');
|
|
6398
|
+
syllable.style.removeProperty('background-color');
|
|
6399
|
+
AmLyrics.applyWipeShape(
|
|
6400
|
+
syllable,
|
|
6401
|
+
AmLyrics.getVisibleCharacterCount(syllable),
|
|
6402
|
+
);
|
|
5653
6403
|
const chars = syllable.querySelectorAll('span.char');
|
|
5654
6404
|
for (let ci = 0; ci < chars.length; ci += 1) {
|
|
5655
6405
|
const charEl = chars[ci] as HTMLElement;
|
|
5656
6406
|
const currentAnim = charEl.style.animation || '';
|
|
5657
6407
|
if (
|
|
5658
6408
|
currentAnim.includes('grow-dynamic') ||
|
|
5659
|
-
currentAnim.includes('rise-char')
|
|
6409
|
+
currentAnim.includes('rise-char') ||
|
|
6410
|
+
currentAnim.includes('drag-char')
|
|
5660
6411
|
) {
|
|
5661
6412
|
const parts = currentAnim.split(',').map(p => p.trim());
|
|
5662
6413
|
const transformAnim = parts.find(
|
|
5663
|
-
p =>
|
|
6414
|
+
p =>
|
|
6415
|
+
p.includes('grow-dynamic') ||
|
|
6416
|
+
p.includes('rise-char') ||
|
|
6417
|
+
p.includes('drag-char'),
|
|
5664
6418
|
);
|
|
5665
6419
|
charEl.style.animation = transformAnim || '';
|
|
5666
6420
|
} else {
|
|
5667
6421
|
charEl.style.animation = '';
|
|
5668
6422
|
}
|
|
6423
|
+
charEl.style.backgroundColor = 'var(--lyplus-text-primary)';
|
|
6424
|
+
AmLyrics.clearPreWipeLead(charEl);
|
|
5669
6425
|
}
|
|
5670
6426
|
}
|
|
5671
6427
|
}
|
|
@@ -5720,14 +6476,20 @@ export class AmLyrics extends LitElement {
|
|
|
5720
6476
|
if (!(currentTimeMs < startTime - 1000 && !hasActiveState)) {
|
|
5721
6477
|
let preHighlightReset = false;
|
|
5722
6478
|
|
|
5723
|
-
//
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
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);
|
|
5731
6493
|
preHighlightReset = true;
|
|
5732
6494
|
}
|
|
5733
6495
|
}
|
|
@@ -5760,6 +6522,8 @@ export class AmLyrics extends LitElement {
|
|
|
5760
6522
|
// Not yet started
|
|
5761
6523
|
AmLyrics.resetSyllable(syllable);
|
|
5762
6524
|
}
|
|
6525
|
+
|
|
6526
|
+
AmLyrics.maybePreWipeNextWord(syllables, i, currentTimeMs, endTime);
|
|
5763
6527
|
}
|
|
5764
6528
|
}
|
|
5765
6529
|
}
|
|
@@ -6120,6 +6884,9 @@ export class AmLyrics extends LitElement {
|
|
|
6120
6884
|
data-end-time="${endTimeMs}"
|
|
6121
6885
|
data-duration="${durationMs}"
|
|
6122
6886
|
data-syllable-index="${syllableIndex}"
|
|
6887
|
+
data-word-index="${syllableIndex}"
|
|
6888
|
+
data-word-length="${syllable.text.replace(/\s/g, '')
|
|
6889
|
+
.length}"
|
|
6123
6890
|
data-wipe-ratio="1"
|
|
6124
6891
|
>${syllable.text}</span
|
|
6125
6892
|
>${bgRomanizedText}</span
|
|
@@ -6144,12 +6911,14 @@ export class AmLyrics extends LitElement {
|
|
|
6144
6911
|
const groupGrowable = lineData?.groupGrowable ?? [];
|
|
6145
6912
|
const groupGlowing = lineData?.groupGlowing ?? [];
|
|
6146
6913
|
const groupCharRise = lineData?.groupCharRise ?? [];
|
|
6914
|
+
const groupCharDrag = lineData?.groupCharDrag ?? [];
|
|
6147
6915
|
const vwFullText = lineData?.vwFullText ?? [];
|
|
6148
6916
|
const vwFullDuration = lineData?.vwFullDuration ?? [];
|
|
6149
6917
|
const vwCharOffset = lineData?.vwCharOffset ?? [];
|
|
6918
|
+
const vwStartMs = lineData?.vwStartMs ?? [];
|
|
6919
|
+
const vwEndMs = lineData?.vwEndMs ?? [];
|
|
6150
6920
|
const lineIsRTL = lineData?.lineIsRTL ?? false;
|
|
6151
6921
|
|
|
6152
|
-
// Create main vocals using YouLyPlus syllable structure
|
|
6153
6922
|
const mainVocalElement = html`<p
|
|
6154
6923
|
class="main-vocal-container ${lineIsRTL ? 'rtl-text' : ''}"
|
|
6155
6924
|
>
|
|
@@ -6157,21 +6926,26 @@ export class AmLyrics extends LitElement {
|
|
|
6157
6926
|
const isGrowable = groupGrowable[groupIdx];
|
|
6158
6927
|
const isGlowing = groupGlowing[groupIdx];
|
|
6159
6928
|
const isCharRise = groupCharRise[groupIdx];
|
|
6160
|
-
const
|
|
6929
|
+
const isCharDrag = groupCharDrag[groupIdx];
|
|
6930
|
+
const isAnimatedByChar = isGrowable || isCharRise || isCharDrag;
|
|
6161
6931
|
const groupLineSynced = group.some(s => s.lineSynced);
|
|
6162
6932
|
|
|
6163
6933
|
const wordText = isAnimatedByChar ? vwFullText[groupIdx] : '';
|
|
6164
6934
|
const wordDuration = isAnimatedByChar
|
|
6165
6935
|
? vwFullDuration[groupIdx]
|
|
6166
6936
|
: 0;
|
|
6167
|
-
const wordNumChars = wordText.length;
|
|
6937
|
+
const wordNumChars = wordText.replace(/\s/g, '').length;
|
|
6168
6938
|
const groupCharOffset = isAnimatedByChar
|
|
6169
6939
|
? vwCharOffset[groupIdx]
|
|
6170
6940
|
: 0;
|
|
6941
|
+
const virtualWordId = `${lineIndex}:${vwStartMs[groupIdx]}:${vwEndMs[groupIdx]}`;
|
|
6942
|
+
const virtualWordStart = vwStartMs[groupIdx];
|
|
6943
|
+
const virtualWordEnd = vwEndMs[groupIdx];
|
|
6171
6944
|
|
|
6172
6945
|
let sylCharAccumulator = 0;
|
|
6173
6946
|
|
|
6174
6947
|
const groupText = group.map(s => s.text).join('');
|
|
6948
|
+
const visibleWordLength = groupText.replace(/\s/g, '').length;
|
|
6175
6949
|
const shouldAllowBreak =
|
|
6176
6950
|
groupText.trim().length >= 16 ||
|
|
6177
6951
|
/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(
|
|
@@ -6191,9 +6965,12 @@ export class AmLyrics extends LitElement {
|
|
|
6191
6965
|
return html`<span
|
|
6192
6966
|
class="lyrics-word${isGrowable ? ' growable' : ''}${isCharRise
|
|
6193
6967
|
? ' char-rise'
|
|
6194
|
-
: ''}${
|
|
6195
|
-
? '
|
|
6196
|
-
: ''}"
|
|
6968
|
+
: ''}${isCharDrag ? ' char-drag' : ''}${isGlowing
|
|
6969
|
+
? ' glowing'
|
|
6970
|
+
: ''}${shouldAllowBreak ? ' allow-break' : ''}"
|
|
6971
|
+
data-virtual-word-id="${virtualWordId}"
|
|
6972
|
+
data-virtual-word-start="${virtualWordStart}"
|
|
6973
|
+
data-virtual-word-end="${virtualWordEnd}"
|
|
6197
6974
|
style="--rise-duration: ${riseDuration}s"
|
|
6198
6975
|
>${group.map((syllable, sylIdx) => {
|
|
6199
6976
|
const startTimeMs = syllable.timestamp;
|
|
@@ -6221,17 +6998,39 @@ export class AmLyrics extends LitElement {
|
|
|
6221
6998
|
let syllableContent: any = text;
|
|
6222
6999
|
|
|
6223
7000
|
if (isAnimatedByChar) {
|
|
6224
|
-
let charIndexInsideSyllable = 0;
|
|
6225
7001
|
const numCharsInSyllable =
|
|
6226
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;
|
|
6227
7016
|
|
|
6228
7017
|
syllableContent = html`${text.split('').map(char => {
|
|
6229
7018
|
if (char === ' ') return ' ';
|
|
6230
7019
|
|
|
6231
7020
|
const charIndexInsideWord =
|
|
6232
7021
|
groupCharOffset + sylCharAccumulator;
|
|
6233
|
-
const
|
|
6234
|
-
|
|
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;
|
|
6235
7034
|
|
|
6236
7035
|
sylCharAccumulator += 1;
|
|
6237
7036
|
charIndexInsideSyllable += 1;
|
|
@@ -6293,29 +7092,41 @@ export class AmLyrics extends LitElement {
|
|
|
6293
7092
|
1,
|
|
6294
7093
|
Math.max(0.3, effectiveDuration / 2000),
|
|
6295
7094
|
);
|
|
6296
|
-
const
|
|
7095
|
+
const baseTranslateYPeak =
|
|
6297
7096
|
-normalizedGrowth * (2 * peakMultiplier); // Further dampened lift peak
|
|
6298
7097
|
|
|
6299
7098
|
const position = (charIndexInsideWord + 0.5) / wordNumChars;
|
|
6300
7099
|
const horizontalOffset =
|
|
6301
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
|
+
}
|
|
6302
7108
|
|
|
7109
|
+
let motionHorizontalOffset = horizontalOffset;
|
|
7110
|
+
if (isCharRise) {
|
|
7111
|
+
motionHorizontalOffset = 0;
|
|
7112
|
+
} else if (isDragMotion) {
|
|
7113
|
+
motionHorizontalOffset = 0;
|
|
7114
|
+
}
|
|
6303
7115
|
return html`<span
|
|
6304
7116
|
class="char"
|
|
6305
7117
|
data-char-index="${charIndexInsideWord}"
|
|
6306
7118
|
data-syllable-char-index="${charIndexInsideWord}"
|
|
6307
7119
|
data-wipe-start="${charStartPercentVal.toFixed(4)}"
|
|
6308
|
-
data-wipe-duration="${
|
|
6309
|
-
4,
|
|
6310
|
-
)}"
|
|
7120
|
+
data-wipe-duration="${charDurationPercentVal.toFixed(4)}"
|
|
6311
7121
|
data-horizontal-offset="${horizontalOffset.toFixed(2)}"
|
|
6312
7122
|
data-max-scale="${charMaxScale.toFixed(3)}"
|
|
6313
7123
|
data-matrix-scale="${(charMaxScale * 0.98).toFixed(3)}"
|
|
6314
|
-
data-char-offset-x="${(
|
|
6315
|
-
|
|
6316
|
-
)}"
|
|
7124
|
+
data-char-offset-x="${(
|
|
7125
|
+
motionHorizontalOffset * 0.98
|
|
7126
|
+
).toFixed(2)}"
|
|
6317
7127
|
data-shadow-intensity="${charShadowIntensity.toFixed(3)}"
|
|
6318
7128
|
data-translate-y-peak="${charTranslateYPeak.toFixed(3)}"
|
|
7129
|
+
style="--word-wipe-width: ${visibleWordChars}ch; --char-wipe-position: -${charIndexInsideWord}ch"
|
|
6319
7130
|
>${char}</span
|
|
6320
7131
|
>`;
|
|
6321
7132
|
})}`;
|
|
@@ -6334,6 +7145,8 @@ export class AmLyrics extends LitElement {
|
|
|
6334
7145
|
data-duration="${durationMs}"
|
|
6335
7146
|
data-word-duration="${wordDuration}"
|
|
6336
7147
|
data-syllable-index="${sylIdx}"
|
|
7148
|
+
data-word-index="${groupIdx}"
|
|
7149
|
+
data-word-length="${visibleWordLength}"
|
|
6337
7150
|
data-wipe-ratio="1"
|
|
6338
7151
|
>${syllableContent}</span
|
|
6339
7152
|
>${romanizedText}</span
|
|
@@ -6574,13 +7387,14 @@ export class AmLyrics extends LitElement {
|
|
|
6574
7387
|
<button
|
|
6575
7388
|
class="download-button source-switch-btn"
|
|
6576
7389
|
title="Switch Lyrics Source"
|
|
6577
|
-
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;"
|
|
6578
7390
|
@click=${this.switchSource}
|
|
6579
7391
|
?disabled=${this.isFetchingAlternatives}
|
|
6580
7392
|
>
|
|
6581
7393
|
<svg
|
|
6582
|
-
class="source-switch-svg lucide lucide-arrow-down-up-icon lucide-arrow-down-up
|
|
6583
|
-
|
|
7394
|
+
class="source-switch-svg lucide lucide-arrow-down-up-icon lucide-arrow-down-up ${this
|
|
7395
|
+
.isFetchingAlternatives
|
|
7396
|
+
? 'is-loading'
|
|
7397
|
+
: ''}"
|
|
6584
7398
|
xmlns="http://www.w3.org/2000/svg"
|
|
6585
7399
|
width="12"
|
|
6586
7400
|
height="12"
|