@uimaxbai/am-lyrics 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/AmLyrics.d.ts +27 -2
- package/dist/src/AmLyrics.d.ts.map +1 -1
- package/dist/src/am-lyrics.js +963 -351
- package/dist/src/am-lyrics.js.map +1 -1
- package/dist/src/react.js +963 -351
- package/dist/src/react.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/AmLyrics.ts +1130 -435
package/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) {
|
|
@@ -2954,6 +3253,7 @@ class AmLyrics extends i {
|
|
|
2954
3253
|
clearTimeout(this.userScrollTimeoutId);
|
|
2955
3254
|
this.userScrollTimeoutId = undefined;
|
|
2956
3255
|
}
|
|
3256
|
+
this.clearPastLineHighlights();
|
|
2957
3257
|
const duration = scrollDuration ?? SCROLL_ANIMATION_DURATION_MS;
|
|
2958
3258
|
setTimeout(() => {
|
|
2959
3259
|
this.isProgrammaticScroll = false;
|
|
@@ -2975,6 +3275,7 @@ class AmLyrics extends i {
|
|
|
2975
3275
|
? Array.from(wordElement.querySelectorAll('span.char'))
|
|
2976
3276
|
: [];
|
|
2977
3277
|
const isGrowable = wordElement?.classList.contains('growable');
|
|
3278
|
+
const isCharRise = wordElement?.classList.contains('char-rise');
|
|
2978
3279
|
const isFirstSyllable = syllable.getAttribute('data-syllable-index') === '0';
|
|
2979
3280
|
const isFirstInContainer = isFirstSyllable; // Simplified
|
|
2980
3281
|
const isGap = syllable.closest('.lyrics-gap') !== null;
|
|
@@ -3021,6 +3322,16 @@ class AmLyrics extends i {
|
|
|
3021
3322
|
});
|
|
3022
3323
|
});
|
|
3023
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
|
+
}
|
|
3024
3335
|
// Step 2: Wipe Pass
|
|
3025
3336
|
if (charSpans.length > 0) {
|
|
3026
3337
|
charSpans.forEach((span, charIndex) => {
|
|
@@ -3038,7 +3349,9 @@ class AmLyrics extends i {
|
|
|
3038
3349
|
}
|
|
3039
3350
|
const existingAnimation = charAnimationsMap.get(span) || span.style.animation || '';
|
|
3040
3351
|
const animationParts = [];
|
|
3041
|
-
if (existingAnimation &&
|
|
3352
|
+
if (existingAnimation &&
|
|
3353
|
+
(existingAnimation.includes('grow-dynamic') ||
|
|
3354
|
+
existingAnimation.includes('rise-char'))) {
|
|
3042
3355
|
animationParts.push(existingAnimation.split(',')[0].trim());
|
|
3043
3356
|
}
|
|
3044
3357
|
if (charIndex > 0) {
|
|
@@ -3104,36 +3417,119 @@ class AmLyrics extends i {
|
|
|
3104
3417
|
// eslint-disable-next-line no-param-reassign
|
|
3105
3418
|
syllable.style.backgroundColor = 'var(--lyplus-text-secondary)';
|
|
3106
3419
|
// Reset character animations — disable transition so finished chars don't slowly fade
|
|
3107
|
-
syllable.querySelectorAll('span.char')
|
|
3108
|
-
|
|
3420
|
+
const charSpans = syllable.querySelectorAll('span.char');
|
|
3421
|
+
for (let i = 0; i < charSpans.length; i += 1) {
|
|
3422
|
+
const el = charSpans[i];
|
|
3109
3423
|
el.style.animation = '';
|
|
3110
|
-
el.style.willChange = '';
|
|
3111
3424
|
el.style.transition = 'none';
|
|
3112
3425
|
el.style.backgroundColor = 'var(--lyplus-text-secondary)';
|
|
3113
|
-
}
|
|
3426
|
+
}
|
|
3114
3427
|
// Immediately remove all state classes
|
|
3115
3428
|
syllable.classList.remove('highlight', 'finished', 'pre-highlight', 'cleanup');
|
|
3116
|
-
// In next frame, clear inline styles so CSS transitions can resume for future use
|
|
3117
|
-
requestAnimationFrame(() => {
|
|
3118
|
-
syllable.style.removeProperty('background-color');
|
|
3119
|
-
syllable.style.removeProperty('transition');
|
|
3120
|
-
syllable.querySelectorAll('span.char').forEach(span => {
|
|
3121
|
-
const el = span;
|
|
3122
|
-
el.style.removeProperty('background-color');
|
|
3123
|
-
el.style.removeProperty('transition');
|
|
3124
|
-
el.style.removeProperty('will-change');
|
|
3125
|
-
});
|
|
3126
|
-
});
|
|
3127
3429
|
}
|
|
3128
3430
|
/**
|
|
3129
|
-
* Reset all syllables in a line
|
|
3431
|
+
* Reset all syllables in a line — batches deferred cleanup into a single rAF
|
|
3130
3432
|
*/
|
|
3131
3433
|
static resetSyllables(line) {
|
|
3132
3434
|
if (!line)
|
|
3133
3435
|
return;
|
|
3436
|
+
line.classList.remove('persist-highlight');
|
|
3134
3437
|
// eslint-disable-next-line no-param-reassign
|
|
3135
3438
|
line._cachedSyllableElements = null;
|
|
3136
|
-
|
|
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
|
+
}
|
|
3137
3533
|
}
|
|
3138
3534
|
/**
|
|
3139
3535
|
* Update syllables based on current time
|
|
@@ -3144,13 +3540,18 @@ class AmLyrics extends i {
|
|
|
3144
3540
|
let syllables = line._cachedSyllableElements;
|
|
3145
3541
|
if (!syllables) {
|
|
3146
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
|
+
}
|
|
3147
3548
|
// eslint-disable-next-line no-param-reassign
|
|
3148
3549
|
line._cachedSyllableElements = syllables;
|
|
3149
3550
|
}
|
|
3150
3551
|
for (let i = 0; i < syllables.length; i += 1) {
|
|
3151
3552
|
const syllable = syllables[i];
|
|
3152
|
-
const startTime =
|
|
3153
|
-
const endTime =
|
|
3553
|
+
const startTime = syllable._cachedStartTime;
|
|
3554
|
+
const endTime = syllable._cachedEndTime;
|
|
3154
3555
|
if (Number.isFinite(startTime) && Number.isFinite(endTime)) {
|
|
3155
3556
|
const { classList } = syllable;
|
|
3156
3557
|
const hasHighlight = classList.contains('highlight');
|
|
@@ -3188,6 +3589,7 @@ class AmLyrics extends i {
|
|
|
3188
3589
|
AmLyrics.updateSyllableAnimation(syllable, currentTimeMs - startTime);
|
|
3189
3590
|
}
|
|
3190
3591
|
classList.add('finished');
|
|
3592
|
+
// Keep the completed wipe state until user scroll resets it.
|
|
3191
3593
|
}
|
|
3192
3594
|
}
|
|
3193
3595
|
else if (hasHighlight || hasFinished) {
|
|
@@ -3470,7 +3872,8 @@ class AmLyrics extends i {
|
|
|
3470
3872
|
// Create background vocals container (with romanization support)
|
|
3471
3873
|
const backgroundVocalElement = hasBackground
|
|
3472
3874
|
? b `<p class="background-vocal-container">
|
|
3473
|
-
|
|
3875
|
+
<span class="background-vocal-wrap">
|
|
3876
|
+
${line.backgroundText.map((syllable, syllableIndex) => {
|
|
3474
3877
|
const startTimeMs = syllable.timestamp;
|
|
3475
3878
|
const endTimeMs = syllable.endtime;
|
|
3476
3879
|
const durationMs = endTimeMs - startTimeMs;
|
|
@@ -3478,36 +3881,37 @@ class AmLyrics extends i {
|
|
|
3478
3881
|
syllable.romanizedText &&
|
|
3479
3882
|
syllable.romanizedText.trim() !== syllable.text.trim()
|
|
3480
3883
|
? b `<span
|
|
3481
|
-
|
|
3884
|
+
class="lyrics-syllable transliteration no-chars ${syllable.lineSynced
|
|
3482
3885
|
? 'line-synced'
|
|
3483
3886
|
: ''}"
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
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
|
+
>`
|
|
3491
3894
|
: '';
|
|
3492
3895
|
return b `<span class="lyrics-word"
|
|
3493
|
-
|
|
3494
|
-
|
|
3896
|
+
><span
|
|
3897
|
+
class="lyrics-syllable-wrap${bgRomanizedText
|
|
3495
3898
|
? ' has-transliteration'
|
|
3496
3899
|
: ''}"
|
|
3497
|
-
|
|
3498
|
-
|
|
3900
|
+
><span
|
|
3901
|
+
class="lyrics-syllable no-chars${syllable.lineSynced
|
|
3499
3902
|
? ' line-synced'
|
|
3500
3903
|
: ''}"
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
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
|
+
>`;
|
|
3510
3913
|
})}
|
|
3914
|
+
</span>
|
|
3511
3915
|
</p>`
|
|
3512
3916
|
: '';
|
|
3513
3917
|
// Background vocals share the same line.translation and line.romanizedText
|
|
@@ -3521,6 +3925,7 @@ class AmLyrics extends i {
|
|
|
3521
3925
|
const wordGroups = lineData?.wordGroups ?? [];
|
|
3522
3926
|
const groupGrowable = lineData?.groupGrowable ?? [];
|
|
3523
3927
|
const groupGlowing = lineData?.groupGlowing ?? [];
|
|
3928
|
+
const groupCharRise = lineData?.groupCharRise ?? [];
|
|
3524
3929
|
const vwFullText = lineData?.vwFullText ?? [];
|
|
3525
3930
|
const vwFullDuration = lineData?.vwFullDuration ?? [];
|
|
3526
3931
|
const vwCharOffset = lineData?.vwCharOffset ?? [];
|
|
@@ -3532,11 +3937,17 @@ class AmLyrics extends i {
|
|
|
3532
3937
|
${wordGroups.map((group, groupIdx) => {
|
|
3533
3938
|
const isGrowable = groupGrowable[groupIdx];
|
|
3534
3939
|
const isGlowing = groupGlowing[groupIdx];
|
|
3940
|
+
const isCharRise = groupCharRise[groupIdx];
|
|
3941
|
+
const isAnimatedByChar = isGrowable || isCharRise;
|
|
3535
3942
|
const groupLineSynced = group.some(s => s.lineSynced);
|
|
3536
|
-
const wordText =
|
|
3537
|
-
const wordDuration =
|
|
3943
|
+
const wordText = isAnimatedByChar ? vwFullText[groupIdx] : '';
|
|
3944
|
+
const wordDuration = isAnimatedByChar
|
|
3945
|
+
? vwFullDuration[groupIdx]
|
|
3946
|
+
: 0;
|
|
3538
3947
|
const wordNumChars = wordText.length;
|
|
3539
|
-
const groupCharOffset =
|
|
3948
|
+
const groupCharOffset = isAnimatedByChar
|
|
3949
|
+
? vwCharOffset[groupIdx]
|
|
3950
|
+
: 0;
|
|
3540
3951
|
let sylCharAccumulator = 0;
|
|
3541
3952
|
const groupText = group.map(s => s.text).join('');
|
|
3542
3953
|
const shouldAllowBreak = groupText.trim().length >= 16 ||
|
|
@@ -3548,9 +3959,11 @@ class AmLyrics extends i {
|
|
|
3548
3959
|
// Base float is 0.8s, plus a portion of the audio duration, capped between 1.0s and 2.5s
|
|
3549
3960
|
const riseDuration = Math.max(1.2, Math.min(2.5, 1.2 + (actualDurationMs / 1000) * 0.6));
|
|
3550
3961
|
return b `<span
|
|
3551
|
-
class="lyrics-word${isGrowable ? ' growable' : ''}${
|
|
3552
|
-
? '
|
|
3553
|
-
: ''}${
|
|
3962
|
+
class="lyrics-word${isGrowable ? ' growable' : ''}${isCharRise
|
|
3963
|
+
? ' char-rise'
|
|
3964
|
+
: ''}${isGlowing ? ' glowing' : ''}${shouldAllowBreak
|
|
3965
|
+
? ' allow-break'
|
|
3966
|
+
: ''}"
|
|
3554
3967
|
style="--rise-duration: ${riseDuration}s"
|
|
3555
3968
|
>${group.map((syllable, sylIdx) => {
|
|
3556
3969
|
const startTimeMs = syllable.timestamp;
|
|
@@ -3573,7 +3986,7 @@ class AmLyrics extends i {
|
|
|
3573
3986
|
>`
|
|
3574
3987
|
: '';
|
|
3575
3988
|
let syllableContent = text;
|
|
3576
|
-
if (
|
|
3989
|
+
if (isAnimatedByChar) {
|
|
3577
3990
|
let charIndexInsideSyllable = 0;
|
|
3578
3991
|
const numCharsInSyllable = text.replace(/\s/g, '').length || 1;
|
|
3579
3992
|
syllableContent = b `${text.split('').map(char => {
|
|
@@ -3653,7 +4066,7 @@ class AmLyrics extends i {
|
|
|
3653
4066
|
><span
|
|
3654
4067
|
class="lyrics-syllable${groupLineSynced
|
|
3655
4068
|
? ' line-synced'
|
|
3656
|
-
: ''}${
|
|
4069
|
+
: ''}${isAnimatedByChar ? ' has-chars' : ' no-chars'}"
|
|
3657
4070
|
data-start-time="${startTimeMs}"
|
|
3658
4071
|
data-end-time="${endTimeMs}"
|
|
3659
4072
|
data-duration="${durationMs}"
|
|
@@ -4015,6 +4428,7 @@ AmLyrics.styles = i$3 `
|
|
|
4015
4428
|
-webkit-overflow-scrolling: touch;
|
|
4016
4429
|
box-sizing: border-box;
|
|
4017
4430
|
scrollbar-width: none;
|
|
4431
|
+
overflow-anchor: none;
|
|
4018
4432
|
}
|
|
4019
4433
|
|
|
4020
4434
|
.lyrics-container::-webkit-scrollbar {
|
|
@@ -4035,9 +4449,16 @@ AmLyrics.styles = i$3 `
|
|
|
4035
4449
|
}
|
|
4036
4450
|
|
|
4037
4451
|
.lyrics-line.scroll-animate {
|
|
4038
|
-
|
|
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);
|
|
4039
4460
|
animation-name: lyrics-scroll;
|
|
4040
|
-
animation-duration: var(--scroll-duration,
|
|
4461
|
+
animation-duration: var(--scroll-duration, 400ms);
|
|
4041
4462
|
animation-timing-function: cubic-bezier(0.41, 0, 0.12, 0.99);
|
|
4042
4463
|
animation-fill-mode: both;
|
|
4043
4464
|
animation-delay: var(--lyrics-line-delay, 0ms);
|
|
@@ -4058,12 +4479,15 @@ AmLyrics.styles = i$3 `
|
|
|
4058
4479
|
font-size: var(--lyplus-font-size-base);
|
|
4059
4480
|
cursor: pointer;
|
|
4060
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. */
|
|
4061
4484
|
transition:
|
|
4062
|
-
opacity 0.
|
|
4485
|
+
opacity 0.7s ease,
|
|
4063
4486
|
transform 0.4s cubic-bezier(0.41, 0, 0.12, 0.99)
|
|
4064
4487
|
var(--lyrics-line-delay, 0ms),
|
|
4065
|
-
filter 0.
|
|
4488
|
+
filter 0.7s ease;
|
|
4066
4489
|
content-visibility: auto;
|
|
4490
|
+
contain: layout style;
|
|
4067
4491
|
text-rendering: optimizeLegibility;
|
|
4068
4492
|
}
|
|
4069
4493
|
|
|
@@ -4075,7 +4499,7 @@ AmLyrics.styles = i$3 `
|
|
|
4075
4499
|
.lyrics-line-container {
|
|
4076
4500
|
overflow-wrap: break-word;
|
|
4077
4501
|
transform-origin: left;
|
|
4078
|
-
transform:
|
|
4502
|
+
transform: translateZ(0);
|
|
4079
4503
|
transition:
|
|
4080
4504
|
transform 0.7s ease,
|
|
4081
4505
|
background-color 0.7s,
|
|
@@ -4084,7 +4508,7 @@ AmLyrics.styles = i$3 `
|
|
|
4084
4508
|
|
|
4085
4509
|
.lyrics-line.active .lyrics-line-container,
|
|
4086
4510
|
.lyrics-line.pre-active .lyrics-line-container {
|
|
4087
|
-
transform:
|
|
4511
|
+
transform: translateZ(0);
|
|
4088
4512
|
transition:
|
|
4089
4513
|
transform 0.5s ease,
|
|
4090
4514
|
background-color 0.18s,
|
|
@@ -4098,31 +4522,50 @@ AmLyrics.styles = i$3 `
|
|
|
4098
4522
|
|
|
4099
4523
|
.background-vocal-container {
|
|
4100
4524
|
max-height: 0;
|
|
4101
|
-
padding-top: 0;
|
|
4102
|
-
transform: translateY(-0.5em) scale(0.95);
|
|
4103
4525
|
overflow: visible;
|
|
4104
4526
|
opacity: 0;
|
|
4105
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. */
|
|
4106
4533
|
transition:
|
|
4107
|
-
max-height
|
|
4108
|
-
opacity
|
|
4109
|
-
transform 450ms cubic-bezier(0.33, 1, 0.68, 1),
|
|
4110
|
-
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);
|
|
4111
4536
|
margin: 0;
|
|
4537
|
+
pointer-events: none;
|
|
4112
4538
|
}
|
|
4113
4539
|
|
|
4114
|
-
.
|
|
4115
|
-
|
|
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 {
|
|
4116
4557
|
max-height: 4em;
|
|
4117
4558
|
opacity: 1;
|
|
4118
|
-
|
|
4119
|
-
transform: translateY(0) scale(1);
|
|
4559
|
+
/* Slower entry (0.6 s) so bg vocals expand smoothly. */
|
|
4120
4560
|
transition:
|
|
4121
|
-
max-height
|
|
4122
|
-
opacity
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
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;
|
|
4126
4569
|
}
|
|
4127
4570
|
|
|
4128
4571
|
/* --- Line States & Modifiers --- */
|
|
@@ -4135,6 +4578,16 @@ AmLyrics.styles = i$3 `
|
|
|
4135
4578
|
opacity: 1;
|
|
4136
4579
|
}
|
|
4137
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
|
+
|
|
4138
4591
|
.lyrics-line.singer-right {
|
|
4139
4592
|
text-align: end;
|
|
4140
4593
|
}
|
|
@@ -4199,22 +4652,32 @@ AmLyrics.styles = i$3 `
|
|
|
4199
4652
|
|
|
4200
4653
|
/* --- Blur Effect for Inactive Lines --- */
|
|
4201
4654
|
.lyrics-container.blur-inactive-enabled:not(.not-focused)
|
|
4202
|
-
.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
|
+
) {
|
|
4203
4658
|
filter: blur(var(--lyplus-blur-amount));
|
|
4204
4659
|
}
|
|
4205
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
|
+
|
|
4206
4669
|
.lyrics-container.blur-inactive-enabled:not(.not-focused)
|
|
4207
4670
|
.lyrics-line.post-active-line:not(.lyrics-gap):not(.active):not(
|
|
4208
4671
|
.pre-active
|
|
4209
|
-
),
|
|
4672
|
+
):not(.persist-highlight),
|
|
4210
4673
|
.lyrics-container.blur-inactive-enabled:not(.not-focused)
|
|
4211
4674
|
.lyrics-line.next-active-line:not(.lyrics-gap):not(.active):not(
|
|
4212
4675
|
.pre-active
|
|
4213
|
-
),
|
|
4676
|
+
):not(.persist-highlight),
|
|
4214
4677
|
.lyrics-container.blur-inactive-enabled:not(.not-focused)
|
|
4215
4678
|
.lyrics-line.lyrics-activest:not(.active):not(.lyrics-gap):not(
|
|
4216
4679
|
.pre-active
|
|
4217
|
-
) {
|
|
4680
|
+
):not(.persist-highlight) {
|
|
4218
4681
|
filter: blur(var(--lyplus-blur-amount-near));
|
|
4219
4682
|
}
|
|
4220
4683
|
|
|
@@ -4244,6 +4707,17 @@ AmLyrics.styles = i$3 `
|
|
|
4244
4707
|
display: inline;
|
|
4245
4708
|
}
|
|
4246
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
|
+
|
|
4247
4721
|
.lyrics-syllable-wrap {
|
|
4248
4722
|
display: inline;
|
|
4249
4723
|
}
|
|
@@ -4273,17 +4747,19 @@ AmLyrics.styles = i$3 `
|
|
|
4273
4747
|
/* --- Syllable States --- */
|
|
4274
4748
|
.lyrics-syllable.finished {
|
|
4275
4749
|
background-color: var(--lyplus-text-primary);
|
|
4276
|
-
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;
|
|
4277
4757
|
}
|
|
4278
4758
|
|
|
4279
4759
|
.lyrics-syllable.finished.has-chars {
|
|
4280
4760
|
background-color: transparent;
|
|
4281
4761
|
}
|
|
4282
4762
|
|
|
4283
|
-
.lyrics-line:not(.active) .lyrics-syllable.finished {
|
|
4284
|
-
transition: color 0.18s;
|
|
4285
|
-
}
|
|
4286
|
-
|
|
4287
4763
|
.lyrics-line.active:not(.lyrics-gap) .lyrics-syllable {
|
|
4288
4764
|
transition:
|
|
4289
4765
|
transform 1s ease,
|
|
@@ -4333,7 +4809,60 @@ AmLyrics.styles = i$3 `
|
|
|
4333
4809
|
);
|
|
4334
4810
|
background-position:
|
|
4335
4811
|
calc(100% + 0.5em) 0%,
|
|
4336
|
-
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
|
+
);
|
|
4337
4866
|
}
|
|
4338
4867
|
|
|
4339
4868
|
/* Non-growable words float up with a gentle curve */
|
|
@@ -4341,18 +4870,88 @@ AmLyrics.styles = i$3 `
|
|
|
4341
4870
|
.lyrics-word:not(.growable)
|
|
4342
4871
|
.lyrics-syllable.highlight {
|
|
4343
4872
|
transform: translateY(-3.5%);
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4873
|
+
}
|
|
4874
|
+
|
|
4875
|
+
.lyrics-line.persist-highlight:not(.lyrics-gap)
|
|
4876
|
+
.lyrics-word:not(.growable)
|
|
4877
|
+
.lyrics-syllable.finished {
|
|
4878
|
+
transform: translateY(-3.5%);
|
|
4348
4879
|
}
|
|
4349
4880
|
|
|
4350
4881
|
.lyrics-word.growable .lyrics-syllable.cleanup .char {
|
|
4351
4882
|
transform: translateY(-3.5%);
|
|
4352
4883
|
}
|
|
4353
4884
|
|
|
4354
|
-
.lyrics-
|
|
4355
|
-
|
|
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;
|
|
4356
4955
|
}
|
|
4357
4956
|
|
|
4358
4957
|
.lyrics-syllable.pre-highlight {
|
|
@@ -4396,8 +4995,11 @@ AmLyrics.styles = i$3 `
|
|
|
4396
4995
|
}
|
|
4397
4996
|
|
|
4398
4997
|
.lyrics-syllable.finished span.char {
|
|
4399
|
-
transition: color 0.18s;
|
|
4400
4998
|
background-color: var(--lyplus-text-primary);
|
|
4999
|
+
transition:
|
|
5000
|
+
color 0.7s,
|
|
5001
|
+
background-color 0.7s,
|
|
5002
|
+
transform 0.7s ease;
|
|
4401
5003
|
}
|
|
4402
5004
|
|
|
4403
5005
|
/* Active char spans: structural only, wipe animation sets gradient */
|
|
@@ -4588,7 +5190,7 @@ AmLyrics.styles = i$3 `
|
|
|
4588
5190
|
position: relative;
|
|
4589
5191
|
box-sizing: border-box;
|
|
4590
5192
|
font-weight: normal;
|
|
4591
|
-
transform: translateY(var(--lyrics-scroll-offset, 0px))
|
|
5193
|
+
transform: translateY(var(--lyrics-scroll-offset, 0px));
|
|
4592
5194
|
transition:
|
|
4593
5195
|
opacity 0.3s ease,
|
|
4594
5196
|
transform 0.6s cubic-bezier(0.23, 1, 0.32, 1)
|
|
@@ -4599,7 +5201,7 @@ AmLyrics.styles = i$3 `
|
|
|
4599
5201
|
.lyrics-plus-empty {
|
|
4600
5202
|
display: block;
|
|
4601
5203
|
height: 100vh;
|
|
4602
|
-
transform: translateY(var(--lyrics-scroll-offset, 0px))
|
|
5204
|
+
transform: translateY(var(--lyrics-scroll-offset, 0px));
|
|
4603
5205
|
}
|
|
4604
5206
|
|
|
4605
5207
|
.lyrics-footer {
|
|
@@ -4608,8 +5210,8 @@ AmLyrics.styles = i$3 `
|
|
|
4608
5210
|
align-items: center;
|
|
4609
5211
|
flex-wrap: wrap;
|
|
4610
5212
|
text-align: left;
|
|
4611
|
-
font-size:
|
|
4612
|
-
color:
|
|
5213
|
+
font-size: calc(var(--lyplus-font-size-base) * 0.5);
|
|
5214
|
+
color: var(--lyplus-text-secondary);
|
|
4613
5215
|
padding: 20px 0 50vh 0;
|
|
4614
5216
|
margin-top: 10px;
|
|
4615
5217
|
font-weight: 400;
|
|
@@ -4622,9 +5224,9 @@ AmLyrics.styles = i$3 `
|
|
|
4622
5224
|
}
|
|
4623
5225
|
|
|
4624
5226
|
.lyrics-footer.lyrics-line {
|
|
4625
|
-
font-size:
|
|
5227
|
+
font-size: calc(var(--lyplus-font-size-base) * 0.5);
|
|
4626
5228
|
padding: 20px var(--lyplus-padding-line) 50vh var(--lyplus-padding-line);
|
|
4627
|
-
|
|
5229
|
+
margin-top: 0;
|
|
4628
5230
|
}
|
|
4629
5231
|
|
|
4630
5232
|
.lyrics-footer.active {
|
|
@@ -4912,7 +5514,7 @@ AmLyrics.styles = i$3 `
|
|
|
4912
5514
|
0% 100%;
|
|
4913
5515
|
background-position:
|
|
4914
5516
|
calc(100% + 0.375em) 0%,
|
|
4915
|
-
calc(100% + 0.36em)
|
|
5517
|
+
calc(100% + 0.36em) 0%;
|
|
4916
5518
|
}
|
|
4917
5519
|
to {
|
|
4918
5520
|
background-size:
|
|
@@ -4920,7 +5522,7 @@ AmLyrics.styles = i$3 `
|
|
|
4920
5522
|
100% 100%;
|
|
4921
5523
|
background-position:
|
|
4922
5524
|
-0.75em 0%,
|
|
4923
|
-
right
|
|
5525
|
+
right 0%;
|
|
4924
5526
|
}
|
|
4925
5527
|
}
|
|
4926
5528
|
|
|
@@ -4931,7 +5533,7 @@ AmLyrics.styles = i$3 `
|
|
|
4931
5533
|
0% 100%;
|
|
4932
5534
|
background-position:
|
|
4933
5535
|
calc(100% + 0.75em) 0%,
|
|
4934
|
-
calc(100% + 0.5em)
|
|
5536
|
+
calc(100% + 0.5em) 0%;
|
|
4935
5537
|
}
|
|
4936
5538
|
100% {
|
|
4937
5539
|
background-size:
|
|
@@ -4939,7 +5541,7 @@ AmLyrics.styles = i$3 `
|
|
|
4939
5541
|
100% 100%;
|
|
4940
5542
|
background-position:
|
|
4941
5543
|
-0.75em 0%,
|
|
4942
|
-
right
|
|
5544
|
+
right 0%;
|
|
4943
5545
|
}
|
|
4944
5546
|
}
|
|
4945
5547
|
|
|
@@ -4969,7 +5571,7 @@ AmLyrics.styles = i$3 `
|
|
|
4969
5571
|
0% 100%;
|
|
4970
5572
|
background-position:
|
|
4971
5573
|
calc(100% + 0.75em) 0%,
|
|
4972
|
-
right
|
|
5574
|
+
right 0%;
|
|
4973
5575
|
}
|
|
4974
5576
|
to {
|
|
4975
5577
|
background-size:
|
|
@@ -4977,7 +5579,7 @@ AmLyrics.styles = i$3 `
|
|
|
4977
5579
|
0% 100%;
|
|
4978
5580
|
background-position:
|
|
4979
5581
|
calc(100% + 0.375em) 0%,
|
|
4980
|
-
right
|
|
5582
|
+
right 0%;
|
|
4981
5583
|
}
|
|
4982
5584
|
}
|
|
4983
5585
|
|
|
@@ -5043,7 +5645,7 @@ AmLyrics.styles = i$3 `
|
|
|
5043
5645
|
}
|
|
5044
5646
|
|
|
5045
5647
|
/* Character grow animation — translate3d+scale3d for smooth transform,
|
|
5046
|
-
drop-shadow for glow
|
|
5648
|
+
drop-shadow for glow */
|
|
5047
5649
|
@keyframes grow-dynamic {
|
|
5048
5650
|
0% {
|
|
5049
5651
|
transform: translate3d(0, 0, 0) scale3d(1, 1, 1);
|
|
@@ -5080,6 +5682,16 @@ AmLyrics.styles = i$3 `
|
|
|
5080
5682
|
}
|
|
5081
5683
|
}
|
|
5082
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
|
+
|
|
5083
5695
|
@keyframes grow-static {
|
|
5084
5696
|
0%,
|
|
5085
5697
|
100% {
|