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