@uimaxbai/am-lyrics 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/AmLyrics.d.ts +27 -2
- package/dist/src/AmLyrics.d.ts.map +1 -1
- package/dist/src/am-lyrics.js +967 -354
- package/dist/src/am-lyrics.js.map +1 -1
- package/dist/src/react.js +967 -354
- package/dist/src/react.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/AmLyrics.ts +1134 -438
package/dist/src/am-lyrics.js
CHANGED
|
@@ -319,14 +319,11 @@ class GoogleService {
|
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
-
const VERSION = '1.
|
|
322
|
+
const VERSION = '1.5.0';
|
|
323
323
|
const INSTRUMENTAL_THRESHOLD_MS = 7000; // Show dots for gaps >= 7s
|
|
324
324
|
const FETCH_TIMEOUT_MS = 8000; // Timeout for all lyrics fetch requests
|
|
325
325
|
const SEEK_THRESHOLD_MS = 500;
|
|
326
|
-
const
|
|
327
|
-
const PRE_SCROLL_LEAD_SHORT_MS = 350;
|
|
328
|
-
const SCROLL_ANIMATION_DURATION_MS = 280;
|
|
329
|
-
const SCROLL_DELAY_INCREMENT_MS = 24;
|
|
326
|
+
const SCROLL_ANIMATION_DURATION_MS = 350;
|
|
330
327
|
const GAP_PULSE_DURATION_MS = 4000;
|
|
331
328
|
const GAP_PULSE_CYCLE_MS = GAP_PULSE_DURATION_MS * 2;
|
|
332
329
|
const GAP_EXIT_LEAD_MS = 600;
|
|
@@ -377,6 +374,8 @@ class AmLyrics extends i {
|
|
|
377
374
|
this.isClickSeeking = false;
|
|
378
375
|
// Cached DOM elements for animation updates
|
|
379
376
|
this.cachedLyricsLines = [];
|
|
377
|
+
// Cached line elements array for scroll/position queries
|
|
378
|
+
this.cachedLineArray = [];
|
|
380
379
|
// Cached line and gap element maps for fast lookup
|
|
381
380
|
this.lineElementCache = new Map();
|
|
382
381
|
this.gapElementCache = new Map();
|
|
@@ -397,6 +396,8 @@ class AmLyrics extends i {
|
|
|
397
396
|
// Syllable animation tracking
|
|
398
397
|
this.lastActiveIndex = 0;
|
|
399
398
|
this.visibleLineIds = new Set();
|
|
399
|
+
// Cached scroll padding top value
|
|
400
|
+
this.cachedScrollPaddingTop = null;
|
|
400
401
|
// Cached element tracking to avoid repeated querySelectorAll calls
|
|
401
402
|
this.preActiveLineElements = [];
|
|
402
403
|
this.positionedLineElements = [];
|
|
@@ -487,9 +488,9 @@ class AmLyrics extends i {
|
|
|
487
488
|
this.activeGapLineElements = [];
|
|
488
489
|
// Stop all running animations and clear highlights immediately
|
|
489
490
|
if (this.lyricsContainer) {
|
|
490
|
-
const activeLines = this.lyricsContainer.querySelectorAll('.lyrics-line.active, .lyrics-line.pre-active');
|
|
491
|
+
const activeLines = this.lyricsContainer.querySelectorAll('.lyrics-line.active, .lyrics-line.pre-active, .lyrics-line.bg-expanded');
|
|
491
492
|
activeLines.forEach(line => {
|
|
492
|
-
line.classList.remove('active', 'pre-active');
|
|
493
|
+
line.classList.remove('active', 'pre-active', 'bg-expanded');
|
|
493
494
|
AmLyrics.resetSyllables(line);
|
|
494
495
|
});
|
|
495
496
|
const activeGaps = this.lyricsContainer.querySelectorAll('.lyrics-gap.active, .lyrics-gap.gap-exiting');
|
|
@@ -543,14 +544,14 @@ class AmLyrics extends i {
|
|
|
543
544
|
clearTimeout(this.clickSeekTimeout);
|
|
544
545
|
this.clickSeekTimeout = undefined;
|
|
545
546
|
}
|
|
546
|
-
if (this.scrollUnlockTimeout) {
|
|
547
|
-
clearTimeout(this.scrollUnlockTimeout);
|
|
548
|
-
this.scrollUnlockTimeout = undefined;
|
|
549
|
-
}
|
|
550
547
|
if (this.scrollAnimationTimeout) {
|
|
551
548
|
clearTimeout(this.scrollAnimationTimeout);
|
|
552
549
|
this.scrollAnimationTimeout = undefined;
|
|
553
550
|
}
|
|
551
|
+
if (this.scrollUnlockTimeout) {
|
|
552
|
+
clearTimeout(this.scrollUnlockTimeout);
|
|
553
|
+
this.scrollUnlockTimeout = undefined;
|
|
554
|
+
}
|
|
554
555
|
// Cancel any in-flight fetch requests
|
|
555
556
|
this.fetchAbortController?.abort();
|
|
556
557
|
this.fetchAbortController = undefined;
|
|
@@ -562,6 +563,8 @@ class AmLyrics extends i {
|
|
|
562
563
|
this.preActiveLineElements = [];
|
|
563
564
|
this.positionedLineElements = [];
|
|
564
565
|
this.activeGapLineElements = [];
|
|
566
|
+
this.visibilityObserver?.disconnect();
|
|
567
|
+
this.visibilityObserver = undefined;
|
|
565
568
|
}
|
|
566
569
|
async fetchLyrics() {
|
|
567
570
|
// Cancel any in-flight fetch to prevent stale results from racing
|
|
@@ -590,9 +593,21 @@ class AmLyrics extends i {
|
|
|
590
593
|
if (resolvedMetadata?.metadata && !isMusicIdOnlyRequest) {
|
|
591
594
|
const title = resolvedMetadata.metadata.title?.trim() || '';
|
|
592
595
|
const artist = resolvedMetadata.metadata.artist?.trim() || '';
|
|
593
|
-
const
|
|
594
|
-
if (
|
|
595
|
-
collectedSources.push(
|
|
596
|
+
const biniResult = await AmLyrics.fetchLyricsFromBiniLyrics(title, artist, resolvedMetadata.catalogIsrc, resolvedMetadata.metadata);
|
|
597
|
+
if (biniResult && biniResult.lines.length > 0) {
|
|
598
|
+
collectedSources.push(biniResult);
|
|
599
|
+
}
|
|
600
|
+
if (collectedSources.length === 0) {
|
|
601
|
+
const unisonResult = await AmLyrics.fetchLyricsFromUnison(resolvedMetadata.metadata);
|
|
602
|
+
if (unisonResult && unisonResult.lines.length > 0) {
|
|
603
|
+
collectedSources.push(unisonResult);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (collectedSources.length === 0) {
|
|
607
|
+
const youLyResults = await AmLyrics.fetchLyricsFromYouLyPlus(title, artist, resolvedMetadata.catalogIsrc, resolvedMetadata.metadata, true);
|
|
608
|
+
if (youLyResults && youLyResults.length > 0) {
|
|
609
|
+
collectedSources.push(...youLyResults);
|
|
610
|
+
}
|
|
596
611
|
}
|
|
597
612
|
}
|
|
598
613
|
if (collectedSources.length === 0 && resolvedMetadata?.metadata) {
|
|
@@ -676,35 +691,47 @@ class AmLyrics extends i {
|
|
|
676
691
|
const isQQ = lower.includes('qq') || lower.includes('lyricsplus');
|
|
677
692
|
if (lower.includes('apple') && hasWordSync)
|
|
678
693
|
return 1;
|
|
679
|
-
if (
|
|
694
|
+
if (lower.includes('bini') && hasWordSync)
|
|
680
695
|
return 2;
|
|
681
|
-
if (lower.includes('
|
|
696
|
+
if (lower.includes('unison') && hasWordSync)
|
|
682
697
|
return 3;
|
|
683
|
-
if (
|
|
698
|
+
if (isQQ && hasWordSync)
|
|
684
699
|
return 4;
|
|
685
|
-
if (hasWordSync)
|
|
700
|
+
if (lower.includes('musixmatch') && hasWordSync)
|
|
686
701
|
return 5;
|
|
687
|
-
if (lower.includes('
|
|
702
|
+
if (lower.includes('lrclib') && hasWordSync)
|
|
688
703
|
return 6;
|
|
689
|
-
if (
|
|
704
|
+
if (hasWordSync)
|
|
690
705
|
return 7;
|
|
691
|
-
if (lower.includes('
|
|
706
|
+
if (lower.includes('apple') && !hasWordSync && !isUnsynced)
|
|
692
707
|
return 8;
|
|
693
|
-
if (lower.includes('
|
|
708
|
+
if (lower.includes('bini') && !hasWordSync && !isUnsynced)
|
|
694
709
|
return 9;
|
|
695
|
-
if (!hasWordSync && !isUnsynced)
|
|
710
|
+
if (lower.includes('unison') && !hasWordSync && !isUnsynced)
|
|
696
711
|
return 10;
|
|
697
|
-
if (
|
|
712
|
+
if (isQQ && !hasWordSync && !isUnsynced)
|
|
698
713
|
return 11;
|
|
699
|
-
if (
|
|
714
|
+
if (lower.includes('musixmatch') && !hasWordSync && !isUnsynced)
|
|
700
715
|
return 12;
|
|
701
|
-
if (lower.includes('
|
|
716
|
+
if (lower.includes('lrclib') && !hasWordSync && !isUnsynced)
|
|
702
717
|
return 13;
|
|
703
|
-
if (
|
|
718
|
+
if (!hasWordSync && !isUnsynced)
|
|
704
719
|
return 14;
|
|
705
|
-
if (lower.includes('
|
|
720
|
+
if (lower.includes('apple') && isUnsynced)
|
|
706
721
|
return 15;
|
|
707
|
-
|
|
722
|
+
if (lower.includes('bini') && isUnsynced)
|
|
723
|
+
return 16;
|
|
724
|
+
if (lower.includes('unison') && isUnsynced)
|
|
725
|
+
return 17;
|
|
726
|
+
if (isQQ && isUnsynced)
|
|
727
|
+
return 18;
|
|
728
|
+
if (lower.includes('musixmatch') && isUnsynced)
|
|
729
|
+
return 19;
|
|
730
|
+
if (lower.includes('lrclib') && isUnsynced)
|
|
731
|
+
return 20;
|
|
732
|
+
if (lower.includes('genius'))
|
|
733
|
+
return 21;
|
|
734
|
+
return 30;
|
|
708
735
|
}
|
|
709
736
|
static mergeAndSortSources(collectedSources) {
|
|
710
737
|
const uniqueSourcesMap = new Map();
|
|
@@ -922,60 +949,11 @@ class AmLyrics extends i {
|
|
|
922
949
|
}
|
|
923
950
|
return null;
|
|
924
951
|
}
|
|
925
|
-
static async
|
|
952
|
+
static async fetchLyricsFromBiniLyrics(title, artist, isrc, metadata = {}) {
|
|
926
953
|
if ((!title || !artist) && !isrc)
|
|
927
|
-
return
|
|
928
|
-
const params = new URLSearchParams();
|
|
929
|
-
if (title)
|
|
930
|
-
params.append('title', title);
|
|
931
|
-
if (artist)
|
|
932
|
-
params.append('artist', artist);
|
|
933
|
-
if (isrc)
|
|
934
|
-
params.append('isrc', isrc);
|
|
935
|
-
if (metadata.album) {
|
|
936
|
-
params.append('album', metadata.album);
|
|
937
|
-
}
|
|
938
|
-
if (metadata.durationMs && metadata.durationMs > 0) {
|
|
939
|
-
params.append('duration', Math.round(metadata.durationMs / 1000).toString());
|
|
940
|
-
}
|
|
941
|
-
if (!DEFAULT_KPOE_SOURCE_ORDER.includes('apple')) {
|
|
942
|
-
params.append('source', DEFAULT_KPOE_SOURCE_ORDER);
|
|
943
|
-
}
|
|
944
|
-
const getRank = (sourceLabel, parsedLines) => {
|
|
945
|
-
const lower = sourceLabel.toLowerCase();
|
|
946
|
-
const hasWordSync = parsedLines.some((line) => line.text && Array.isArray(line.text) && line.text.length > 1);
|
|
947
|
-
const isUnsynced = parsedLines.length > 0 &&
|
|
948
|
-
parsedLines.every((line) => line.timestamp === 0 && line.endtime === 0);
|
|
949
|
-
const isQQ = lower.includes('qq') || lower.includes('lyricsplus');
|
|
950
|
-
if (lower.includes('apple') && hasWordSync)
|
|
951
|
-
return 1;
|
|
952
|
-
if (isQQ && hasWordSync)
|
|
953
|
-
return 2;
|
|
954
|
-
if (lower.includes('musixmatch') && hasWordSync)
|
|
955
|
-
return 3;
|
|
956
|
-
if (hasWordSync)
|
|
957
|
-
return 4;
|
|
958
|
-
if (lower.includes('apple') && !hasWordSync && !isUnsynced)
|
|
959
|
-
return 5;
|
|
960
|
-
if (isQQ && !hasWordSync && !isUnsynced)
|
|
961
|
-
return 6;
|
|
962
|
-
if (lower.includes('musixmatch') && !hasWordSync && !isUnsynced)
|
|
963
|
-
return 7;
|
|
964
|
-
if (!hasWordSync && !isUnsynced)
|
|
965
|
-
return 8;
|
|
966
|
-
if (lower.includes('apple') && isUnsynced)
|
|
967
|
-
return 9;
|
|
968
|
-
if (isQQ && isUnsynced)
|
|
969
|
-
return 10;
|
|
970
|
-
if (lower.includes('musixmatch') && isUnsynced)
|
|
971
|
-
return 11;
|
|
972
|
-
return 20;
|
|
973
|
-
};
|
|
974
|
-
const allResults = [];
|
|
975
|
-
// Try BiniLyrics cache API first
|
|
954
|
+
return null;
|
|
976
955
|
try {
|
|
977
956
|
let cacheData = null;
|
|
978
|
-
// First attempt: Prefer ISRC search if available
|
|
979
957
|
if (isrc) {
|
|
980
958
|
try {
|
|
981
959
|
const isrcUrl = `https://lyrics-api.binimum.org/?isrc=${encodeURIComponent(isrc)}`;
|
|
@@ -987,11 +965,10 @@ class AmLyrics extends i {
|
|
|
987
965
|
}
|
|
988
966
|
}
|
|
989
967
|
}
|
|
990
|
-
catch
|
|
968
|
+
catch {
|
|
991
969
|
// Fall through to title/artist search
|
|
992
970
|
}
|
|
993
971
|
}
|
|
994
|
-
// Second attempt: Fallback to title and artist search if ISRC search failed or was not available
|
|
995
972
|
if (!cacheData && title && artist) {
|
|
996
973
|
const cacheParams = new URLSearchParams({
|
|
997
974
|
track: title,
|
|
@@ -1011,57 +988,17 @@ class AmLyrics extends i {
|
|
|
1011
988
|
}
|
|
1012
989
|
if (cacheData && cacheData.results && cacheData.results.length > 0) {
|
|
1013
990
|
const result = cacheData.results[0];
|
|
1014
|
-
if (result.
|
|
991
|
+
if (result.lyricsUrl) {
|
|
1015
992
|
const ttmlRes = await fetchWithTimeout(result.lyricsUrl);
|
|
1016
993
|
if (ttmlRes.ok) {
|
|
1017
994
|
const ttmlText = await ttmlRes.text();
|
|
1018
995
|
const parseResult = AmLyrics.parseTTML(ttmlText);
|
|
1019
996
|
if (parseResult && parseResult.lines.length > 0) {
|
|
1020
|
-
|
|
997
|
+
return {
|
|
1021
998
|
lines: parseResult.lines,
|
|
1022
999
|
source: 'BiniLyrics',
|
|
1023
1000
|
songwriters: parseResult.songwriters,
|
|
1024
|
-
}
|
|
1025
|
-
return allResults;
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
else {
|
|
1030
|
-
// Not word type, try fetching any word synced lyrics from lyricsplus
|
|
1031
|
-
const fallbackParams = new URLSearchParams(params);
|
|
1032
|
-
const fallbackUrl = `https://lyricsplus.binimum.org/v2/lyrics/get?${fallbackParams.toString()}`;
|
|
1033
|
-
try {
|
|
1034
|
-
const fallbackRes = await fetchWithTimeout(fallbackUrl);
|
|
1035
|
-
if (fallbackRes.ok) {
|
|
1036
|
-
const payload = await fallbackRes.json();
|
|
1037
|
-
const lines = AmLyrics.convertKPoeLyrics(payload);
|
|
1038
|
-
const hasWordSync = lines?.some((line) => line.text && Array.isArray(line.text) && line.text.length > 1);
|
|
1039
|
-
if (lines && lines.length > 0 && hasWordSync) {
|
|
1040
|
-
const sourceLabel = payload?.metadata?.source ||
|
|
1041
|
-
payload?.metadata?.provider ||
|
|
1042
|
-
'LyricsPlus (KPoe)';
|
|
1043
|
-
allResults.push({ lines, source: sourceLabel });
|
|
1044
|
-
return allResults;
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
catch (fallbackError) {
|
|
1049
|
-
// Ignore fallback fetch error
|
|
1050
|
-
}
|
|
1051
|
-
// If fallback fails or has no word sync, fall back to bini lyrics
|
|
1052
|
-
if (result.lyricsUrl) {
|
|
1053
|
-
const ttmlRes = await fetchWithTimeout(result.lyricsUrl);
|
|
1054
|
-
if (ttmlRes.ok) {
|
|
1055
|
-
const ttmlText = await ttmlRes.text();
|
|
1056
|
-
const parseResult = AmLyrics.parseTTML(ttmlText);
|
|
1057
|
-
if (parseResult && parseResult.lines.length > 0) {
|
|
1058
|
-
allResults.push({
|
|
1059
|
-
lines: parseResult.lines,
|
|
1060
|
-
source: 'BiniLyrics',
|
|
1061
|
-
songwriters: parseResult.songwriters,
|
|
1062
|
-
});
|
|
1063
|
-
return allResults;
|
|
1064
|
-
}
|
|
1001
|
+
};
|
|
1065
1002
|
}
|
|
1066
1003
|
}
|
|
1067
1004
|
}
|
|
@@ -1071,6 +1008,77 @@ class AmLyrics extends i {
|
|
|
1071
1008
|
// eslint-disable-next-line no-console
|
|
1072
1009
|
console.error('Cache API failed', e);
|
|
1073
1010
|
}
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
static async fetchLyricsFromYouLyPlus(title, artist, isrc, metadata = {}, skipBiniCache = false) {
|
|
1014
|
+
if ((!title || !artist) && !isrc)
|
|
1015
|
+
return [];
|
|
1016
|
+
const params = new URLSearchParams();
|
|
1017
|
+
if (title)
|
|
1018
|
+
params.append('title', title);
|
|
1019
|
+
if (artist)
|
|
1020
|
+
params.append('artist', artist);
|
|
1021
|
+
if (isrc)
|
|
1022
|
+
params.append('isrc', isrc);
|
|
1023
|
+
if (metadata.album) {
|
|
1024
|
+
params.append('album', metadata.album);
|
|
1025
|
+
}
|
|
1026
|
+
if (metadata.durationMs && metadata.durationMs > 0) {
|
|
1027
|
+
params.append('duration', Math.round(metadata.durationMs / 1000).toString());
|
|
1028
|
+
}
|
|
1029
|
+
if (!DEFAULT_KPOE_SOURCE_ORDER.includes('apple')) {
|
|
1030
|
+
params.append('source', DEFAULT_KPOE_SOURCE_ORDER);
|
|
1031
|
+
}
|
|
1032
|
+
const getRank = (sourceLabel, parsedLines) => {
|
|
1033
|
+
const lower = sourceLabel.toLowerCase();
|
|
1034
|
+
const hasWordSync = parsedLines.some((line) => line.text && Array.isArray(line.text) && line.text.length > 1);
|
|
1035
|
+
const isUnsynced = parsedLines.length > 0 &&
|
|
1036
|
+
parsedLines.every((line) => line.timestamp === 0 && line.endtime === 0);
|
|
1037
|
+
const isQQ = lower.includes('qq') || lower.includes('lyricsplus');
|
|
1038
|
+
if (lower.includes('apple') && hasWordSync)
|
|
1039
|
+
return 1;
|
|
1040
|
+
if (lower.includes('bini') && hasWordSync)
|
|
1041
|
+
return 2;
|
|
1042
|
+
if (lower.includes('unison') && hasWordSync)
|
|
1043
|
+
return 3;
|
|
1044
|
+
if (isQQ && hasWordSync)
|
|
1045
|
+
return 4;
|
|
1046
|
+
if (lower.includes('musixmatch') && hasWordSync)
|
|
1047
|
+
return 5;
|
|
1048
|
+
if (hasWordSync)
|
|
1049
|
+
return 6;
|
|
1050
|
+
if (lower.includes('apple') && !hasWordSync && !isUnsynced)
|
|
1051
|
+
return 7;
|
|
1052
|
+
if (lower.includes('bini') && !hasWordSync && !isUnsynced)
|
|
1053
|
+
return 8;
|
|
1054
|
+
if (lower.includes('unison') && !hasWordSync && !isUnsynced)
|
|
1055
|
+
return 9;
|
|
1056
|
+
if (isQQ && !hasWordSync && !isUnsynced)
|
|
1057
|
+
return 10;
|
|
1058
|
+
if (lower.includes('musixmatch') && !hasWordSync && !isUnsynced)
|
|
1059
|
+
return 11;
|
|
1060
|
+
if (!hasWordSync && !isUnsynced)
|
|
1061
|
+
return 12;
|
|
1062
|
+
if (lower.includes('apple') && isUnsynced)
|
|
1063
|
+
return 13;
|
|
1064
|
+
if (lower.includes('bini') && isUnsynced)
|
|
1065
|
+
return 14;
|
|
1066
|
+
if (lower.includes('unison') && isUnsynced)
|
|
1067
|
+
return 15;
|
|
1068
|
+
if (isQQ && isUnsynced)
|
|
1069
|
+
return 16;
|
|
1070
|
+
if (lower.includes('musixmatch') && isUnsynced)
|
|
1071
|
+
return 17;
|
|
1072
|
+
return 30;
|
|
1073
|
+
};
|
|
1074
|
+
const allResults = [];
|
|
1075
|
+
if (!skipBiniCache) {
|
|
1076
|
+
const biniResult = await AmLyrics.fetchLyricsFromBiniLyrics(title, artist, isrc, metadata);
|
|
1077
|
+
if (biniResult) {
|
|
1078
|
+
allResults.push(biniResult);
|
|
1079
|
+
return allResults;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1074
1082
|
// Shuffle servers so we pick a random one first, with all others as fallback
|
|
1075
1083
|
// Try up to 3 servers to improve reliability when some have CORS or connectivity issues
|
|
1076
1084
|
const shuffledServers = [...KPOE_SERVERS]
|
|
@@ -1296,6 +1304,73 @@ class AmLyrics extends i {
|
|
|
1296
1304
|
}
|
|
1297
1305
|
return null;
|
|
1298
1306
|
}
|
|
1307
|
+
static async fetchLyricsFromUnison(metadata) {
|
|
1308
|
+
const title = metadata.title?.trim();
|
|
1309
|
+
const artist = metadata.artist?.trim();
|
|
1310
|
+
if (!title || !artist)
|
|
1311
|
+
return null;
|
|
1312
|
+
const params = new URLSearchParams();
|
|
1313
|
+
params.append('song', title);
|
|
1314
|
+
params.append('artist', artist);
|
|
1315
|
+
if (metadata.album) {
|
|
1316
|
+
params.append('album', metadata.album);
|
|
1317
|
+
}
|
|
1318
|
+
if (metadata.durationMs && metadata.durationMs > 0) {
|
|
1319
|
+
params.append('duration', Math.round(metadata.durationMs / 1000).toString());
|
|
1320
|
+
}
|
|
1321
|
+
try {
|
|
1322
|
+
const response = await fetchWithTimeout(`https://unison.boidu.dev/lyrics?${params.toString()}`);
|
|
1323
|
+
if (!response.ok)
|
|
1324
|
+
return null;
|
|
1325
|
+
const data = await response.json();
|
|
1326
|
+
if (!data.success || !data.data?.lyrics)
|
|
1327
|
+
return null;
|
|
1328
|
+
const lyricsData = data.data;
|
|
1329
|
+
const format = lyricsData.format || 'lrc';
|
|
1330
|
+
const syncType = lyricsData.syncType || 'linesync';
|
|
1331
|
+
const lyricsText = lyricsData.lyrics;
|
|
1332
|
+
if (format === 'ttml') {
|
|
1333
|
+
const parseResult = AmLyrics.parseTTML(lyricsText);
|
|
1334
|
+
if (parseResult && parseResult.lines.length > 0) {
|
|
1335
|
+
return {
|
|
1336
|
+
lines: parseResult.lines,
|
|
1337
|
+
source: 'Unison',
|
|
1338
|
+
songwriters: parseResult.songwriters,
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
if (format === 'lrc') {
|
|
1343
|
+
if (syncType === 'plain') {
|
|
1344
|
+
const plainLines = lyricsText
|
|
1345
|
+
.split('\n')
|
|
1346
|
+
.map((l) => l.trim())
|
|
1347
|
+
.filter((l) => l);
|
|
1348
|
+
if (plainLines.length > 0) {
|
|
1349
|
+
const lines = plainLines.map((text) => ({
|
|
1350
|
+
text: [{ text, part: false, timestamp: 0, endtime: 0 }],
|
|
1351
|
+
background: false,
|
|
1352
|
+
backgroundText: [],
|
|
1353
|
+
oppositeTurn: false,
|
|
1354
|
+
timestamp: 0,
|
|
1355
|
+
endtime: 0,
|
|
1356
|
+
isWordSynced: false,
|
|
1357
|
+
}));
|
|
1358
|
+
return { lines, source: 'Unison (unsynced)' };
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
else {
|
|
1362
|
+
const lines = AmLyrics.parseLrcSubtitles(lyricsText);
|
|
1363
|
+
if (lines.length > 0) {
|
|
1364
|
+
return { lines, source: 'Unison' };
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
catch {
|
|
1370
|
+
// Unison fetch failed
|
|
1371
|
+
}
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1299
1374
|
static calculateLineAlignments(lineSingers, agentTypes) {
|
|
1300
1375
|
const lineSideAssignments = new Array(lineSingers.length).fill(undefined);
|
|
1301
1376
|
let currentSideIsLeft = true;
|
|
@@ -1771,25 +1846,35 @@ class AmLyrics extends i {
|
|
|
1771
1846
|
const linesChanged = !AmLyrics.arraysEqual(newActiveLines, oldActiveLines);
|
|
1772
1847
|
if (linesChanged || isSeek) {
|
|
1773
1848
|
if (this.lyricsContainer) {
|
|
1774
|
-
// Remove
|
|
1849
|
+
// Remove .active and .bg-expanded immediately when a line drops.
|
|
1850
|
+
// All visual fading is handled by CSS transitions — no JS delays,
|
|
1851
|
+
// so overlapping lyrics never get stuck with multiple .active lines.
|
|
1775
1852
|
for (const lineIndex of oldActiveLines) {
|
|
1776
1853
|
if (!newActiveLines.includes(lineIndex)) {
|
|
1777
1854
|
const lineElement = this._getLineElement(lineIndex);
|
|
1778
1855
|
if (lineElement) {
|
|
1779
|
-
|
|
1856
|
+
if (isSeek || this.isUserScrolling) {
|
|
1857
|
+
AmLyrics.unfinishSyllables(lineElement);
|
|
1858
|
+
}
|
|
1859
|
+
else {
|
|
1860
|
+
AmLyrics.finishSyllablesUpToTime(lineElement, newTime);
|
|
1861
|
+
}
|
|
1862
|
+
lineElement.classList.remove('active', 'bg-expanded');
|
|
1863
|
+
if (lineElement.classList.contains('pre-active')) {
|
|
1864
|
+
lineElement.classList.remove('pre-active');
|
|
1865
|
+
}
|
|
1780
1866
|
const preIdx = this.preActiveLineElements.indexOf(lineElement);
|
|
1781
1867
|
if (preIdx !== -1)
|
|
1782
1868
|
this.preActiveLineElements.splice(preIdx, 1);
|
|
1783
|
-
AmLyrics.resetSyllables(lineElement);
|
|
1784
1869
|
}
|
|
1785
1870
|
}
|
|
1786
1871
|
}
|
|
1787
|
-
// Add 'active' to newly active lines
|
|
1872
|
+
// Add 'active' and 'bg-expanded' to newly active lines
|
|
1788
1873
|
for (const lineIndex of newActiveLines) {
|
|
1789
1874
|
if (!oldActiveLines.includes(lineIndex)) {
|
|
1790
1875
|
const lineElement = this._getLineElement(lineIndex);
|
|
1791
1876
|
if (lineElement) {
|
|
1792
|
-
lineElement.classList.add('active');
|
|
1877
|
+
lineElement.classList.add('active', 'bg-expanded');
|
|
1793
1878
|
lineElement.classList.remove('pre-active');
|
|
1794
1879
|
const preIdx = this.preActiveLineElements.indexOf(lineElement);
|
|
1795
1880
|
if (preIdx !== -1)
|
|
@@ -1797,13 +1882,24 @@ class AmLyrics extends i {
|
|
|
1797
1882
|
}
|
|
1798
1883
|
}
|
|
1799
1884
|
}
|
|
1800
|
-
|
|
1801
|
-
|
|
1885
|
+
// Remove pre-active from lines that are now active (they no longer
|
|
1886
|
+
// need the unblur preview class) and from lines that dropped.
|
|
1887
|
+
for (const lineElement of this.preActiveLineElements) {
|
|
1888
|
+
const idx = AmLyrics.getLineIndexFromElement(lineElement);
|
|
1889
|
+
if (idx === null ||
|
|
1890
|
+
(!newActiveLines.includes(idx) &&
|
|
1891
|
+
lineElement !== this.currentPrimaryActiveLine)) {
|
|
1892
|
+
lineElement.classList.remove('pre-active');
|
|
1893
|
+
}
|
|
1802
1894
|
}
|
|
1895
|
+
this.preActiveLineElements = this.preActiveLineElements.filter(el => el.classList.contains('pre-active'));
|
|
1803
1896
|
}
|
|
1804
1897
|
this.startAnimationFromTime(newTime);
|
|
1805
|
-
this._handleActiveLineScroll(oldActiveLines, isSeek);
|
|
1806
1898
|
}
|
|
1899
|
+
// Predictive scroll: run on every tick so we scroll *before* the next
|
|
1900
|
+
// line starts, matching YouLyPlus behaviour.
|
|
1901
|
+
this._handleActiveLineScroll(oldActiveLines, isSeek);
|
|
1902
|
+
this.clearPastLineHighlights();
|
|
1807
1903
|
if (this.lyricsContainer) {
|
|
1808
1904
|
// Update syllables in active lines using cached elements
|
|
1809
1905
|
for (const lineIndex of this.activeLineIndices) {
|
|
@@ -1819,8 +1915,10 @@ class AmLyrics extends i {
|
|
|
1819
1915
|
// Imperatively manage gap active state
|
|
1820
1916
|
if (this.gapElementCache.size > 0) {
|
|
1821
1917
|
for (const [, gap] of this.gapElementCache) {
|
|
1822
|
-
const gapStartTime =
|
|
1823
|
-
|
|
1918
|
+
const gapStartTime = gap._cachedStartTime ??
|
|
1919
|
+
parseFloat(gap.getAttribute('data-start-time') || '0');
|
|
1920
|
+
const gapEndTime = gap._cachedEndTime ??
|
|
1921
|
+
parseFloat(gap.getAttribute('data-end-time') || '0');
|
|
1824
1922
|
const shouldBeActive = newTime >= gapStartTime && newTime < gapEndTime;
|
|
1825
1923
|
const isActive = gap.classList.contains('active');
|
|
1826
1924
|
const isExiting = gap.classList.contains('gap-exiting');
|
|
@@ -1951,6 +2049,15 @@ class AmLyrics extends i {
|
|
|
1951
2049
|
const isFooterActive = newTime > lastLyric.endtime + 200; // Snappier 200ms buffer
|
|
1952
2050
|
if (isFooterActive && !footer.classList.contains('active')) {
|
|
1953
2051
|
footer.classList.add('active');
|
|
2052
|
+
// Clear pre-active from the last lyric so it doesn't stay
|
|
2053
|
+
// unblurred when the footer takes over.
|
|
2054
|
+
const lastLine = this.lyricsContainer.querySelector('.lyrics-line:last-of-type');
|
|
2055
|
+
if (lastLine) {
|
|
2056
|
+
lastLine.classList.remove('pre-active');
|
|
2057
|
+
const preIdx = this.preActiveLineElements.indexOf(lastLine);
|
|
2058
|
+
if (preIdx !== -1)
|
|
2059
|
+
this.preActiveLineElements.splice(preIdx, 1);
|
|
2060
|
+
}
|
|
1954
2061
|
if (this.autoScroll &&
|
|
1955
2062
|
!this.isUserScrolling &&
|
|
1956
2063
|
!this.isClickSeeking) {
|
|
@@ -1961,41 +2068,6 @@ class AmLyrics extends i {
|
|
|
1961
2068
|
footer.classList.remove('active');
|
|
1962
2069
|
}
|
|
1963
2070
|
}
|
|
1964
|
-
// Pre-scroll: scroll to upcoming line ~0.5s before it starts
|
|
1965
|
-
if (this.autoScroll &&
|
|
1966
|
-
!this.isUserScrolling &&
|
|
1967
|
-
!this.isClickSeeking &&
|
|
1968
|
-
this.lyrics) {
|
|
1969
|
-
let preActiveLineIndex = null;
|
|
1970
|
-
for (let i = 0; i < this.lyrics.length; i += 1) {
|
|
1971
|
-
const line = this.lyrics[i];
|
|
1972
|
-
const timeUntilStart = line.timestamp - newTime;
|
|
1973
|
-
const nextLineEl = this._getLineElement(i);
|
|
1974
|
-
const isBackToBack = this.activeLineIndices.length > 0;
|
|
1975
|
-
const leadTime = isBackToBack
|
|
1976
|
-
? PRE_SCROLL_LEAD_SHORT_MS
|
|
1977
|
-
: PRE_SCROLL_LEAD_MS;
|
|
1978
|
-
if (timeUntilStart > leadTime) {
|
|
1979
|
-
break;
|
|
1980
|
-
}
|
|
1981
|
-
if (timeUntilStart > 0 && timeUntilStart <= leadTime) {
|
|
1982
|
-
if (nextLineEl) {
|
|
1983
|
-
preActiveLineIndex = i;
|
|
1984
|
-
if (!isBackToBack) {
|
|
1985
|
-
nextLineEl.classList.add('pre-active');
|
|
1986
|
-
if (!this.preActiveLineElements.includes(nextLineEl)) {
|
|
1987
|
-
this.preActiveLineElements.push(nextLineEl);
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1990
|
-
this.clearPreActiveClasses(i);
|
|
1991
|
-
const slowScrollDuration = Math.max(SCROLL_ANIMATION_DURATION_MS, timeUntilStart);
|
|
1992
|
-
this.focusLine(nextLineEl, false, isBackToBack ? 500 : slowScrollDuration);
|
|
1993
|
-
}
|
|
1994
|
-
break;
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
this.clearPreActiveClasses(preActiveLineIndex);
|
|
1998
|
-
}
|
|
1999
2071
|
}
|
|
2000
2072
|
}
|
|
2001
2073
|
updated(changedProperties) {
|
|
@@ -2013,11 +2085,31 @@ class AmLyrics extends i {
|
|
|
2013
2085
|
for (const lineIndex of activeLines) {
|
|
2014
2086
|
const lineEl = this._getLineElement(lineIndex);
|
|
2015
2087
|
if (lineEl)
|
|
2016
|
-
lineEl.classList.add('active');
|
|
2088
|
+
lineEl.classList.add('active', 'bg-expanded');
|
|
2017
2089
|
}
|
|
2018
2090
|
// Trigger a faux time-change so that updateSyllablesForLine fires
|
|
2019
2091
|
// to setup inline syllable CSS wipe animations for whatever the current time is
|
|
2020
2092
|
this._onTimeChanged(0, this.currentTime);
|
|
2093
|
+
// Ensure position classes are applied on initial render if not playing yet
|
|
2094
|
+
if (this.positionedLineElements.length === 0) {
|
|
2095
|
+
const firstLine = this.lyricsContainer.querySelector('.lyrics-line');
|
|
2096
|
+
if (firstLine)
|
|
2097
|
+
this.updatePositionClasses(firstLine);
|
|
2098
|
+
}
|
|
2099
|
+
// Set up IntersectionObserver for viewport virtualization
|
|
2100
|
+
this.visibilityObserver?.disconnect();
|
|
2101
|
+
this.visibilityObserver = new IntersectionObserver(entries => {
|
|
2102
|
+
entries.forEach(entry => {
|
|
2103
|
+
const el = entry.target;
|
|
2104
|
+
el.classList.toggle('far-line', !entry.isIntersecting);
|
|
2105
|
+
});
|
|
2106
|
+
}, {
|
|
2107
|
+
root: this.lyricsContainer,
|
|
2108
|
+
rootMargin: '200px',
|
|
2109
|
+
threshold: 0,
|
|
2110
|
+
});
|
|
2111
|
+
const lines = this.lyricsContainer.querySelectorAll('.lyrics-line');
|
|
2112
|
+
lines.forEach(line => this.visibilityObserver.observe(line));
|
|
2021
2113
|
}
|
|
2022
2114
|
}
|
|
2023
2115
|
// Handle duration reset (-1 stops playback and resets currentTime to 0)
|
|
@@ -2044,6 +2136,14 @@ class AmLyrics extends i {
|
|
|
2044
2136
|
clearTimeout(this.userScrollTimeoutId);
|
|
2045
2137
|
this.userScrollTimeoutId = undefined;
|
|
2046
2138
|
}
|
|
2139
|
+
if (this.scrollUnlockTimeout) {
|
|
2140
|
+
clearTimeout(this.scrollUnlockTimeout);
|
|
2141
|
+
this.scrollUnlockTimeout = undefined;
|
|
2142
|
+
}
|
|
2143
|
+
if (this.scrollAnimationTimeout) {
|
|
2144
|
+
clearTimeout(this.scrollAnimationTimeout);
|
|
2145
|
+
this.scrollAnimationTimeout = undefined;
|
|
2146
|
+
}
|
|
2047
2147
|
// Scroll to top
|
|
2048
2148
|
if (this.lyricsContainer) {
|
|
2049
2149
|
this.lyricsContainer.scrollTop = 0;
|
|
@@ -2065,30 +2165,56 @@ class AmLyrics extends i {
|
|
|
2065
2165
|
/**
|
|
2066
2166
|
* Handle scrolling when active line indices change.
|
|
2067
2167
|
* Called imperatively from _onTimeChanged instead of from updated().
|
|
2168
|
+
*
|
|
2169
|
+
* Uses predictive scroll like YouLyPlus: computes a scrollLookAheadMs based
|
|
2170
|
+
* on the gap to the next line, finds the primary line at predictiveTime,
|
|
2171
|
+
* and scrolls with a duration matching the lookahead.
|
|
2068
2172
|
*/
|
|
2069
2173
|
_handleActiveLineScroll(_oldActiveIndices, forceScroll = false) {
|
|
2070
|
-
if (this.
|
|
2174
|
+
if (!this.lyricsContainer || !this.lyrics || this.lyrics.length === 0) {
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
// If the footer is already active, it set up its own scroll.
|
|
2178
|
+
// Don't override it with a scroll back to the last lyric.
|
|
2179
|
+
const footer = this.lyricsContainer.querySelector('.lyrics-footer');
|
|
2180
|
+
if (footer?.classList.contains('active')) {
|
|
2071
2181
|
return;
|
|
2072
2182
|
}
|
|
2073
|
-
|
|
2074
|
-
|
|
2183
|
+
// 1. Compute scroll lookahead based on gap to next line (YouLyPlus style)
|
|
2184
|
+
let scrollLookAheadMs = 350;
|
|
2185
|
+
const currentAudioIndex = this.getLineIndexAtTime(this.currentTime, this.lastActiveIndex);
|
|
2186
|
+
if (currentAudioIndex !== -1 &&
|
|
2187
|
+
currentAudioIndex + 1 < this.lyrics.length) {
|
|
2188
|
+
const currentLine = this.lyrics[currentAudioIndex];
|
|
2189
|
+
const nextLine = this.lyrics[currentAudioIndex + 1];
|
|
2190
|
+
const gap = nextLine.timestamp - currentLine.endtime;
|
|
2191
|
+
scrollLookAheadMs = Math.min(500, Math.max(350, gap));
|
|
2192
|
+
}
|
|
2193
|
+
// 2. Find scroll target at predictive time
|
|
2194
|
+
const predictiveTime = this.currentTime + scrollLookAheadMs;
|
|
2195
|
+
const predictiveActiveIndices = this.findActiveLineIndices(predictiveTime);
|
|
2196
|
+
let targetLineIndex;
|
|
2197
|
+
if (predictiveActiveIndices.length > 0) {
|
|
2198
|
+
targetLineIndex = this.getPrimaryScrollLineIndex(predictiveActiveIndices, predictiveTime);
|
|
2199
|
+
}
|
|
2200
|
+
else {
|
|
2201
|
+
// Fallback: closest line before predictiveTime
|
|
2202
|
+
targetLineIndex = this.getLineIndexAtTime(predictiveTime, 0);
|
|
2203
|
+
}
|
|
2204
|
+
if (targetLineIndex === null || targetLineIndex === -1)
|
|
2075
2205
|
return;
|
|
2076
2206
|
const targetLine = this._getLineElement(targetLineIndex);
|
|
2077
2207
|
if (!targetLine)
|
|
2078
2208
|
return;
|
|
2079
|
-
//
|
|
2080
|
-
//
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
this.lyrics) {
|
|
2086
|
-
const gap = this.lyrics[targetLineIndex].timestamp -
|
|
2087
|
-
this.lyrics[prevPrimaryIndex].endtime;
|
|
2088
|
-
if (gap > 200) {
|
|
2089
|
-
scrollDuration = Math.min(Math.max(gap * 0.85, SCROLL_ANIMATION_DURATION_MS), 4000);
|
|
2209
|
+
// Unblur the upcoming target line early (pre-active) so background
|
|
2210
|
+
// vocals start their max-height/opacity transition in sync with scroll.
|
|
2211
|
+
if (!targetLine.classList.contains('active')) {
|
|
2212
|
+
targetLine.classList.add('pre-active');
|
|
2213
|
+
if (!this.preActiveLineElements.includes(targetLine)) {
|
|
2214
|
+
this.preActiveLineElements.push(targetLine);
|
|
2090
2215
|
}
|
|
2091
2216
|
}
|
|
2217
|
+
const scrollDuration = scrollLookAheadMs;
|
|
2092
2218
|
this.focusLine(targetLine, forceScroll, scrollDuration);
|
|
2093
2219
|
}
|
|
2094
2220
|
_getTextWidth(text, font) {
|
|
@@ -2109,6 +2235,7 @@ class AmLyrics extends i {
|
|
|
2109
2235
|
return;
|
|
2110
2236
|
this.lineElementCache.clear();
|
|
2111
2237
|
this.gapElementCache.clear();
|
|
2238
|
+
this.cachedLineArray = [];
|
|
2112
2239
|
if (!this.lyrics)
|
|
2113
2240
|
return;
|
|
2114
2241
|
for (let i = 0; i < this.lyrics.length; i += 1) {
|
|
@@ -2116,9 +2243,16 @@ class AmLyrics extends i {
|
|
|
2116
2243
|
if (lineEl)
|
|
2117
2244
|
this.lineElementCache.set(i, lineEl);
|
|
2118
2245
|
const gapEl = this.lyricsContainer.querySelector(`#gap-${i}`);
|
|
2119
|
-
if (gapEl)
|
|
2246
|
+
if (gapEl) {
|
|
2247
|
+
// Cache numeric timing values to avoid parseFloat on every frame
|
|
2248
|
+
gapEl._cachedStartTime = parseFloat(gapEl.getAttribute('data-start-time') || '0');
|
|
2249
|
+
gapEl._cachedEndTime = parseFloat(gapEl.getAttribute('data-end-time') || '0');
|
|
2120
2250
|
this.gapElementCache.set(i, gapEl);
|
|
2251
|
+
}
|
|
2121
2252
|
}
|
|
2253
|
+
// Rebuild cached line array for scroll/position queries
|
|
2254
|
+
const lineElements = this.lyricsContainer.querySelectorAll('.lyrics-line');
|
|
2255
|
+
this.cachedLineArray = Array.from(lineElements);
|
|
2122
2256
|
}
|
|
2123
2257
|
_getLineElement(index) {
|
|
2124
2258
|
const cached = this.lineElementCache.get(index);
|
|
@@ -2148,9 +2282,13 @@ class AmLyrics extends i {
|
|
|
2148
2282
|
this.cachedLineData = null;
|
|
2149
2283
|
this.lineElementCache.clear();
|
|
2150
2284
|
this.gapElementCache.clear();
|
|
2285
|
+
this.cachedLineArray = [];
|
|
2286
|
+
this.cachedScrollPaddingTop = null;
|
|
2151
2287
|
this.preActiveLineElements = [];
|
|
2152
2288
|
this.positionedLineElements = [];
|
|
2153
2289
|
this.activeGapLineElements = [];
|
|
2290
|
+
this.visibilityObserver?.disconnect();
|
|
2291
|
+
this.visibilityObserver = undefined;
|
|
2154
2292
|
}
|
|
2155
2293
|
_updateCachedIsUnsynced() {
|
|
2156
2294
|
this.cachedIsUnsynced =
|
|
@@ -2183,6 +2321,7 @@ class AmLyrics extends i {
|
|
|
2183
2321
|
}
|
|
2184
2322
|
const groupGrowable = new Array(wordGroups.length).fill(false);
|
|
2185
2323
|
const groupGlowing = new Array(wordGroups.length).fill(false);
|
|
2324
|
+
const groupCharRise = new Array(wordGroups.length).fill(false);
|
|
2186
2325
|
const vwFullText = new Array(wordGroups.length).fill('');
|
|
2187
2326
|
const vwFullDuration = new Array(wordGroups.length).fill(0);
|
|
2188
2327
|
const vwCharOffset = new Array(wordGroups.length).fill(0);
|
|
@@ -2227,10 +2366,12 @@ class AmLyrics extends i {
|
|
|
2227
2366
|
}
|
|
2228
2367
|
const isLineSynced = line.isWordSynced === false || line.text.some(s => s.lineSynced);
|
|
2229
2368
|
const isGlowingVW = isGrowableVW && !isLineSynced;
|
|
2369
|
+
const isCharRiseVW = !isGrowableVW && !isLineSynced && !isCJK && !isRTL && wordLen >= 8;
|
|
2230
2370
|
let charOff = 0;
|
|
2231
2371
|
for (let gi = vwStart; gi <= vwEnd; gi += 1) {
|
|
2232
2372
|
groupGrowable[gi] = isGrowableVW;
|
|
2233
2373
|
groupGlowing[gi] = isGlowingVW;
|
|
2374
|
+
groupCharRise[gi] = isCharRiseVW;
|
|
2234
2375
|
vwFullText[gi] = combinedText;
|
|
2235
2376
|
vwFullDuration[gi] = combinedDuration;
|
|
2236
2377
|
vwCharOffset[gi] = charOff;
|
|
@@ -2245,6 +2386,7 @@ class AmLyrics extends i {
|
|
|
2245
2386
|
wordGroups,
|
|
2246
2387
|
groupGrowable,
|
|
2247
2388
|
groupGlowing,
|
|
2389
|
+
groupCharRise,
|
|
2248
2390
|
vwFullText,
|
|
2249
2391
|
vwFullDuration,
|
|
2250
2392
|
vwCharOffset,
|
|
@@ -2265,10 +2407,10 @@ class AmLyrics extends i {
|
|
|
2265
2407
|
const computedStyle = getComputedStyle(referenceSyllable);
|
|
2266
2408
|
const { font } = computedStyle; // Full font string
|
|
2267
2409
|
const fontSize = parseFloat(computedStyle.fontSize);
|
|
2268
|
-
const
|
|
2269
|
-
if (!
|
|
2410
|
+
const charTimedWords = this.shadowRoot.querySelectorAll('.lyrics-word.growable, .lyrics-word.char-rise');
|
|
2411
|
+
if (!charTimedWords)
|
|
2270
2412
|
return;
|
|
2271
|
-
|
|
2413
|
+
charTimedWords.forEach((wordSpan) => {
|
|
2272
2414
|
const syllableWraps = wordSpan.querySelectorAll('.lyrics-syllable-wrap');
|
|
2273
2415
|
// Flatten syllables
|
|
2274
2416
|
const syllables = [];
|
|
@@ -2351,21 +2493,88 @@ class AmLyrics extends i {
|
|
|
2351
2493
|
let candidateIndex = Math.max(groupStart, groupEnd - 2);
|
|
2352
2494
|
const currentPrimaryIndex = AmLyrics.getLineIndexFromElement(this.currentPrimaryActiveLine);
|
|
2353
2495
|
if (currentPrimaryIndex !== null &&
|
|
2354
|
-
activeIndices.includes(currentPrimaryIndex)
|
|
2355
|
-
|
|
2356
|
-
|
|
2496
|
+
activeIndices.includes(currentPrimaryIndex)) {
|
|
2497
|
+
if (activeIndices.length <= 3) {
|
|
2498
|
+
candidateIndex = currentPrimaryIndex;
|
|
2499
|
+
}
|
|
2500
|
+
else if (candidateIndex < currentPrimaryIndex) {
|
|
2501
|
+
candidateIndex = currentPrimaryIndex;
|
|
2502
|
+
}
|
|
2357
2503
|
}
|
|
2358
2504
|
return candidateIndex;
|
|
2359
2505
|
}
|
|
2360
|
-
|
|
2506
|
+
getPrimaryScrollLineIndex(_activeIndices, time) {
|
|
2507
|
+
if (!this.lyrics || this.lyrics.length === 0)
|
|
2508
|
+
return null;
|
|
2509
|
+
// YouLyPlus-style: primary is simply the line at predictive time.
|
|
2510
|
+
const primaryIndex = this.getLineIndexAtTime(time, this.lastActiveIndex);
|
|
2511
|
+
if (primaryIndex === -1)
|
|
2512
|
+
return null;
|
|
2513
|
+
// Guard: if new primary is ahead of current but they share the same
|
|
2514
|
+
// end time, keep current to prevent bounce during overlaps.
|
|
2515
|
+
const currentPrimaryIndex = AmLyrics.getLineIndexFromElement(this.currentPrimaryActiveLine);
|
|
2516
|
+
if (currentPrimaryIndex !== null &&
|
|
2517
|
+
primaryIndex > currentPrimaryIndex &&
|
|
2518
|
+
this.lyrics[currentPrimaryIndex] &&
|
|
2519
|
+
this.lyrics[primaryIndex] &&
|
|
2520
|
+
this.lyrics[currentPrimaryIndex].endtime ===
|
|
2521
|
+
this.lyrics[primaryIndex].endtime) {
|
|
2522
|
+
const activeCount = this.findActiveLineIndices(time).length;
|
|
2523
|
+
if (activeCount <= 3) {
|
|
2524
|
+
return currentPrimaryIndex;
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
return primaryIndex;
|
|
2528
|
+
}
|
|
2529
|
+
getOverlapClusterForActiveIndices(activeIndices, time) {
|
|
2530
|
+
if (!this.lyrics || activeIndices.length === 0)
|
|
2531
|
+
return null;
|
|
2532
|
+
let start = activeIndices[0];
|
|
2533
|
+
while (start > 0 &&
|
|
2534
|
+
this.lyrics[start - 1].endtime >= this.lyrics[start].timestamp) {
|
|
2535
|
+
start -= 1;
|
|
2536
|
+
}
|
|
2537
|
+
let end = start;
|
|
2538
|
+
let clusterEndTime = this.lyrics[start].endtime;
|
|
2539
|
+
while (end + 1 < this.lyrics.length &&
|
|
2540
|
+
this.lyrics[end + 1].timestamp <= clusterEndTime) {
|
|
2541
|
+
end += 1;
|
|
2542
|
+
clusterEndTime = Math.max(clusterEndTime, this.lyrics[end].endtime);
|
|
2543
|
+
}
|
|
2544
|
+
let startedEnd = start;
|
|
2545
|
+
let startedEndTime = this.lyrics[start].endtime;
|
|
2546
|
+
for (let i = start; i <= end; i += 1) {
|
|
2547
|
+
if (this.lyrics[i].timestamp <= time) {
|
|
2548
|
+
startedEnd = i;
|
|
2549
|
+
startedEndTime = Math.max(startedEndTime, this.lyrics[i].endtime);
|
|
2550
|
+
}
|
|
2551
|
+
else {
|
|
2552
|
+
break;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
return { start, end, startedEnd, startedEndTime };
|
|
2556
|
+
}
|
|
2557
|
+
focusLine(lineElement, forceScroll = false, scrollDuration = undefined, skipScroll = false, preservePrimary = false) {
|
|
2361
2558
|
const primaryChanged = lineElement !== this.currentPrimaryActiveLine;
|
|
2362
|
-
if (primaryChanged) {
|
|
2559
|
+
if (primaryChanged && !preservePrimary) {
|
|
2560
|
+
// .active is now managed solely by findActiveLineIndices (which uses
|
|
2561
|
+
// effectiveEndTimes). Lines stay active until their extended end,
|
|
2562
|
+
// so we no longer need to remove .active here.
|
|
2363
2563
|
this.lastPrimaryActiveLine = this.currentPrimaryActiveLine;
|
|
2364
2564
|
this.currentPrimaryActiveLine = lineElement;
|
|
2565
|
+
const lineIndex = AmLyrics.getLineIndexFromElement(lineElement);
|
|
2566
|
+
if (lineIndex !== null) {
|
|
2567
|
+
this.lastActiveIndex = lineIndex;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
// Only update blur/opacity position classes when the primary line
|
|
2571
|
+
// actually changes (or on force scroll). Running this every tick
|
|
2572
|
+
// causes visual churn and upward glitches.
|
|
2573
|
+
if (primaryChanged || forceScroll) {
|
|
2574
|
+
this.updatePositionClasses(lineElement);
|
|
2365
2575
|
}
|
|
2366
|
-
this.updatePositionClasses(lineElement);
|
|
2367
2576
|
if (!skipScroll &&
|
|
2368
|
-
(forceScroll || primaryChanged) &&
|
|
2577
|
+
(forceScroll || primaryChanged || preservePrimary) &&
|
|
2369
2578
|
this.autoScroll &&
|
|
2370
2579
|
!this.isUserScrolling &&
|
|
2371
2580
|
!this.isClickSeeking) {
|
|
@@ -2388,6 +2597,7 @@ class AmLyrics extends i {
|
|
|
2388
2597
|
}
|
|
2389
2598
|
// Mark that user is currently scrolling
|
|
2390
2599
|
this.setUserScrolling(true);
|
|
2600
|
+
this.clearPastLineHighlights();
|
|
2391
2601
|
// Clear any existing timeout
|
|
2392
2602
|
if (this.userScrollTimeoutId) {
|
|
2393
2603
|
clearTimeout(this.userScrollTimeoutId);
|
|
@@ -2398,29 +2608,75 @@ class AmLyrics extends i {
|
|
|
2398
2608
|
this.userScrollTimeoutId = undefined;
|
|
2399
2609
|
// Optionally scroll back to current active line when re-enabling auto-scroll
|
|
2400
2610
|
if (this.activeLineIndices.length > 0) {
|
|
2401
|
-
this.
|
|
2611
|
+
this._handleActiveLineScroll([], false);
|
|
2402
2612
|
}
|
|
2403
2613
|
}, 2000);
|
|
2404
2614
|
}
|
|
2615
|
+
clearPastLineHighlights() {
|
|
2616
|
+
if (!this.lyricsContainer)
|
|
2617
|
+
return;
|
|
2618
|
+
const lineElements = this.cachedLineArray.length
|
|
2619
|
+
? this.cachedLineArray
|
|
2620
|
+
: Array.from(this.lyricsContainer.querySelectorAll('.lyrics-line:not(.lyrics-gap)'));
|
|
2621
|
+
const containerRect = this.lyricsContainer.getBoundingClientRect();
|
|
2622
|
+
const anchorY = containerRect.top + this.getScrollPaddingTop();
|
|
2623
|
+
for (let i = 0; i < lineElements.length; i += 1) {
|
|
2624
|
+
const lineElement = lineElements[i];
|
|
2625
|
+
const isActive = lineElement.classList.contains('active');
|
|
2626
|
+
const lineRect = lineElement.getBoundingClientRect();
|
|
2627
|
+
const hasScrolledPast = lineRect.bottom < anchorY - 2;
|
|
2628
|
+
if (!isActive && hasScrolledPast) {
|
|
2629
|
+
AmLyrics.unfinishSyllables(lineElement);
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
/**
|
|
2634
|
+
* Find the first (lowest-index) line whose raw time range contains `timeMs`.
|
|
2635
|
+
* Uses a stable forward scan so overlapping ranges always return the same
|
|
2636
|
+
* line, preventing primary-target jitter that causes scroll glitches.
|
|
2637
|
+
*/
|
|
2638
|
+
getLineIndexAtTime(timeMs, startHintIndex = 0) {
|
|
2639
|
+
if (!this.lyrics || this.lyrics.length === 0)
|
|
2640
|
+
return -1;
|
|
2641
|
+
const len = this.lyrics.length;
|
|
2642
|
+
// 1. Check hint and immediate neighbours first (fast path)
|
|
2643
|
+
const hint = Math.max(0, Math.min(startHintIndex, len - 1));
|
|
2644
|
+
for (let i = hint; i < len; i += 1) {
|
|
2645
|
+
const line = this.lyrics[i];
|
|
2646
|
+
if (line.timestamp > timeMs)
|
|
2647
|
+
break;
|
|
2648
|
+
if (timeMs >= line.timestamp && timeMs < line.endtime) {
|
|
2649
|
+
return i;
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
for (let i = hint - 1; i >= 0; i -= 1) {
|
|
2653
|
+
const line = this.lyrics[i];
|
|
2654
|
+
if (timeMs >= line.timestamp && timeMs < line.endtime) {
|
|
2655
|
+
return i;
|
|
2656
|
+
}
|
|
2657
|
+
if (line.endtime < timeMs)
|
|
2658
|
+
break;
|
|
2659
|
+
}
|
|
2660
|
+
// 2. Full forward scan — guaranteed deterministic for overlaps
|
|
2661
|
+
for (let i = 0; i < len; i += 1) {
|
|
2662
|
+
const line = this.lyrics[i];
|
|
2663
|
+
if (line.timestamp > timeMs)
|
|
2664
|
+
break;
|
|
2665
|
+
if (timeMs >= line.timestamp && timeMs < line.endtime) {
|
|
2666
|
+
return i;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
return -1;
|
|
2670
|
+
}
|
|
2405
2671
|
findActiveLineIndices(time) {
|
|
2406
2672
|
if (!this.lyrics || this.lyrics.length === 0)
|
|
2407
2673
|
return [];
|
|
2408
2674
|
const activeLines = [];
|
|
2409
2675
|
for (let i = 0; i < this.lyrics.length; i += 1) {
|
|
2410
2676
|
const line = this.lyrics[i];
|
|
2411
|
-
let effectiveEndTime = line.endtime;
|
|
2412
|
-
if (i < this.lyrics.length - 1) {
|
|
2413
|
-
const nextLineStart = this.lyrics[i + 1].timestamp;
|
|
2414
|
-
const gapDuration = nextLineStart - line.endtime;
|
|
2415
|
-
if (gapDuration < INSTRUMENTAL_THRESHOLD_MS) {
|
|
2416
|
-
if (effectiveEndTime < nextLineStart) {
|
|
2417
|
-
effectiveEndTime = Math.max(effectiveEndTime, nextLineStart - 500);
|
|
2418
|
-
}
|
|
2419
|
-
}
|
|
2420
|
-
}
|
|
2421
2677
|
if (line.timestamp > time)
|
|
2422
2678
|
break;
|
|
2423
|
-
if (time >= line.timestamp && time
|
|
2679
|
+
if (time >= line.timestamp && time < line.endtime) {
|
|
2424
2680
|
activeLines.push(i);
|
|
2425
2681
|
}
|
|
2426
2682
|
}
|
|
@@ -2630,10 +2886,6 @@ class AmLyrics extends i {
|
|
|
2630
2886
|
this.scrollAnimationState.pendingUpdate = null;
|
|
2631
2887
|
}
|
|
2632
2888
|
// Clear scroll animation timeouts
|
|
2633
|
-
if (this.scrollUnlockTimeout) {
|
|
2634
|
-
clearTimeout(this.scrollUnlockTimeout);
|
|
2635
|
-
this.scrollUnlockTimeout = undefined;
|
|
2636
|
-
}
|
|
2637
2889
|
if (this.scrollAnimationTimeout) {
|
|
2638
2890
|
clearTimeout(this.scrollAnimationTimeout);
|
|
2639
2891
|
this.scrollAnimationTimeout = undefined;
|
|
@@ -2728,6 +2980,7 @@ class AmLyrics extends i {
|
|
|
2728
2980
|
const paddingTop = this.getScrollPaddingTop();
|
|
2729
2981
|
const targetTranslateY = paddingTop - gapTarget.offsetTop;
|
|
2730
2982
|
this.isProgrammaticScroll = true;
|
|
2983
|
+
this.clearPastLineHighlights();
|
|
2731
2984
|
this.animateScrollYouLy(targetTranslateY, false);
|
|
2732
2985
|
setTimeout(() => {
|
|
2733
2986
|
this.isProgrammaticScroll = false;
|
|
@@ -2739,14 +2992,22 @@ class AmLyrics extends i {
|
|
|
2739
2992
|
* Get the scroll padding top value from CSS variable
|
|
2740
2993
|
*/
|
|
2741
2994
|
getScrollPaddingTop() {
|
|
2995
|
+
if (this.cachedScrollPaddingTop !== null)
|
|
2996
|
+
return this.cachedScrollPaddingTop;
|
|
2742
2997
|
if (!this.lyricsContainer)
|
|
2743
2998
|
return 0;
|
|
2744
2999
|
const style = getComputedStyle(this);
|
|
2745
3000
|
const paddingTopValue = style.getPropertyValue('--lyrics-scroll-padding-top') || '25%';
|
|
3001
|
+
let result;
|
|
2746
3002
|
if (paddingTopValue.includes('%')) {
|
|
2747
|
-
|
|
3003
|
+
result =
|
|
3004
|
+
this.lyricsContainer.clientHeight * (parseFloat(paddingTopValue) / 100);
|
|
2748
3005
|
}
|
|
2749
|
-
|
|
3006
|
+
else {
|
|
3007
|
+
result = parseFloat(paddingTopValue) || 0;
|
|
3008
|
+
}
|
|
3009
|
+
this.cachedScrollPaddingTop = result;
|
|
3010
|
+
return result;
|
|
2750
3011
|
}
|
|
2751
3012
|
/**
|
|
2752
3013
|
* Animate scroll with staggered delay for smooth YouLyPlus-style scrolling
|
|
@@ -2755,6 +3016,7 @@ class AmLyrics extends i {
|
|
|
2755
3016
|
if (!this.lyricsContainer)
|
|
2756
3017
|
return;
|
|
2757
3018
|
const parent = this.lyricsContainer;
|
|
3019
|
+
const targetTop = Math.max(0, -newTranslateY);
|
|
2758
3020
|
if (!this.scrollAnimationState) {
|
|
2759
3021
|
this.scrollAnimationState = {
|
|
2760
3022
|
isAnimating: false,
|
|
@@ -2764,19 +3026,25 @@ class AmLyrics extends i {
|
|
|
2764
3026
|
}
|
|
2765
3027
|
const animState = this.scrollAnimationState;
|
|
2766
3028
|
if (animState.isAnimating && !forceScroll) {
|
|
3029
|
+
const pendingTop = animState.pendingUpdate === null
|
|
3030
|
+
? null
|
|
3031
|
+
: Math.max(0, -animState.pendingUpdate);
|
|
3032
|
+
if (Math.abs(parent.scrollTop - targetTop) < 2 ||
|
|
3033
|
+
(pendingTop !== null && Math.abs(pendingTop - targetTop) < 2)) {
|
|
3034
|
+
return;
|
|
3035
|
+
}
|
|
2767
3036
|
animState.pendingUpdate = newTranslateY;
|
|
2768
3037
|
return;
|
|
2769
3038
|
}
|
|
2770
|
-
if (this.scrollUnlockTimeout) {
|
|
2771
|
-
clearTimeout(this.scrollUnlockTimeout);
|
|
2772
|
-
this.scrollUnlockTimeout = undefined;
|
|
2773
|
-
}
|
|
2774
3039
|
if (this.scrollAnimationTimeout) {
|
|
2775
3040
|
clearTimeout(this.scrollAnimationTimeout);
|
|
2776
3041
|
this.scrollAnimationTimeout = undefined;
|
|
2777
3042
|
}
|
|
3043
|
+
if (this.scrollUnlockTimeout) {
|
|
3044
|
+
clearTimeout(this.scrollUnlockTimeout);
|
|
3045
|
+
this.scrollUnlockTimeout = undefined;
|
|
3046
|
+
}
|
|
2778
3047
|
const { animatingLines } = this;
|
|
2779
|
-
const targetTop = Math.max(0, -newTranslateY);
|
|
2780
3048
|
const appliedTranslateY = -targetTop;
|
|
2781
3049
|
const prevOffset = -parent.scrollTop;
|
|
2782
3050
|
const delta = prevOffset - appliedTranslateY;
|
|
@@ -2791,7 +3059,6 @@ class AmLyrics extends i {
|
|
|
2791
3059
|
// Clean up any lingering scroll animations before smooth scroll
|
|
2792
3060
|
for (const line of animatingLines) {
|
|
2793
3061
|
line.classList.remove('scroll-animate');
|
|
2794
|
-
line.style.removeProperty('will-change');
|
|
2795
3062
|
line.style.removeProperty('--scroll-delta');
|
|
2796
3063
|
line.style.removeProperty('--lyrics-line-delay');
|
|
2797
3064
|
line.style.removeProperty('--scroll-duration');
|
|
@@ -2802,14 +3069,21 @@ class AmLyrics extends i {
|
|
|
2802
3069
|
animState.pendingUpdate = null;
|
|
2803
3070
|
return;
|
|
2804
3071
|
}
|
|
2805
|
-
// --- Step 1: Remove scroll-animate
|
|
3072
|
+
// --- Step 1: Remove scroll-animate and custom properties from ALL
|
|
3073
|
+
// previously animating lines so stale deltas don't interfere. ---
|
|
2806
3074
|
for (const line of animatingLines) {
|
|
2807
3075
|
line.classList.remove('scroll-animate');
|
|
3076
|
+
line.style.removeProperty('--scroll-delta');
|
|
3077
|
+
line.style.removeProperty('--lyrics-line-delay');
|
|
3078
|
+
line.style.removeProperty('--scroll-duration');
|
|
2808
3079
|
}
|
|
2809
3080
|
animatingLines.length = 0;
|
|
2810
|
-
// Get lines for staggered animation
|
|
2811
|
-
|
|
2812
|
-
|
|
3081
|
+
// Get lines for staggered animation — use cached array
|
|
3082
|
+
if (this.cachedLineArray.length === 0) {
|
|
3083
|
+
const lineElements = this.lyricsContainer.querySelectorAll('.lyrics-line');
|
|
3084
|
+
this.cachedLineArray = Array.from(lineElements);
|
|
3085
|
+
}
|
|
3086
|
+
const lineArray = this.cachedLineArray;
|
|
2813
3087
|
const referenceLine = this.currentPrimaryActiveLine ||
|
|
2814
3088
|
this.lastPrimaryActiveLine ||
|
|
2815
3089
|
lineArray[0];
|
|
@@ -2818,43 +3092,64 @@ class AmLyrics extends i {
|
|
|
2818
3092
|
const referenceIndex = lineArray.indexOf(referenceLine);
|
|
2819
3093
|
if (referenceIndex === -1)
|
|
2820
3094
|
return;
|
|
2821
|
-
const
|
|
2822
|
-
const
|
|
2823
|
-
const lookAhead =
|
|
3095
|
+
const duration = Math.min(450, scrollDuration ?? SCROLL_ANIMATION_DURATION_MS);
|
|
3096
|
+
const delayIncrement = duration * 0.1;
|
|
3097
|
+
const lookAhead = 20;
|
|
2824
3098
|
const len = lineArray.length;
|
|
2825
|
-
const start = Math.max(0, referenceIndex -
|
|
3099
|
+
const start = Math.max(0, referenceIndex - lookAhead);
|
|
2826
3100
|
const end = Math.min(len, referenceIndex + lookAhead);
|
|
2827
3101
|
let maxAnimationDuration = 0;
|
|
2828
|
-
let delayCounter = 0;
|
|
2829
|
-
// --- Step 2: Set CSS custom properties on target lines ---
|
|
2830
3102
|
const newAnimatingLines = [];
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
3103
|
+
const scrollingDown = delta >= 0;
|
|
3104
|
+
if (scrollingDown) {
|
|
3105
|
+
let delayCounter = 0;
|
|
3106
|
+
for (let i = start; i < end; i += 1) {
|
|
3107
|
+
const line = lineArray[i];
|
|
3108
|
+
const delay = i >= referenceIndex ? delayCounter * delayIncrement : 0;
|
|
3109
|
+
if (i >= referenceIndex && !line.classList.contains('lyrics-gap')) {
|
|
3110
|
+
delayCounter += 1;
|
|
3111
|
+
}
|
|
3112
|
+
line.style.setProperty('--scroll-delta', `${delta}px`);
|
|
3113
|
+
line.style.setProperty('--lyrics-line-delay', `${delay}ms`);
|
|
3114
|
+
line.style.setProperty('--scroll-duration', `${duration + 100}ms`);
|
|
3115
|
+
newAnimatingLines.push(line);
|
|
3116
|
+
const lineDuration = duration + delay;
|
|
3117
|
+
if (lineDuration > maxAnimationDuration) {
|
|
3118
|
+
maxAnimationDuration = lineDuration;
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
else {
|
|
3123
|
+
let delayCounter = 0;
|
|
3124
|
+
for (let i = end - 1; i >= start; i -= 1) {
|
|
3125
|
+
const line = lineArray[i];
|
|
3126
|
+
const delay = i <= referenceIndex ? delayCounter * delayIncrement : 0;
|
|
3127
|
+
if (i <= referenceIndex && !line.classList.contains('lyrics-gap')) {
|
|
3128
|
+
delayCounter += 1;
|
|
3129
|
+
}
|
|
3130
|
+
line.style.setProperty('--scroll-delta', `${delta}px`);
|
|
3131
|
+
line.style.setProperty('--lyrics-line-delay', `${delay}ms`);
|
|
3132
|
+
line.style.setProperty('--scroll-duration', `${duration + 100}ms`);
|
|
3133
|
+
newAnimatingLines.push(line);
|
|
3134
|
+
const lineDuration = duration + delay;
|
|
3135
|
+
if (lineDuration > maxAnimationDuration) {
|
|
3136
|
+
maxAnimationDuration = lineDuration;
|
|
3137
|
+
}
|
|
2844
3138
|
}
|
|
2845
3139
|
}
|
|
2846
3140
|
// --- Step 3: Force reflow so the browser sees the class removal ---
|
|
2847
|
-
//
|
|
2848
|
-
//
|
|
2849
|
-
parent.
|
|
3141
|
+
// Use offsetHeight which is cheaper than getBoundingClientRect
|
|
3142
|
+
// eslint-disable-next-line no-void
|
|
3143
|
+
void parent.offsetHeight;
|
|
2850
3144
|
// --- Step 4: Re-add scroll-animate class to start fresh animations ---
|
|
2851
3145
|
for (const line of newAnimatingLines) {
|
|
2852
3146
|
line.classList.add('scroll-animate');
|
|
2853
|
-
line.style.willChange = 'transform';
|
|
2854
3147
|
animatingLines.push(line);
|
|
2855
3148
|
}
|
|
2856
3149
|
animState.isAnimating = true;
|
|
2857
|
-
|
|
3150
|
+
// YouLyPlus-style early unlock: allow new scrolls to start after a
|
|
3151
|
+
// short base duration, even if CSS animations are still running.
|
|
3152
|
+
const BASE_DURATION = 400;
|
|
2858
3153
|
this.scrollUnlockTimeout = setTimeout(() => {
|
|
2859
3154
|
animState.isAnimating = false;
|
|
2860
3155
|
if (animState.pendingUpdate !== null) {
|
|
@@ -2867,7 +3162,6 @@ class AmLyrics extends i {
|
|
|
2867
3162
|
for (let i = 0; i < animatingLines.length; i += 1) {
|
|
2868
3163
|
const line = animatingLines[i];
|
|
2869
3164
|
line.classList.remove('scroll-animate');
|
|
2870
|
-
line.style.removeProperty('will-change');
|
|
2871
3165
|
line.style.removeProperty('--scroll-delta');
|
|
2872
3166
|
line.style.removeProperty('--lyrics-line-delay');
|
|
2873
3167
|
line.style.removeProperty('--scroll-duration');
|
|
@@ -2904,8 +3198,13 @@ class AmLyrics extends i {
|
|
|
2904
3198
|
// Add new position classes
|
|
2905
3199
|
lineToScroll.classList.add('lyrics-activest');
|
|
2906
3200
|
this.positionedLineElements.push(lineToScroll);
|
|
2907
|
-
|
|
3201
|
+
if (this.cachedLineArray.length === 0) {
|
|
3202
|
+
this.cachedLineArray = Array.from(this.lyricsContainer.querySelectorAll('.lyrics-line'));
|
|
3203
|
+
}
|
|
3204
|
+
const lineElements = this.cachedLineArray;
|
|
2908
3205
|
const scrollLineIndex = lineElements.indexOf(lineToScroll);
|
|
3206
|
+
if (scrollLineIndex === -1)
|
|
3207
|
+
return;
|
|
2909
3208
|
for (let i = Math.max(0, scrollLineIndex - 4); i <= Math.min(lineElements.length - 1, scrollLineIndex + 4); i += 1) {
|
|
2910
3209
|
const position = i - scrollLineIndex;
|
|
2911
3210
|
if (position !== 0) {
|
|
@@ -2938,11 +3237,12 @@ class AmLyrics extends i {
|
|
|
2938
3237
|
paddingTop) < 1) {
|
|
2939
3238
|
return;
|
|
2940
3239
|
}
|
|
2941
|
-
// Skip scroll if near the bottom of content
|
|
3240
|
+
// Skip scroll if near the bottom of content and we aren't trying to scroll back up
|
|
2942
3241
|
if (!forceScroll && !activeLine.classList.contains('lyrics-footer')) {
|
|
2943
3242
|
const parent = this.lyricsContainer;
|
|
2944
3243
|
const atBottom = parent.scrollTop + parent.clientHeight >= parent.scrollHeight - 50;
|
|
2945
|
-
|
|
3244
|
+
const targetTop = Math.max(0, -(paddingTop - activeLine.offsetTop));
|
|
3245
|
+
if (atBottom && targetTop > parent.scrollTop - 50) {
|
|
2946
3246
|
return;
|
|
2947
3247
|
}
|
|
2948
3248
|
}
|
|
@@ -2953,6 +3253,7 @@ class AmLyrics extends i {
|
|
|
2953
3253
|
clearTimeout(this.userScrollTimeoutId);
|
|
2954
3254
|
this.userScrollTimeoutId = undefined;
|
|
2955
3255
|
}
|
|
3256
|
+
this.clearPastLineHighlights();
|
|
2956
3257
|
const duration = scrollDuration ?? SCROLL_ANIMATION_DURATION_MS;
|
|
2957
3258
|
setTimeout(() => {
|
|
2958
3259
|
this.isProgrammaticScroll = false;
|
|
@@ -2974,6 +3275,7 @@ class AmLyrics extends i {
|
|
|
2974
3275
|
? Array.from(wordElement.querySelectorAll('span.char'))
|
|
2975
3276
|
: [];
|
|
2976
3277
|
const isGrowable = wordElement?.classList.contains('growable');
|
|
3278
|
+
const isCharRise = wordElement?.classList.contains('char-rise');
|
|
2977
3279
|
const isFirstSyllable = syllable.getAttribute('data-syllable-index') === '0';
|
|
2978
3280
|
const isFirstInContainer = isFirstSyllable; // Simplified
|
|
2979
3281
|
const isGap = syllable.closest('.lyrics-gap') !== null;
|
|
@@ -3020,6 +3322,16 @@ class AmLyrics extends i {
|
|
|
3020
3322
|
});
|
|
3021
3323
|
});
|
|
3022
3324
|
}
|
|
3325
|
+
if (isCharRise && isFirstSyllable && allWordCharSpans.length > 0) {
|
|
3326
|
+
const finalDuration = Math.max(wordDurationMs, syllableDurationMs);
|
|
3327
|
+
const baseDelayPerChar = finalDuration * 0.09;
|
|
3328
|
+
const riseDurationMs = finalDuration * 1.5;
|
|
3329
|
+
allWordCharSpans.forEach(span => {
|
|
3330
|
+
const charIndex = parseFloat(span.dataset.syllableCharIndex || '0');
|
|
3331
|
+
const riseDelay = baseDelayPerChar * charIndex;
|
|
3332
|
+
charAnimationsMap.set(span, `rise-char ${riseDurationMs}ms ease-in-out ${riseDelay}ms forwards`);
|
|
3333
|
+
});
|
|
3334
|
+
}
|
|
3023
3335
|
// Step 2: Wipe Pass
|
|
3024
3336
|
if (charSpans.length > 0) {
|
|
3025
3337
|
charSpans.forEach((span, charIndex) => {
|
|
@@ -3037,7 +3349,9 @@ class AmLyrics extends i {
|
|
|
3037
3349
|
}
|
|
3038
3350
|
const existingAnimation = charAnimationsMap.get(span) || span.style.animation || '';
|
|
3039
3351
|
const animationParts = [];
|
|
3040
|
-
if (existingAnimation &&
|
|
3352
|
+
if (existingAnimation &&
|
|
3353
|
+
(existingAnimation.includes('grow-dynamic') ||
|
|
3354
|
+
existingAnimation.includes('rise-char'))) {
|
|
3041
3355
|
animationParts.push(existingAnimation.split(',')[0].trim());
|
|
3042
3356
|
}
|
|
3043
3357
|
if (charIndex > 0) {
|
|
@@ -3103,36 +3417,119 @@ class AmLyrics extends i {
|
|
|
3103
3417
|
// eslint-disable-next-line no-param-reassign
|
|
3104
3418
|
syllable.style.backgroundColor = 'var(--lyplus-text-secondary)';
|
|
3105
3419
|
// Reset character animations — disable transition so finished chars don't slowly fade
|
|
3106
|
-
syllable.querySelectorAll('span.char')
|
|
3107
|
-
|
|
3420
|
+
const charSpans = syllable.querySelectorAll('span.char');
|
|
3421
|
+
for (let i = 0; i < charSpans.length; i += 1) {
|
|
3422
|
+
const el = charSpans[i];
|
|
3108
3423
|
el.style.animation = '';
|
|
3109
|
-
el.style.willChange = '';
|
|
3110
3424
|
el.style.transition = 'none';
|
|
3111
3425
|
el.style.backgroundColor = 'var(--lyplus-text-secondary)';
|
|
3112
|
-
}
|
|
3426
|
+
}
|
|
3113
3427
|
// Immediately remove all state classes
|
|
3114
3428
|
syllable.classList.remove('highlight', 'finished', 'pre-highlight', 'cleanup');
|
|
3115
|
-
// In next frame, clear inline styles so CSS transitions can resume for future use
|
|
3116
|
-
requestAnimationFrame(() => {
|
|
3117
|
-
syllable.style.removeProperty('background-color');
|
|
3118
|
-
syllable.style.removeProperty('transition');
|
|
3119
|
-
syllable.querySelectorAll('span.char').forEach(span => {
|
|
3120
|
-
const el = span;
|
|
3121
|
-
el.style.removeProperty('background-color');
|
|
3122
|
-
el.style.removeProperty('transition');
|
|
3123
|
-
el.style.removeProperty('will-change');
|
|
3124
|
-
});
|
|
3125
|
-
});
|
|
3126
3429
|
}
|
|
3127
3430
|
/**
|
|
3128
|
-
* Reset all syllables in a line
|
|
3431
|
+
* Reset all syllables in a line — batches deferred cleanup into a single rAF
|
|
3129
3432
|
*/
|
|
3130
3433
|
static resetSyllables(line) {
|
|
3131
3434
|
if (!line)
|
|
3132
3435
|
return;
|
|
3436
|
+
line.classList.remove('persist-highlight');
|
|
3133
3437
|
// eslint-disable-next-line no-param-reassign
|
|
3134
3438
|
line._cachedSyllableElements = null;
|
|
3135
|
-
|
|
3439
|
+
const syllables = line.getElementsByClassName('lyrics-syllable');
|
|
3440
|
+
for (let i = 0; i < syllables.length; i += 1) {
|
|
3441
|
+
AmLyrics.resetSyllable(syllables[i]);
|
|
3442
|
+
}
|
|
3443
|
+
// Batch deferred style cleanup into a single rAF for all syllables in the line
|
|
3444
|
+
requestAnimationFrame(() => {
|
|
3445
|
+
for (let i = 0; i < syllables.length; i += 1) {
|
|
3446
|
+
const syllable = syllables[i];
|
|
3447
|
+
syllable.style.removeProperty('background-color');
|
|
3448
|
+
syllable.style.removeProperty('transition');
|
|
3449
|
+
const chars = syllable.querySelectorAll('span.char');
|
|
3450
|
+
for (let j = 0; j < chars.length; j += 1) {
|
|
3451
|
+
const el = chars[j];
|
|
3452
|
+
el.style.removeProperty('background-color');
|
|
3453
|
+
el.style.removeProperty('transition');
|
|
3454
|
+
el.style.removeProperty('will-change');
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
});
|
|
3458
|
+
}
|
|
3459
|
+
/**
|
|
3460
|
+
* Gentle reset for normal playback: remove highlight/finished classes
|
|
3461
|
+
* without forcing inline styles. Lets CSS transition fade syllables
|
|
3462
|
+
* back to secondary colour smoothly.
|
|
3463
|
+
*/
|
|
3464
|
+
static unfinishSyllables(line) {
|
|
3465
|
+
if (!line)
|
|
3466
|
+
return;
|
|
3467
|
+
line.classList.remove('persist-highlight');
|
|
3468
|
+
const syllables = line.getElementsByClassName('lyrics-syllable');
|
|
3469
|
+
for (let i = 0; i < syllables.length; i += 1) {
|
|
3470
|
+
const s = syllables[i];
|
|
3471
|
+
s.classList.remove('highlight', 'finished', 'pre-highlight', 'cleanup');
|
|
3472
|
+
s.style.animation = '';
|
|
3473
|
+
s.style.removeProperty('--pre-wipe-duration');
|
|
3474
|
+
s.style.removeProperty('--pre-wipe-delay');
|
|
3475
|
+
s.style.removeProperty('background-color');
|
|
3476
|
+
s.style.removeProperty('transition');
|
|
3477
|
+
const chars = s.querySelectorAll('span.char');
|
|
3478
|
+
for (let j = 0; j < chars.length; j += 1) {
|
|
3479
|
+
const el = chars[j];
|
|
3480
|
+
el.style.animation = '';
|
|
3481
|
+
el.style.removeProperty('will-change');
|
|
3482
|
+
el.style.removeProperty('background-color');
|
|
3483
|
+
el.style.removeProperty('transition');
|
|
3484
|
+
el.style.removeProperty('filter');
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
static finishSyllablesUpToTime(line, currentTimeMs) {
|
|
3489
|
+
if (!line)
|
|
3490
|
+
return;
|
|
3491
|
+
let hasFinishedSyllable = false;
|
|
3492
|
+
let syllables = line._cachedSyllableElements;
|
|
3493
|
+
if (!syllables) {
|
|
3494
|
+
syllables = Array.from(line.querySelectorAll('.lyrics-syllable'));
|
|
3495
|
+
for (let i = 0; i < syllables.length; i += 1) {
|
|
3496
|
+
const syllable = syllables[i];
|
|
3497
|
+
syllable._cachedStartTime = parseFloat(syllable.getAttribute('data-start-time') || '0');
|
|
3498
|
+
syllable._cachedEndTime = parseFloat(syllable.getAttribute('data-end-time') || '0');
|
|
3499
|
+
}
|
|
3500
|
+
// eslint-disable-next-line no-param-reassign
|
|
3501
|
+
line._cachedSyllableElements = syllables;
|
|
3502
|
+
}
|
|
3503
|
+
for (let i = 0; i < syllables.length; i += 1) {
|
|
3504
|
+
const syllable = syllables[i];
|
|
3505
|
+
const startTime = syllable._cachedStartTime;
|
|
3506
|
+
if (Number.isFinite(startTime) && currentTimeMs >= startTime) {
|
|
3507
|
+
const { classList } = syllable;
|
|
3508
|
+
if (!classList.contains('finished')) {
|
|
3509
|
+
if (!classList.contains('highlight')) {
|
|
3510
|
+
AmLyrics.updateSyllableAnimation(syllable, Math.max(0, currentTimeMs - startTime));
|
|
3511
|
+
}
|
|
3512
|
+
classList.add('finished');
|
|
3513
|
+
}
|
|
3514
|
+
hasFinishedSyllable = true;
|
|
3515
|
+
classList.remove('highlight');
|
|
3516
|
+
classList.remove('pre-highlight');
|
|
3517
|
+
classList.add('cleanup');
|
|
3518
|
+
syllable.style.animation = '';
|
|
3519
|
+
syllable.style.removeProperty('--pre-wipe-duration');
|
|
3520
|
+
syllable.style.removeProperty('--pre-wipe-delay');
|
|
3521
|
+
const chars = syllable.querySelectorAll('span.char');
|
|
3522
|
+
for (let ci = 0; ci < chars.length; ci += 1) {
|
|
3523
|
+
chars[ci].style.animation = '';
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
if (hasFinishedSyllable) {
|
|
3528
|
+
line.classList.add('persist-highlight');
|
|
3529
|
+
}
|
|
3530
|
+
else {
|
|
3531
|
+
line.classList.remove('persist-highlight');
|
|
3532
|
+
}
|
|
3136
3533
|
}
|
|
3137
3534
|
/**
|
|
3138
3535
|
* Update syllables based on current time
|
|
@@ -3143,13 +3540,18 @@ class AmLyrics extends i {
|
|
|
3143
3540
|
let syllables = line._cachedSyllableElements;
|
|
3144
3541
|
if (!syllables) {
|
|
3145
3542
|
syllables = Array.from(line.querySelectorAll('.lyrics-syllable'));
|
|
3543
|
+
for (let i = 0; i < syllables.length; i += 1) {
|
|
3544
|
+
const syllable = syllables[i];
|
|
3545
|
+
syllable._cachedStartTime = parseFloat(syllable.getAttribute('data-start-time') || '0');
|
|
3546
|
+
syllable._cachedEndTime = parseFloat(syllable.getAttribute('data-end-time') || '0');
|
|
3547
|
+
}
|
|
3146
3548
|
// eslint-disable-next-line no-param-reassign
|
|
3147
3549
|
line._cachedSyllableElements = syllables;
|
|
3148
3550
|
}
|
|
3149
3551
|
for (let i = 0; i < syllables.length; i += 1) {
|
|
3150
3552
|
const syllable = syllables[i];
|
|
3151
|
-
const startTime =
|
|
3152
|
-
const endTime =
|
|
3553
|
+
const startTime = syllable._cachedStartTime;
|
|
3554
|
+
const endTime = syllable._cachedEndTime;
|
|
3153
3555
|
if (Number.isFinite(startTime) && Number.isFinite(endTime)) {
|
|
3154
3556
|
const { classList } = syllable;
|
|
3155
3557
|
const hasHighlight = classList.contains('highlight');
|
|
@@ -3187,6 +3589,7 @@ class AmLyrics extends i {
|
|
|
3187
3589
|
AmLyrics.updateSyllableAnimation(syllable, currentTimeMs - startTime);
|
|
3188
3590
|
}
|
|
3189
3591
|
classList.add('finished');
|
|
3592
|
+
// Keep the completed wipe state until user scroll resets it.
|
|
3190
3593
|
}
|
|
3191
3594
|
}
|
|
3192
3595
|
else if (hasHighlight || hasFinished) {
|
|
@@ -3469,7 +3872,8 @@ class AmLyrics extends i {
|
|
|
3469
3872
|
// Create background vocals container (with romanization support)
|
|
3470
3873
|
const backgroundVocalElement = hasBackground
|
|
3471
3874
|
? b `<p class="background-vocal-container">
|
|
3472
|
-
|
|
3875
|
+
<span class="background-vocal-wrap">
|
|
3876
|
+
${line.backgroundText.map((syllable, syllableIndex) => {
|
|
3473
3877
|
const startTimeMs = syllable.timestamp;
|
|
3474
3878
|
const endTimeMs = syllable.endtime;
|
|
3475
3879
|
const durationMs = endTimeMs - startTimeMs;
|
|
@@ -3477,36 +3881,37 @@ class AmLyrics extends i {
|
|
|
3477
3881
|
syllable.romanizedText &&
|
|
3478
3882
|
syllable.romanizedText.trim() !== syllable.text.trim()
|
|
3479
3883
|
? b `<span
|
|
3480
|
-
|
|
3884
|
+
class="lyrics-syllable transliteration no-chars ${syllable.lineSynced
|
|
3481
3885
|
? 'line-synced'
|
|
3482
3886
|
: ''}"
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3887
|
+
data-start-time="${startTimeMs}"
|
|
3888
|
+
data-end-time="${endTimeMs}"
|
|
3889
|
+
data-duration="${durationMs}"
|
|
3890
|
+
data-syllable-index="0"
|
|
3891
|
+
data-wipe-ratio="1"
|
|
3892
|
+
>${syllable.romanizedText}</span
|
|
3893
|
+
>`
|
|
3490
3894
|
: '';
|
|
3491
3895
|
return b `<span class="lyrics-word"
|
|
3492
|
-
|
|
3493
|
-
|
|
3896
|
+
><span
|
|
3897
|
+
class="lyrics-syllable-wrap${bgRomanizedText
|
|
3494
3898
|
? ' has-transliteration'
|
|
3495
3899
|
: ''}"
|
|
3496
|
-
|
|
3497
|
-
|
|
3900
|
+
><span
|
|
3901
|
+
class="lyrics-syllable no-chars${syllable.lineSynced
|
|
3498
3902
|
? ' line-synced'
|
|
3499
3903
|
: ''}"
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3904
|
+
data-start-time="${startTimeMs}"
|
|
3905
|
+
data-end-time="${endTimeMs}"
|
|
3906
|
+
data-duration="${durationMs}"
|
|
3907
|
+
data-syllable-index="${syllableIndex}"
|
|
3908
|
+
data-wipe-ratio="1"
|
|
3909
|
+
>${syllable.text}</span
|
|
3910
|
+
>${bgRomanizedText}</span
|
|
3911
|
+
></span
|
|
3912
|
+
>`;
|
|
3509
3913
|
})}
|
|
3914
|
+
</span>
|
|
3510
3915
|
</p>`
|
|
3511
3916
|
: '';
|
|
3512
3917
|
// Background vocals share the same line.translation and line.romanizedText
|
|
@@ -3520,6 +3925,7 @@ class AmLyrics extends i {
|
|
|
3520
3925
|
const wordGroups = lineData?.wordGroups ?? [];
|
|
3521
3926
|
const groupGrowable = lineData?.groupGrowable ?? [];
|
|
3522
3927
|
const groupGlowing = lineData?.groupGlowing ?? [];
|
|
3928
|
+
const groupCharRise = lineData?.groupCharRise ?? [];
|
|
3523
3929
|
const vwFullText = lineData?.vwFullText ?? [];
|
|
3524
3930
|
const vwFullDuration = lineData?.vwFullDuration ?? [];
|
|
3525
3931
|
const vwCharOffset = lineData?.vwCharOffset ?? [];
|
|
@@ -3531,11 +3937,17 @@ class AmLyrics extends i {
|
|
|
3531
3937
|
${wordGroups.map((group, groupIdx) => {
|
|
3532
3938
|
const isGrowable = groupGrowable[groupIdx];
|
|
3533
3939
|
const isGlowing = groupGlowing[groupIdx];
|
|
3940
|
+
const isCharRise = groupCharRise[groupIdx];
|
|
3941
|
+
const isAnimatedByChar = isGrowable || isCharRise;
|
|
3534
3942
|
const groupLineSynced = group.some(s => s.lineSynced);
|
|
3535
|
-
const wordText =
|
|
3536
|
-
const wordDuration =
|
|
3943
|
+
const wordText = isAnimatedByChar ? vwFullText[groupIdx] : '';
|
|
3944
|
+
const wordDuration = isAnimatedByChar
|
|
3945
|
+
? vwFullDuration[groupIdx]
|
|
3946
|
+
: 0;
|
|
3537
3947
|
const wordNumChars = wordText.length;
|
|
3538
|
-
const groupCharOffset =
|
|
3948
|
+
const groupCharOffset = isAnimatedByChar
|
|
3949
|
+
? vwCharOffset[groupIdx]
|
|
3950
|
+
: 0;
|
|
3539
3951
|
let sylCharAccumulator = 0;
|
|
3540
3952
|
const groupText = group.map(s => s.text).join('');
|
|
3541
3953
|
const shouldAllowBreak = groupText.trim().length >= 16 ||
|
|
@@ -3547,9 +3959,11 @@ class AmLyrics extends i {
|
|
|
3547
3959
|
// Base float is 0.8s, plus a portion of the audio duration, capped between 1.0s and 2.5s
|
|
3548
3960
|
const riseDuration = Math.max(1.2, Math.min(2.5, 1.2 + (actualDurationMs / 1000) * 0.6));
|
|
3549
3961
|
return b `<span
|
|
3550
|
-
class="lyrics-word${isGrowable ? ' growable' : ''}${
|
|
3551
|
-
? '
|
|
3552
|
-
: ''}${
|
|
3962
|
+
class="lyrics-word${isGrowable ? ' growable' : ''}${isCharRise
|
|
3963
|
+
? ' char-rise'
|
|
3964
|
+
: ''}${isGlowing ? ' glowing' : ''}${shouldAllowBreak
|
|
3965
|
+
? ' allow-break'
|
|
3966
|
+
: ''}"
|
|
3553
3967
|
style="--rise-duration: ${riseDuration}s"
|
|
3554
3968
|
>${group.map((syllable, sylIdx) => {
|
|
3555
3969
|
const startTimeMs = syllable.timestamp;
|
|
@@ -3560,7 +3974,7 @@ class AmLyrics extends i {
|
|
|
3560
3974
|
syllable.romanizedText &&
|
|
3561
3975
|
syllable.romanizedText.trim() !== syllable.text.trim()
|
|
3562
3976
|
? b `<span
|
|
3563
|
-
class="lyrics-syllable transliteration ${groupLineSynced
|
|
3977
|
+
class="lyrics-syllable transliteration no-chars ${groupLineSynced
|
|
3564
3978
|
? 'line-synced'
|
|
3565
3979
|
: ''}"
|
|
3566
3980
|
data-start-time="${startTimeMs}"
|
|
@@ -3572,7 +3986,7 @@ class AmLyrics extends i {
|
|
|
3572
3986
|
>`
|
|
3573
3987
|
: '';
|
|
3574
3988
|
let syllableContent = text;
|
|
3575
|
-
if (
|
|
3989
|
+
if (isAnimatedByChar) {
|
|
3576
3990
|
let charIndexInsideSyllable = 0;
|
|
3577
3991
|
const numCharsInSyllable = text.replace(/\s/g, '').length || 1;
|
|
3578
3992
|
syllableContent = b `${text.split('').map(char => {
|
|
@@ -3652,7 +4066,7 @@ class AmLyrics extends i {
|
|
|
3652
4066
|
><span
|
|
3653
4067
|
class="lyrics-syllable${groupLineSynced
|
|
3654
4068
|
? ' line-synced'
|
|
3655
|
-
: ''}${
|
|
4069
|
+
: ''}${isAnimatedByChar ? ' has-chars' : ' no-chars'}"
|
|
3656
4070
|
data-start-time="${startTimeMs}"
|
|
3657
4071
|
data-end-time="${endTimeMs}"
|
|
3658
4072
|
data-duration="${durationMs}"
|
|
@@ -4014,6 +4428,7 @@ AmLyrics.styles = i$3 `
|
|
|
4014
4428
|
-webkit-overflow-scrolling: touch;
|
|
4015
4429
|
box-sizing: border-box;
|
|
4016
4430
|
scrollbar-width: none;
|
|
4431
|
+
overflow-anchor: none;
|
|
4017
4432
|
}
|
|
4018
4433
|
|
|
4019
4434
|
.lyrics-container::-webkit-scrollbar {
|
|
@@ -4034,9 +4449,16 @@ AmLyrics.styles = i$3 `
|
|
|
4034
4449
|
}
|
|
4035
4450
|
|
|
4036
4451
|
.lyrics-line.scroll-animate {
|
|
4037
|
-
|
|
4452
|
+
/* Preserve the graceful fade duration; the keyframe handles the
|
|
4453
|
+
transform, so we only need to keep opacity/filter transitions
|
|
4454
|
+
alive without !important overriding the base rule. */
|
|
4455
|
+
transition:
|
|
4456
|
+
opacity 0.7s ease,
|
|
4457
|
+
filter 0.7s ease,
|
|
4458
|
+
transform 0.4s cubic-bezier(0.41, 0, 0.12, 0.99)
|
|
4459
|
+
var(--lyrics-line-delay, 0ms);
|
|
4038
4460
|
animation-name: lyrics-scroll;
|
|
4039
|
-
animation-duration: var(--scroll-duration,
|
|
4461
|
+
animation-duration: var(--scroll-duration, 400ms);
|
|
4040
4462
|
animation-timing-function: cubic-bezier(0.41, 0, 0.12, 0.99);
|
|
4041
4463
|
animation-fill-mode: both;
|
|
4042
4464
|
animation-delay: var(--lyrics-line-delay, 0ms);
|
|
@@ -4057,12 +4479,15 @@ AmLyrics.styles = i$3 `
|
|
|
4057
4479
|
font-size: var(--lyplus-font-size-base);
|
|
4058
4480
|
cursor: pointer;
|
|
4059
4481
|
transform-origin: left;
|
|
4482
|
+
/* Graceful 0.7 s fade so the line stays mostly bright while the
|
|
4483
|
+
0.4 s scroll animation runs, then settles into the inactive state. */
|
|
4060
4484
|
transition:
|
|
4061
|
-
opacity 0.
|
|
4485
|
+
opacity 0.7s ease,
|
|
4062
4486
|
transform 0.4s cubic-bezier(0.41, 0, 0.12, 0.99)
|
|
4063
4487
|
var(--lyrics-line-delay, 0ms),
|
|
4064
|
-
filter 0.
|
|
4488
|
+
filter 0.7s ease;
|
|
4065
4489
|
content-visibility: auto;
|
|
4490
|
+
contain: layout style;
|
|
4066
4491
|
text-rendering: optimizeLegibility;
|
|
4067
4492
|
}
|
|
4068
4493
|
|
|
@@ -4074,7 +4499,7 @@ AmLyrics.styles = i$3 `
|
|
|
4074
4499
|
.lyrics-line-container {
|
|
4075
4500
|
overflow-wrap: break-word;
|
|
4076
4501
|
transform-origin: left;
|
|
4077
|
-
transform:
|
|
4502
|
+
transform: translateZ(0);
|
|
4078
4503
|
transition:
|
|
4079
4504
|
transform 0.7s ease,
|
|
4080
4505
|
background-color 0.7s,
|
|
@@ -4083,7 +4508,7 @@ AmLyrics.styles = i$3 `
|
|
|
4083
4508
|
|
|
4084
4509
|
.lyrics-line.active .lyrics-line-container,
|
|
4085
4510
|
.lyrics-line.pre-active .lyrics-line-container {
|
|
4086
|
-
transform:
|
|
4511
|
+
transform: translateZ(0);
|
|
4087
4512
|
transition:
|
|
4088
4513
|
transform 0.5s ease,
|
|
4089
4514
|
background-color 0.18s,
|
|
@@ -4097,31 +4522,50 @@ AmLyrics.styles = i$3 `
|
|
|
4097
4522
|
|
|
4098
4523
|
.background-vocal-container {
|
|
4099
4524
|
max-height: 0;
|
|
4100
|
-
padding-top: 0;
|
|
4101
|
-
transform: translateY(-0.5em) scale(0.95);
|
|
4102
4525
|
overflow: visible;
|
|
4103
4526
|
opacity: 0;
|
|
4104
4527
|
font-size: var(--lyplus-font-size-subtext);
|
|
4528
|
+
line-height: 1.15;
|
|
4529
|
+
color: color-mix(in srgb, var(--lyplus-text-secondary) 80%, transparent);
|
|
4530
|
+
/* Fast exit (0.25 s) so bg vocals collapse quickly and feel snappy.
|
|
4531
|
+
The scroll takes ~0.4 s; finishing the collapse first prevents
|
|
4532
|
+
the container from trailing behind the motion. */
|
|
4105
4533
|
transition:
|
|
4106
|
-
max-height
|
|
4107
|
-
opacity
|
|
4108
|
-
transform 450ms cubic-bezier(0.33, 1, 0.68, 1),
|
|
4109
|
-
padding 450ms cubic-bezier(0.33, 1, 0.68, 1);
|
|
4534
|
+
max-height 250ms cubic-bezier(0.41, 0, 0.12, 0.99),
|
|
4535
|
+
opacity 250ms cubic-bezier(0.41, 0, 0.12, 0.99);
|
|
4110
4536
|
margin: 0;
|
|
4537
|
+
pointer-events: none;
|
|
4111
4538
|
}
|
|
4112
4539
|
|
|
4113
|
-
.
|
|
4114
|
-
|
|
4540
|
+
.background-vocal-wrap {
|
|
4541
|
+
display: block;
|
|
4542
|
+
padding-top: 0;
|
|
4543
|
+
padding-bottom: 0;
|
|
4544
|
+
transition: padding-top 250ms cubic-bezier(0.41, 0, 0.12, 0.99);
|
|
4545
|
+
}
|
|
4546
|
+
|
|
4547
|
+
.lyrics-line.singer-right .background-vocal-container,
|
|
4548
|
+
.lyrics-line.rtl-text .background-vocal-container {
|
|
4549
|
+
margin-left: auto;
|
|
4550
|
+
margin-right: 0;
|
|
4551
|
+
}
|
|
4552
|
+
|
|
4553
|
+
/* Background vocals expand only when .bg-expanded is present.
|
|
4554
|
+
This is separate from .active so bg vocals can collapse immediately
|
|
4555
|
+
while .active stays to keep text white until the scroll passes. */
|
|
4556
|
+
.lyrics-line.bg-expanded .background-vocal-container {
|
|
4115
4557
|
max-height: 4em;
|
|
4116
4558
|
opacity: 1;
|
|
4117
|
-
|
|
4118
|
-
transform: translateY(0) scale(1);
|
|
4559
|
+
/* Slower entry (0.6 s) so bg vocals expand smoothly. */
|
|
4119
4560
|
transition:
|
|
4120
|
-
max-height
|
|
4121
|
-
opacity
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4561
|
+
max-height 0.6s ease,
|
|
4562
|
+
opacity 0.6s ease;
|
|
4563
|
+
will-change: max-height, opacity;
|
|
4564
|
+
}
|
|
4565
|
+
|
|
4566
|
+
.lyrics-line.bg-expanded .background-vocal-wrap {
|
|
4567
|
+
padding-top: 0.26em;
|
|
4568
|
+
transition: padding-top 0.6s ease;
|
|
4125
4569
|
}
|
|
4126
4570
|
|
|
4127
4571
|
/* --- Line States & Modifiers --- */
|
|
@@ -4134,6 +4578,16 @@ AmLyrics.styles = i$3 `
|
|
|
4134
4578
|
opacity: 1;
|
|
4135
4579
|
}
|
|
4136
4580
|
|
|
4581
|
+
.lyrics-line.persist-highlight {
|
|
4582
|
+
filter: none !important;
|
|
4583
|
+
opacity: 1;
|
|
4584
|
+
}
|
|
4585
|
+
|
|
4586
|
+
.lyrics-line.persist-highlight .lyrics-syllable.finished,
|
|
4587
|
+
.lyrics-line.persist-highlight .lyrics-syllable.finished span.char {
|
|
4588
|
+
transition: none !important;
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4137
4591
|
.lyrics-line.singer-right {
|
|
4138
4592
|
text-align: end;
|
|
4139
4593
|
}
|
|
@@ -4198,22 +4652,32 @@ AmLyrics.styles = i$3 `
|
|
|
4198
4652
|
|
|
4199
4653
|
/* --- Blur Effect for Inactive Lines --- */
|
|
4200
4654
|
.lyrics-container.blur-inactive-enabled:not(.not-focused)
|
|
4201
|
-
.lyrics-line:not(.active):not(.pre-active):not(.lyrics-gap)
|
|
4655
|
+
.lyrics-line:not(.active):not(.pre-active):not(.lyrics-gap):not(
|
|
4656
|
+
.persist-highlight
|
|
4657
|
+
) {
|
|
4202
4658
|
filter: blur(var(--lyplus-blur-amount));
|
|
4203
4659
|
}
|
|
4204
4660
|
|
|
4661
|
+
/* Viewport Virtualization: Strip expensive filters and animations from
|
|
4662
|
+
offscreen lines. IntersectionObserver toggles this class. */
|
|
4663
|
+
.lyrics-line.far-line {
|
|
4664
|
+
filter: none !important;
|
|
4665
|
+
will-change: auto !important;
|
|
4666
|
+
animation: none !important;
|
|
4667
|
+
}
|
|
4668
|
+
|
|
4205
4669
|
.lyrics-container.blur-inactive-enabled:not(.not-focused)
|
|
4206
4670
|
.lyrics-line.post-active-line:not(.lyrics-gap):not(.active):not(
|
|
4207
4671
|
.pre-active
|
|
4208
|
-
),
|
|
4672
|
+
):not(.persist-highlight),
|
|
4209
4673
|
.lyrics-container.blur-inactive-enabled:not(.not-focused)
|
|
4210
4674
|
.lyrics-line.next-active-line:not(.lyrics-gap):not(.active):not(
|
|
4211
4675
|
.pre-active
|
|
4212
|
-
),
|
|
4676
|
+
):not(.persist-highlight),
|
|
4213
4677
|
.lyrics-container.blur-inactive-enabled:not(.not-focused)
|
|
4214
4678
|
.lyrics-line.lyrics-activest:not(.active):not(.lyrics-gap):not(
|
|
4215
4679
|
.pre-active
|
|
4216
|
-
) {
|
|
4680
|
+
):not(.persist-highlight) {
|
|
4217
4681
|
filter: blur(var(--lyplus-blur-amount-near));
|
|
4218
4682
|
}
|
|
4219
4683
|
|
|
@@ -4243,6 +4707,17 @@ AmLyrics.styles = i$3 `
|
|
|
4243
4707
|
display: inline;
|
|
4244
4708
|
}
|
|
4245
4709
|
|
|
4710
|
+
.lyrics-word.char-rise {
|
|
4711
|
+
display: inline-block;
|
|
4712
|
+
vertical-align: baseline;
|
|
4713
|
+
white-space: nowrap;
|
|
4714
|
+
}
|
|
4715
|
+
|
|
4716
|
+
.lyrics-word.char-rise.allow-break {
|
|
4717
|
+
display: inline;
|
|
4718
|
+
white-space: normal;
|
|
4719
|
+
}
|
|
4720
|
+
|
|
4246
4721
|
.lyrics-syllable-wrap {
|
|
4247
4722
|
display: inline;
|
|
4248
4723
|
}
|
|
@@ -4272,17 +4747,19 @@ AmLyrics.styles = i$3 `
|
|
|
4272
4747
|
/* --- Syllable States --- */
|
|
4273
4748
|
.lyrics-syllable.finished {
|
|
4274
4749
|
background-color: var(--lyplus-text-primary);
|
|
4275
|
-
transition: transform 1s
|
|
4750
|
+
/* Unified transition: transform keeps its 1s glow decay, while
|
|
4751
|
+
background-color and color fade at 0.7s so everything dims
|
|
4752
|
+
together when the line becomes inactive. */
|
|
4753
|
+
transition:
|
|
4754
|
+
transform 1s ease,
|
|
4755
|
+
background-color 0.7s ease,
|
|
4756
|
+
color 0.7s ease;
|
|
4276
4757
|
}
|
|
4277
4758
|
|
|
4278
4759
|
.lyrics-syllable.finished.has-chars {
|
|
4279
4760
|
background-color: transparent;
|
|
4280
4761
|
}
|
|
4281
4762
|
|
|
4282
|
-
.lyrics-line:not(.active) .lyrics-syllable.finished {
|
|
4283
|
-
transition: color 0.18s;
|
|
4284
|
-
}
|
|
4285
|
-
|
|
4286
4763
|
.lyrics-line.active:not(.lyrics-gap) .lyrics-syllable {
|
|
4287
4764
|
transition:
|
|
4288
4765
|
transform 1s ease,
|
|
@@ -4332,7 +4809,60 @@ AmLyrics.styles = i$3 `
|
|
|
4332
4809
|
);
|
|
4333
4810
|
background-position:
|
|
4334
4811
|
calc(100% + 0.5em) 0%,
|
|
4335
|
-
right
|
|
4812
|
+
right 0%;
|
|
4813
|
+
}
|
|
4814
|
+
|
|
4815
|
+
/* Background vocals: muted gray wipe instead of white.
|
|
4816
|
+
Must match specificity of the main .active .highlight rule (0,3,1). */
|
|
4817
|
+
.lyrics-line.active
|
|
4818
|
+
.background-vocal-container
|
|
4819
|
+
.lyrics-syllable.highlight.no-chars,
|
|
4820
|
+
.lyrics-line.active
|
|
4821
|
+
.background-vocal-container
|
|
4822
|
+
.lyrics-syllable.pre-highlight.no-chars,
|
|
4823
|
+
.lyrics-line.pre-active
|
|
4824
|
+
.background-vocal-container
|
|
4825
|
+
.lyrics-syllable.highlight.no-chars,
|
|
4826
|
+
.lyrics-line.pre-active
|
|
4827
|
+
.background-vocal-container
|
|
4828
|
+
.lyrics-syllable.pre-highlight.no-chars {
|
|
4829
|
+
background-image:
|
|
4830
|
+
linear-gradient(
|
|
4831
|
+
90deg,
|
|
4832
|
+
#ffffff00 0%,
|
|
4833
|
+
color-mix(in srgb, var(--lyplus-text-primary, #fff) 50%, #888888) 50%,
|
|
4834
|
+
#0000 100%
|
|
4835
|
+
),
|
|
4836
|
+
linear-gradient(
|
|
4837
|
+
90deg,
|
|
4838
|
+
color-mix(in srgb, var(--lyplus-text-primary, #fff) 50%, #888888) 100%,
|
|
4839
|
+
#0000 100%
|
|
4840
|
+
);
|
|
4841
|
+
}
|
|
4842
|
+
|
|
4843
|
+
.lyrics-line.active
|
|
4844
|
+
.background-vocal-container
|
|
4845
|
+
.lyrics-syllable.highlight.rtl-text,
|
|
4846
|
+
.lyrics-line.active
|
|
4847
|
+
.background-vocal-container
|
|
4848
|
+
.lyrics-syllable.pre-highlight.rtl-text,
|
|
4849
|
+
.lyrics-line.pre-active
|
|
4850
|
+
.background-vocal-container
|
|
4851
|
+
.lyrics-syllable.highlight.rtl-text,
|
|
4852
|
+
.lyrics-line.pre-active
|
|
4853
|
+
.background-vocal-container
|
|
4854
|
+
.lyrics-syllable.pre-highlight.rtl-text {
|
|
4855
|
+
background-image:
|
|
4856
|
+
linear-gradient(
|
|
4857
|
+
-90deg,
|
|
4858
|
+
color-mix(in srgb, var(--lyplus-text-primary) 50%, #888888) 0%,
|
|
4859
|
+
transparent 100%
|
|
4860
|
+
),
|
|
4861
|
+
linear-gradient(
|
|
4862
|
+
-90deg,
|
|
4863
|
+
color-mix(in srgb, var(--lyplus-text-primary) 50%, #888888) 100%,
|
|
4864
|
+
transparent 100%
|
|
4865
|
+
);
|
|
4336
4866
|
}
|
|
4337
4867
|
|
|
4338
4868
|
/* Non-growable words float up with a gentle curve */
|
|
@@ -4340,18 +4870,88 @@ AmLyrics.styles = i$3 `
|
|
|
4340
4870
|
.lyrics-word:not(.growable)
|
|
4341
4871
|
.lyrics-syllable.highlight {
|
|
4342
4872
|
transform: translateY(-3.5%);
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4873
|
+
}
|
|
4874
|
+
|
|
4875
|
+
.lyrics-line.persist-highlight:not(.lyrics-gap)
|
|
4876
|
+
.lyrics-word:not(.growable)
|
|
4877
|
+
.lyrics-syllable.finished {
|
|
4878
|
+
transform: translateY(-3.5%);
|
|
4347
4879
|
}
|
|
4348
4880
|
|
|
4349
4881
|
.lyrics-word.growable .lyrics-syllable.cleanup .char {
|
|
4350
4882
|
transform: translateY(-3.5%);
|
|
4351
4883
|
}
|
|
4352
4884
|
|
|
4353
|
-
.lyrics-
|
|
4354
|
-
|
|
4885
|
+
.lyrics-word.char-rise .lyrics-syllable.cleanup .char {
|
|
4886
|
+
transform: translateY(-3.5%);
|
|
4887
|
+
}
|
|
4888
|
+
|
|
4889
|
+
.lyrics-line.persist-highlight
|
|
4890
|
+
.lyrics-word.growable
|
|
4891
|
+
.lyrics-syllable.finished
|
|
4892
|
+
.char,
|
|
4893
|
+
.lyrics-line.persist-highlight
|
|
4894
|
+
.lyrics-word.char-rise
|
|
4895
|
+
.lyrics-syllable.finished
|
|
4896
|
+
.char {
|
|
4897
|
+
transform: translateY(-3.5%);
|
|
4898
|
+
}
|
|
4899
|
+
|
|
4900
|
+
/* Background vocal overrides — placed AFTER main rules so they win
|
|
4901
|
+
on equal specificity. */
|
|
4902
|
+
.background-vocal-container .lyrics-syllable {
|
|
4903
|
+
background-color: color-mix(
|
|
4904
|
+
in srgb,
|
|
4905
|
+
var(--lyplus-text-secondary) 50%,
|
|
4906
|
+
#888888
|
|
4907
|
+
);
|
|
4908
|
+
}
|
|
4909
|
+
|
|
4910
|
+
.lyrics-line.active:not(.lyrics-gap)
|
|
4911
|
+
.background-vocal-container
|
|
4912
|
+
.lyrics-syllable.finished,
|
|
4913
|
+
.lyrics-line.pre-active
|
|
4914
|
+
.background-vocal-container
|
|
4915
|
+
.lyrics-syllable.finished {
|
|
4916
|
+
background-color: color-mix(
|
|
4917
|
+
in srgb,
|
|
4918
|
+
var(--lyplus-text-primary) 50%,
|
|
4919
|
+
#888888
|
|
4920
|
+
);
|
|
4921
|
+
}
|
|
4922
|
+
|
|
4923
|
+
.background-vocal-container .lyrics-syllable.line-synced {
|
|
4924
|
+
color: color-mix(
|
|
4925
|
+
in srgb,
|
|
4926
|
+
var(--lyplus-text-secondary) 50%,
|
|
4927
|
+
#888888
|
|
4928
|
+
) !important;
|
|
4929
|
+
}
|
|
4930
|
+
|
|
4931
|
+
.lyrics-line.active:not(.lyrics-gap)
|
|
4932
|
+
.background-vocal-container
|
|
4933
|
+
.lyrics-syllable.line-synced,
|
|
4934
|
+
.lyrics-line.pre-active
|
|
4935
|
+
.background-vocal-container
|
|
4936
|
+
.lyrics-syllable.line-synced {
|
|
4937
|
+
color: color-mix(
|
|
4938
|
+
in srgb,
|
|
4939
|
+
var(--lyplus-text-primary) 50%,
|
|
4940
|
+
#888888
|
|
4941
|
+
) !important;
|
|
4942
|
+
}
|
|
4943
|
+
|
|
4944
|
+
.lyrics-line.active:not(.lyrics-gap)
|
|
4945
|
+
.background-vocal-container
|
|
4946
|
+
.lyrics-syllable.line-synced.finished,
|
|
4947
|
+
.lyrics-line.pre-active
|
|
4948
|
+
.background-vocal-container
|
|
4949
|
+
.lyrics-syllable.line-synced.finished {
|
|
4950
|
+
color: color-mix(
|
|
4951
|
+
in srgb,
|
|
4952
|
+
var(--lyplus-text-primary) 50%,
|
|
4953
|
+
#888888
|
|
4954
|
+
) !important;
|
|
4355
4955
|
}
|
|
4356
4956
|
|
|
4357
4957
|
.lyrics-syllable.pre-highlight {
|
|
@@ -4395,8 +4995,11 @@ AmLyrics.styles = i$3 `
|
|
|
4395
4995
|
}
|
|
4396
4996
|
|
|
4397
4997
|
.lyrics-syllable.finished span.char {
|
|
4398
|
-
transition: color 0.18s;
|
|
4399
4998
|
background-color: var(--lyplus-text-primary);
|
|
4999
|
+
transition:
|
|
5000
|
+
color 0.7s,
|
|
5001
|
+
background-color 0.7s,
|
|
5002
|
+
transform 0.7s ease;
|
|
4400
5003
|
}
|
|
4401
5004
|
|
|
4402
5005
|
/* Active char spans: structural only, wipe animation sets gradient */
|
|
@@ -4587,7 +5190,7 @@ AmLyrics.styles = i$3 `
|
|
|
4587
5190
|
position: relative;
|
|
4588
5191
|
box-sizing: border-box;
|
|
4589
5192
|
font-weight: normal;
|
|
4590
|
-
transform: translateY(var(--lyrics-scroll-offset, 0px))
|
|
5193
|
+
transform: translateY(var(--lyrics-scroll-offset, 0px));
|
|
4591
5194
|
transition:
|
|
4592
5195
|
opacity 0.3s ease,
|
|
4593
5196
|
transform 0.6s cubic-bezier(0.23, 1, 0.32, 1)
|
|
@@ -4598,7 +5201,7 @@ AmLyrics.styles = i$3 `
|
|
|
4598
5201
|
.lyrics-plus-empty {
|
|
4599
5202
|
display: block;
|
|
4600
5203
|
height: 100vh;
|
|
4601
|
-
transform: translateY(var(--lyrics-scroll-offset, 0px))
|
|
5204
|
+
transform: translateY(var(--lyrics-scroll-offset, 0px));
|
|
4602
5205
|
}
|
|
4603
5206
|
|
|
4604
5207
|
.lyrics-footer {
|
|
@@ -4607,8 +5210,8 @@ AmLyrics.styles = i$3 `
|
|
|
4607
5210
|
align-items: center;
|
|
4608
5211
|
flex-wrap: wrap;
|
|
4609
5212
|
text-align: left;
|
|
4610
|
-
font-size:
|
|
4611
|
-
color:
|
|
5213
|
+
font-size: calc(var(--lyplus-font-size-base) * 0.5);
|
|
5214
|
+
color: var(--lyplus-text-secondary);
|
|
4612
5215
|
padding: 20px 0 50vh 0;
|
|
4613
5216
|
margin-top: 10px;
|
|
4614
5217
|
font-weight: 400;
|
|
@@ -4621,9 +5224,9 @@ AmLyrics.styles = i$3 `
|
|
|
4621
5224
|
}
|
|
4622
5225
|
|
|
4623
5226
|
.lyrics-footer.lyrics-line {
|
|
4624
|
-
font-size:
|
|
5227
|
+
font-size: calc(var(--lyplus-font-size-base) * 0.5);
|
|
4625
5228
|
padding: 20px var(--lyplus-padding-line) 50vh var(--lyplus-padding-line);
|
|
4626
|
-
|
|
5229
|
+
margin-top: 0;
|
|
4627
5230
|
}
|
|
4628
5231
|
|
|
4629
5232
|
.lyrics-footer.active {
|
|
@@ -4911,7 +5514,7 @@ AmLyrics.styles = i$3 `
|
|
|
4911
5514
|
0% 100%;
|
|
4912
5515
|
background-position:
|
|
4913
5516
|
calc(100% + 0.375em) 0%,
|
|
4914
|
-
calc(100% + 0.36em)
|
|
5517
|
+
calc(100% + 0.36em) 0%;
|
|
4915
5518
|
}
|
|
4916
5519
|
to {
|
|
4917
5520
|
background-size:
|
|
@@ -4919,7 +5522,7 @@ AmLyrics.styles = i$3 `
|
|
|
4919
5522
|
100% 100%;
|
|
4920
5523
|
background-position:
|
|
4921
5524
|
-0.75em 0%,
|
|
4922
|
-
right
|
|
5525
|
+
right 0%;
|
|
4923
5526
|
}
|
|
4924
5527
|
}
|
|
4925
5528
|
|
|
@@ -4930,7 +5533,7 @@ AmLyrics.styles = i$3 `
|
|
|
4930
5533
|
0% 100%;
|
|
4931
5534
|
background-position:
|
|
4932
5535
|
calc(100% + 0.75em) 0%,
|
|
4933
|
-
calc(100% + 0.5em)
|
|
5536
|
+
calc(100% + 0.5em) 0%;
|
|
4934
5537
|
}
|
|
4935
5538
|
100% {
|
|
4936
5539
|
background-size:
|
|
@@ -4938,7 +5541,7 @@ AmLyrics.styles = i$3 `
|
|
|
4938
5541
|
100% 100%;
|
|
4939
5542
|
background-position:
|
|
4940
5543
|
-0.75em 0%,
|
|
4941
|
-
right
|
|
5544
|
+
right 0%;
|
|
4942
5545
|
}
|
|
4943
5546
|
}
|
|
4944
5547
|
|
|
@@ -4968,7 +5571,7 @@ AmLyrics.styles = i$3 `
|
|
|
4968
5571
|
0% 100%;
|
|
4969
5572
|
background-position:
|
|
4970
5573
|
calc(100% + 0.75em) 0%,
|
|
4971
|
-
right
|
|
5574
|
+
right 0%;
|
|
4972
5575
|
}
|
|
4973
5576
|
to {
|
|
4974
5577
|
background-size:
|
|
@@ -4976,7 +5579,7 @@ AmLyrics.styles = i$3 `
|
|
|
4976
5579
|
0% 100%;
|
|
4977
5580
|
background-position:
|
|
4978
5581
|
calc(100% + 0.375em) 0%,
|
|
4979
|
-
right
|
|
5582
|
+
right 0%;
|
|
4980
5583
|
}
|
|
4981
5584
|
}
|
|
4982
5585
|
|
|
@@ -5042,7 +5645,7 @@ AmLyrics.styles = i$3 `
|
|
|
5042
5645
|
}
|
|
5043
5646
|
|
|
5044
5647
|
/* Character grow animation — translate3d+scale3d for smooth transform,
|
|
5045
|
-
drop-shadow for glow
|
|
5648
|
+
drop-shadow for glow */
|
|
5046
5649
|
@keyframes grow-dynamic {
|
|
5047
5650
|
0% {
|
|
5048
5651
|
transform: translate3d(0, 0, 0) scale3d(1, 1, 1);
|
|
@@ -5079,6 +5682,16 @@ AmLyrics.styles = i$3 `
|
|
|
5079
5682
|
}
|
|
5080
5683
|
}
|
|
5081
5684
|
|
|
5685
|
+
@keyframes rise-char {
|
|
5686
|
+
0% {
|
|
5687
|
+
transform: translate3d(0, 0, 0);
|
|
5688
|
+
}
|
|
5689
|
+
65%,
|
|
5690
|
+
100% {
|
|
5691
|
+
transform: translate3d(0, var(--char-rise-y, -1.12px), 0);
|
|
5692
|
+
}
|
|
5693
|
+
}
|
|
5694
|
+
|
|
5082
5695
|
@keyframes grow-static {
|
|
5083
5696
|
0%,
|
|
5084
5697
|
100% {
|