@uimaxbai/am-lyrics 1.1.7 → 1.2.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/README.md +4 -0
- package/demo.webp +0 -0
- package/dist/src/AmLyrics.d.ts +8 -0
- package/dist/src/AmLyrics.d.ts.map +1 -1
- package/dist/src/GoogleService.d.ts +16 -3
- package/dist/src/GoogleService.d.ts.map +1 -1
- package/dist/src/am-lyrics.js +502 -376
- package/dist/src/am-lyrics.js.map +1 -1
- package/dist/src/react.js +502 -376
- package/dist/src/react.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/AmLyrics.ts +617 -432
- package/src/GoogleService.ts +49 -19
package/dist/src/react.js
CHANGED
|
@@ -88,6 +88,7 @@ const CONFIG = {
|
|
|
88
88
|
GOOGLE: {
|
|
89
89
|
MAX_RETRIES: 3,
|
|
90
90
|
RETRY_DELAY_MS: 1000,
|
|
91
|
+
FETCH_TIMEOUT_MS: 6000,
|
|
91
92
|
},
|
|
92
93
|
};
|
|
93
94
|
/**
|
|
@@ -99,6 +100,11 @@ class GoogleService {
|
|
|
99
100
|
setTimeout(resolve, ms);
|
|
100
101
|
});
|
|
101
102
|
}
|
|
103
|
+
static fetchWithTimeout(url, timeoutMs = CONFIG.GOOGLE.FETCH_TIMEOUT_MS) {
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
106
|
+
return fetch(url, { signal: controller.signal }).finally(() => clearTimeout(timeoutId));
|
|
107
|
+
}
|
|
102
108
|
static isPurelyLatinScript(text) {
|
|
103
109
|
// Basic check for Latin script characters plus common punctuation and numbers
|
|
104
110
|
// eslint-disable-next-line no-control-regex
|
|
@@ -140,7 +146,7 @@ class GoogleService {
|
|
|
140
146
|
try {
|
|
141
147
|
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${targetLang}&dt=t&q=${encodeURIComponent(joinedText)}`;
|
|
142
148
|
// eslint-disable-next-line no-await-in-loop
|
|
143
|
-
const response = await
|
|
149
|
+
const response = await GoogleService.fetchWithTimeout(url);
|
|
144
150
|
if (!response.ok)
|
|
145
151
|
throw new Error(`Status ${response.status}`);
|
|
146
152
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -213,9 +219,11 @@ class GoogleService {
|
|
|
213
219
|
// Determine if we should treat as word-synced (has syllabus) or line-synced
|
|
214
220
|
const lines = Array.isArray(originalLyrics)
|
|
215
221
|
? originalLyrics
|
|
216
|
-
: originalLyrics.data ||
|
|
217
|
-
|
|
218
|
-
|
|
222
|
+
: originalLyrics.data ||
|
|
223
|
+
originalLyrics.content ||
|
|
224
|
+
[];
|
|
225
|
+
if (!lines || lines.length === 0)
|
|
226
|
+
return Array.isArray(originalLyrics) ? originalLyrics : [];
|
|
219
227
|
// Check if word synced
|
|
220
228
|
const isWordSynced = lines.some((l) => l.isWordSynced !== false && Array.isArray(l.text) && l.text.length > 1);
|
|
221
229
|
if (isWordSynced) {
|
|
@@ -232,7 +240,9 @@ class GoogleService {
|
|
|
232
240
|
)
|
|
233
241
|
return line;
|
|
234
242
|
// Get the entire line text to romanize together for context-aware pronunciation
|
|
235
|
-
const fullText = line.text
|
|
243
|
+
const fullText = line.text
|
|
244
|
+
.map((s) => s.text)
|
|
245
|
+
.join('');
|
|
236
246
|
// romanizeTexts expects an array of strings, so we pass an array of one
|
|
237
247
|
const [romanizedFullLine] = await this.romanizeTexts([fullText]);
|
|
238
248
|
const newSyllabus = line.text.map((s) => ({
|
|
@@ -282,7 +292,7 @@ class GoogleService {
|
|
|
282
292
|
while (attempt < CONFIG.GOOGLE.MAX_RETRIES && !success) {
|
|
283
293
|
try {
|
|
284
294
|
const romanizeUrl = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=en&dt=rm&q=${encodeURIComponent(text)}`;
|
|
285
|
-
const response = await
|
|
295
|
+
const response = await GoogleService.fetchWithTimeout(romanizeUrl);
|
|
286
296
|
const data = await response.json();
|
|
287
297
|
// Response format is [[["...","...","...","romanization"]],...]
|
|
288
298
|
// or [null, ...] for English/Latin input where no romanization is needed
|
|
@@ -312,8 +322,26 @@ class GoogleService {
|
|
|
312
322
|
}
|
|
313
323
|
}
|
|
314
324
|
|
|
315
|
-
const VERSION = '1.
|
|
325
|
+
const VERSION = '1.2.0';
|
|
316
326
|
const INSTRUMENTAL_THRESHOLD_MS = 7000; // Show dots for gaps >= 7s
|
|
327
|
+
const FETCH_TIMEOUT_MS = 8000; // Timeout for all lyrics fetch requests
|
|
328
|
+
const SEEK_THRESHOLD_MS = 500;
|
|
329
|
+
const PRE_SCROLL_LEAD_MS = 500;
|
|
330
|
+
const SCROLL_ANIMATION_DURATION_MS = 280;
|
|
331
|
+
const SCROLL_DELAY_INCREMENT_MS = 24;
|
|
332
|
+
const GAP_PULSE_DURATION_MS = 4000;
|
|
333
|
+
const GAP_PULSE_CYCLE_MS = GAP_PULSE_DURATION_MS * 2;
|
|
334
|
+
const GAP_EXIT_LEAD_MS = 360;
|
|
335
|
+
const GAP_MIN_SCALE = 0.85;
|
|
336
|
+
/**
|
|
337
|
+
* Fetch with an automatic timeout via AbortSignal.
|
|
338
|
+
* Rejects if the request takes longer than `timeoutMs`.
|
|
339
|
+
*/
|
|
340
|
+
function fetchWithTimeout(url, options = {}, timeoutMs = FETCH_TIMEOUT_MS) {
|
|
341
|
+
const controller = new AbortController();
|
|
342
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
343
|
+
return fetch(url, { ...options, signal: controller.signal }).finally(() => clearTimeout(timeoutId));
|
|
344
|
+
}
|
|
317
345
|
const KPOE_SERVERS = [
|
|
318
346
|
'https://lyricsplus.binimum.org',
|
|
319
347
|
'https://lyricsplus.atomix.one',
|
|
@@ -377,6 +405,9 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
377
405
|
// Syllable animation tracking
|
|
378
406
|
this.lastActiveIndex = 0;
|
|
379
407
|
this.visibleLineIds = new Set();
|
|
408
|
+
// Bound handler references for proper event listener removal
|
|
409
|
+
this._boundHandleUserScroll = this.handleUserScroll.bind(this);
|
|
410
|
+
this._boundAnimateProgress = this.animateProgress.bind(this);
|
|
380
411
|
}
|
|
381
412
|
async toggleRomanization() {
|
|
382
413
|
this.showRomanization = !this.showRomanization;
|
|
@@ -462,12 +493,38 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
462
493
|
super.disconnectedCallback();
|
|
463
494
|
if (this.animationFrameId) {
|
|
464
495
|
cancelAnimationFrame(this.animationFrameId);
|
|
496
|
+
this.animationFrameId = undefined;
|
|
465
497
|
}
|
|
466
498
|
if (this.userScrollTimeoutId) {
|
|
467
499
|
clearTimeout(this.userScrollTimeoutId);
|
|
500
|
+
this.userScrollTimeoutId = undefined;
|
|
501
|
+
}
|
|
502
|
+
if (this.clickSeekTimeout) {
|
|
503
|
+
clearTimeout(this.clickSeekTimeout);
|
|
504
|
+
this.clickSeekTimeout = undefined;
|
|
505
|
+
}
|
|
506
|
+
if (this.scrollUnlockTimeout) {
|
|
507
|
+
clearTimeout(this.scrollUnlockTimeout);
|
|
508
|
+
this.scrollUnlockTimeout = undefined;
|
|
509
|
+
}
|
|
510
|
+
if (this.scrollAnimationTimeout) {
|
|
511
|
+
clearTimeout(this.scrollAnimationTimeout);
|
|
512
|
+
this.scrollAnimationTimeout = undefined;
|
|
513
|
+
}
|
|
514
|
+
// Cancel any in-flight fetch requests
|
|
515
|
+
this.fetchAbortController?.abort();
|
|
516
|
+
this.fetchAbortController = undefined;
|
|
517
|
+
// Remove scroll event listeners
|
|
518
|
+
if (this.lyricsContainer) {
|
|
519
|
+
this.lyricsContainer.removeEventListener('wheel', this._boundHandleUserScroll);
|
|
520
|
+
this.lyricsContainer.removeEventListener('touchmove', this._boundHandleUserScroll);
|
|
468
521
|
}
|
|
469
522
|
}
|
|
470
523
|
async fetchLyrics() {
|
|
524
|
+
// Cancel any in-flight fetch to prevent stale results from racing
|
|
525
|
+
this.fetchAbortController?.abort();
|
|
526
|
+
const controller = new AbortController();
|
|
527
|
+
this.fetchAbortController = controller;
|
|
471
528
|
this.isLoading = true;
|
|
472
529
|
this.lyrics = undefined;
|
|
473
530
|
this.lyricsSource = null;
|
|
@@ -477,6 +534,9 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
477
534
|
this.hasFetchedAllProviders = false;
|
|
478
535
|
try {
|
|
479
536
|
const resolvedMetadata = await this.resolveSongMetadata();
|
|
537
|
+
// If a newer fetch was triggered while we awaited, bail out
|
|
538
|
+
if (controller.signal.aborted)
|
|
539
|
+
return;
|
|
480
540
|
const isMusicIdOnlyRequest = Boolean(this.musicId) &&
|
|
481
541
|
!this.songTitle &&
|
|
482
542
|
!this.songArtist &&
|
|
@@ -535,7 +595,10 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
535
595
|
this.lyricsSource = null;
|
|
536
596
|
}
|
|
537
597
|
finally {
|
|
538
|
-
this
|
|
598
|
+
// Only update loading state if this fetch wasn't superseded
|
|
599
|
+
if (!controller.signal.aborted) {
|
|
600
|
+
this.isLoading = false;
|
|
601
|
+
}
|
|
539
602
|
}
|
|
540
603
|
}
|
|
541
604
|
async onLyricsLoaded() {
|
|
@@ -796,7 +859,7 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
796
859
|
const url = `${normalizedBase}/v1/songlist/search?q=${encodeURIComponent(trimmedQuery)}`;
|
|
797
860
|
try {
|
|
798
861
|
// eslint-disable-next-line no-await-in-loop
|
|
799
|
-
const response = await
|
|
862
|
+
const response = await fetchWithTimeout(url);
|
|
800
863
|
if (response.ok) {
|
|
801
864
|
// eslint-disable-next-line no-await-in-loop
|
|
802
865
|
const payload = await response.json();
|
|
@@ -830,7 +893,9 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
830
893
|
if (metadata.durationMs && metadata.durationMs > 0) {
|
|
831
894
|
params.append('duration', Math.round(metadata.durationMs / 1000).toString());
|
|
832
895
|
}
|
|
833
|
-
|
|
896
|
+
if (!DEFAULT_KPOE_SOURCE_ORDER.includes('apple')) {
|
|
897
|
+
params.append('source', DEFAULT_KPOE_SOURCE_ORDER);
|
|
898
|
+
}
|
|
834
899
|
const getRank = (sourceLabel, parsedLines) => {
|
|
835
900
|
const lower = sourceLabel.toLowerCase();
|
|
836
901
|
const hasWordSync = parsedLines.some((line) => line.text && Array.isArray(line.text) && line.text.length > 1);
|
|
@@ -875,13 +940,13 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
875
940
|
cacheParams.append('duration', Math.round(metadata.durationMs / 1000).toString());
|
|
876
941
|
}
|
|
877
942
|
const cacheUrl = `https://lyrics-api.binimum.org/?${cacheParams.toString()}`;
|
|
878
|
-
const cacheRes = await
|
|
943
|
+
const cacheRes = await fetchWithTimeout(cacheUrl);
|
|
879
944
|
if (cacheRes.ok) {
|
|
880
945
|
const cacheData = await cacheRes.json();
|
|
881
946
|
if (cacheData.results && cacheData.results.length > 0) {
|
|
882
947
|
const result = cacheData.results[0];
|
|
883
948
|
if (result.timing_type === 'word' && result.lyricsUrl) {
|
|
884
|
-
const ttmlRes = await
|
|
949
|
+
const ttmlRes = await fetchWithTimeout(result.lyricsUrl);
|
|
885
950
|
if (ttmlRes.ok) {
|
|
886
951
|
const ttmlText = await ttmlRes.text();
|
|
887
952
|
const lines = AmLyrics.parseTTML(ttmlText);
|
|
@@ -896,7 +961,7 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
896
961
|
const fallbackParams = new URLSearchParams(params);
|
|
897
962
|
const fallbackUrl = `https://lyricsplus.binimum.org/v2/lyrics/get?${fallbackParams.toString()}`;
|
|
898
963
|
try {
|
|
899
|
-
const fallbackRes = await
|
|
964
|
+
const fallbackRes = await fetchWithTimeout(fallbackUrl);
|
|
900
965
|
if (fallbackRes.ok) {
|
|
901
966
|
const payload = await fallbackRes.json();
|
|
902
967
|
const lines = AmLyrics.convertKPoeLyrics(payload);
|
|
@@ -917,7 +982,7 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
917
982
|
}
|
|
918
983
|
// If fallback fails or has no word sync, fall back to bini lyrics
|
|
919
984
|
if (result.lyricsUrl) {
|
|
920
|
-
const ttmlRes = await
|
|
985
|
+
const ttmlRes = await fetchWithTimeout(result.lyricsUrl);
|
|
921
986
|
if (ttmlRes.ok) {
|
|
922
987
|
const ttmlText = await ttmlRes.text();
|
|
923
988
|
const lines = AmLyrics.parseTTML(ttmlText);
|
|
@@ -939,23 +1004,23 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
939
1004
|
console.error('Cache API failed', e);
|
|
940
1005
|
}
|
|
941
1006
|
// Shuffle servers so we pick a random one first, with all others as fallback
|
|
942
|
-
//
|
|
1007
|
+
// Try up to 3 servers to improve reliability when some have CORS or connectivity issues
|
|
943
1008
|
const shuffledServers = [...KPOE_SERVERS]
|
|
944
1009
|
.sort(() => Math.random() - 0.5)
|
|
945
|
-
.slice(0,
|
|
1010
|
+
.slice(0, 3);
|
|
946
1011
|
for (const base of shuffledServers) {
|
|
947
1012
|
const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base;
|
|
948
1013
|
const url = `${normalizedBase}/v2/lyrics/get?${params.toString()}`;
|
|
949
1014
|
let payload = null;
|
|
950
1015
|
try {
|
|
951
1016
|
// eslint-disable-next-line no-await-in-loop
|
|
952
|
-
const response = await
|
|
1017
|
+
const response = await fetchWithTimeout(url);
|
|
953
1018
|
if (response.ok) {
|
|
954
1019
|
// eslint-disable-next-line no-await-in-loop
|
|
955
1020
|
payload = await response.json();
|
|
956
1021
|
}
|
|
957
1022
|
}
|
|
958
|
-
catch
|
|
1023
|
+
catch {
|
|
959
1024
|
payload = null;
|
|
960
1025
|
}
|
|
961
1026
|
if (payload) {
|
|
@@ -981,7 +1046,7 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
981
1046
|
try {
|
|
982
1047
|
const fallbackParams = new URLSearchParams(params);
|
|
983
1048
|
const url = `https://lyricsplus.binimum.org/v2/lyrics/get?${fallbackParams.toString()}`;
|
|
984
|
-
const response = await
|
|
1049
|
+
const response = await fetchWithTimeout(url);
|
|
985
1050
|
if (response.ok) {
|
|
986
1051
|
const payload = await response.json();
|
|
987
1052
|
if (payload) {
|
|
@@ -1017,7 +1082,6 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1017
1082
|
if (!match) {
|
|
1018
1083
|
// Skip non-timestamped lines (headers like [ti:], [ar:], etc.)
|
|
1019
1084
|
// eslint-disable-next-line no-continue
|
|
1020
|
-
// eslint-disable-next-line no-continue
|
|
1021
1085
|
continue;
|
|
1022
1086
|
}
|
|
1023
1087
|
const minutes = parseInt(match[1], 10);
|
|
@@ -1037,7 +1101,6 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1037
1101
|
const endtime = i + 1 < parsed.length ? parsed[i + 1].timestamp : timestamp + 5000;
|
|
1038
1102
|
// Skip empty lines (instrumental gaps)
|
|
1039
1103
|
if (!text.trim()) {
|
|
1040
|
-
// eslint-disable-next-line no-continue
|
|
1041
1104
|
// eslint-disable-next-line no-continue
|
|
1042
1105
|
continue;
|
|
1043
1106
|
}
|
|
@@ -1069,9 +1132,9 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1069
1132
|
const artist = metadata.artist?.trim();
|
|
1070
1133
|
if (!title || !artist)
|
|
1071
1134
|
return null;
|
|
1072
|
-
// Pick
|
|
1135
|
+
// Pick 3 random unique servers for better reliability
|
|
1073
1136
|
const shuffled = [...TIDAL_SERVERS].sort(() => Math.random() - 0.5);
|
|
1074
|
-
const serversToTry = shuffled.slice(0,
|
|
1137
|
+
const serversToTry = shuffled.slice(0, 3);
|
|
1075
1138
|
for (const base of serversToTry) {
|
|
1076
1139
|
try {
|
|
1077
1140
|
const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base;
|
|
@@ -1079,9 +1142,8 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1079
1142
|
const searchQuery = `${title} ${artist}`;
|
|
1080
1143
|
const searchParams = new URLSearchParams({ s: searchQuery });
|
|
1081
1144
|
// eslint-disable-next-line no-await-in-loop
|
|
1082
|
-
const searchResponse = await
|
|
1145
|
+
const searchResponse = await fetchWithTimeout(`${normalizedBase}/search/?${searchParams.toString()}`);
|
|
1083
1146
|
if (!searchResponse.ok) {
|
|
1084
|
-
// eslint-disable-next-line no-continue
|
|
1085
1147
|
// eslint-disable-next-line no-continue
|
|
1086
1148
|
continue;
|
|
1087
1149
|
}
|
|
@@ -1089,7 +1151,6 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1089
1151
|
const searchData = await searchResponse.json();
|
|
1090
1152
|
const items = searchData?.data?.items;
|
|
1091
1153
|
if (!Array.isArray(items) || items.length === 0) {
|
|
1092
|
-
// eslint-disable-next-line no-continue
|
|
1093
1154
|
// eslint-disable-next-line no-continue
|
|
1094
1155
|
continue;
|
|
1095
1156
|
}
|
|
@@ -1103,15 +1164,13 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1103
1164
|
}
|
|
1104
1165
|
const trackId = bestTrack?.id;
|
|
1105
1166
|
if (!trackId) {
|
|
1106
|
-
// eslint-disable-next-line no-continue
|
|
1107
1167
|
// eslint-disable-next-line no-continue
|
|
1108
1168
|
continue;
|
|
1109
1169
|
}
|
|
1110
1170
|
// Step 2: Fetch lyrics
|
|
1111
1171
|
// eslint-disable-next-line no-await-in-loop
|
|
1112
|
-
const lyricsResponse = await
|
|
1172
|
+
const lyricsResponse = await fetchWithTimeout(`${normalizedBase}/lyrics/?id=${trackId}`);
|
|
1113
1173
|
if (!lyricsResponse.ok) {
|
|
1114
|
-
// eslint-disable-next-line no-continue
|
|
1115
1174
|
// eslint-disable-next-line no-continue
|
|
1116
1175
|
continue;
|
|
1117
1176
|
}
|
|
@@ -1147,7 +1206,7 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1147
1206
|
try {
|
|
1148
1207
|
const searchQuery = `${artist} ${title}`;
|
|
1149
1208
|
const params = new URLSearchParams({ q: searchQuery });
|
|
1150
|
-
const response = await
|
|
1209
|
+
const response = await fetchWithTimeout(`https://lrclib.net/api/search?${params.toString()}`, {
|
|
1151
1210
|
headers: {
|
|
1152
1211
|
'User-Agent': `apple-music-web-components/${VERSION}`,
|
|
1153
1212
|
},
|
|
@@ -1205,7 +1264,7 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1205
1264
|
return null;
|
|
1206
1265
|
try {
|
|
1207
1266
|
const params = new URLSearchParams({ title, artist });
|
|
1208
|
-
const response = await
|
|
1267
|
+
const response = await fetchWithTimeout(`${GENIUS_WORKER_URL}?${params.toString()}`);
|
|
1209
1268
|
if (!response.ok)
|
|
1210
1269
|
return null;
|
|
1211
1270
|
const data = await response.json();
|
|
@@ -1236,8 +1295,7 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1236
1295
|
}
|
|
1237
1296
|
}
|
|
1238
1297
|
catch {
|
|
1239
|
-
//
|
|
1240
|
-
console.error('No Genius lyrics found');
|
|
1298
|
+
// Genius fetch failed, will fall through to return null
|
|
1241
1299
|
}
|
|
1242
1300
|
return null;
|
|
1243
1301
|
}
|
|
@@ -1329,19 +1387,6 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1329
1387
|
}
|
|
1330
1388
|
}
|
|
1331
1389
|
}
|
|
1332
|
-
const transliterationNodes = doc.getElementsByTagName('transliteration');
|
|
1333
|
-
for (let i = 0; i < transliterationNodes.length; i += 1) {
|
|
1334
|
-
const texts = transliterationNodes[i].getElementsByTagName('text');
|
|
1335
|
-
for (let j = 0; j < texts.length; j += 1) {
|
|
1336
|
-
const textNode = texts[j];
|
|
1337
|
-
const key = textNode.getAttribute('for');
|
|
1338
|
-
if (key && textNode.textContent) {
|
|
1339
|
-
transliterations[key] = textNode.textContent
|
|
1340
|
-
.trim()
|
|
1341
|
-
.replace(/\s+/g, ' ');
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
1390
|
const timeToMs = (timeStr) => {
|
|
1346
1391
|
if (!timeStr)
|
|
1347
1392
|
return 0;
|
|
@@ -1361,6 +1406,52 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1361
1406
|
}
|
|
1362
1407
|
return Math.round(seconds * 1000);
|
|
1363
1408
|
};
|
|
1409
|
+
const transliterationNodes = doc.getElementsByTagName('transliteration');
|
|
1410
|
+
for (let i = 0; i < transliterationNodes.length; i += 1) {
|
|
1411
|
+
const texts = transliterationNodes[i].getElementsByTagName('text');
|
|
1412
|
+
for (let j = 0; j < texts.length; j += 1) {
|
|
1413
|
+
const textNode = texts[j];
|
|
1414
|
+
const key = textNode.getAttribute('for');
|
|
1415
|
+
if (!key) {
|
|
1416
|
+
// eslint-disable-next-line no-continue
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
const spans = Array.from(textNode.getElementsByTagName('span')).filter(span => span.getAttribute('begin'));
|
|
1420
|
+
if (spans.length > 0) {
|
|
1421
|
+
const syllabus = [];
|
|
1422
|
+
let fullText = '';
|
|
1423
|
+
for (let k = 0; k < spans.length; k += 1) {
|
|
1424
|
+
const span = spans[k];
|
|
1425
|
+
const begin = span.getAttribute('begin');
|
|
1426
|
+
const end = span.getAttribute('end');
|
|
1427
|
+
let spanText = span.textContent || '';
|
|
1428
|
+
const nextNode = span.nextSibling;
|
|
1429
|
+
if (nextNode &&
|
|
1430
|
+
nextNode.nodeType === 3 &&
|
|
1431
|
+
/^\s/.test(nextNode.textContent || '') &&
|
|
1432
|
+
!spanText.endsWith(' ')) {
|
|
1433
|
+
spanText += ' ';
|
|
1434
|
+
}
|
|
1435
|
+
if (spanText.trim() === '') {
|
|
1436
|
+
// eslint-disable-next-line no-continue
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
syllabus.push({
|
|
1440
|
+
time: timeToMs(begin),
|
|
1441
|
+
duration: timeToMs(end) - timeToMs(begin),
|
|
1442
|
+
text: spanText,
|
|
1443
|
+
});
|
|
1444
|
+
fullText += spanText;
|
|
1445
|
+
}
|
|
1446
|
+
transliterations[key] = { text: fullText.trim(), syllabus };
|
|
1447
|
+
}
|
|
1448
|
+
else if (textNode.textContent) {
|
|
1449
|
+
transliterations[key] = {
|
|
1450
|
+
text: textNode.textContent.trim().replace(/\s+/g, ' '),
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1364
1455
|
const lines = [];
|
|
1365
1456
|
const pNodes = doc.getElementsByTagName('p');
|
|
1366
1457
|
const lineSingers = [];
|
|
@@ -1438,6 +1529,64 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1438
1529
|
});
|
|
1439
1530
|
}
|
|
1440
1531
|
const alignment = alignments[i];
|
|
1532
|
+
// Distribute line-level transliteration to individual syllables
|
|
1533
|
+
// so that per-syllable animated romanisation works (like KPoe lyrics)
|
|
1534
|
+
const lineTransliterationItem = key ? transliterations[key] : undefined;
|
|
1535
|
+
if (lineTransliterationItem &&
|
|
1536
|
+
mainSyllables.length > 1 &&
|
|
1537
|
+
spans.length > 0) {
|
|
1538
|
+
if (lineTransliterationItem.syllabus &&
|
|
1539
|
+
lineTransliterationItem.syllabus.length === mainSyllables.length) {
|
|
1540
|
+
mainSyllables.forEach((syl, mapIdx) => {
|
|
1541
|
+
// eslint-disable-next-line no-param-reassign
|
|
1542
|
+
syl.romanizedText = lineTransliterationItem.syllabus[mapIdx].text;
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
else {
|
|
1546
|
+
const lineTransliteration = lineTransliterationItem.text;
|
|
1547
|
+
const romanWords = lineTransliteration.split(/\s+/).filter(Boolean);
|
|
1548
|
+
const syllableGroups = [];
|
|
1549
|
+
for (let si = 0; si < mainSyllables.length; si += 1) {
|
|
1550
|
+
if (mainSyllables[si].part && syllableGroups.length > 0) {
|
|
1551
|
+
syllableGroups[syllableGroups.length - 1].push(si);
|
|
1552
|
+
}
|
|
1553
|
+
else {
|
|
1554
|
+
syllableGroups.push([si]);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
const isCJK = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(mainSyllables.map(s => s.text).join(''));
|
|
1558
|
+
if (romanWords.length === syllableGroups.length) {
|
|
1559
|
+
syllableGroups.forEach((group, gi) => {
|
|
1560
|
+
// eslint-disable-next-line no-param-reassign
|
|
1561
|
+
mainSyllables[group[0]].romanizedText = romanWords[gi];
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
else if (romanWords.length === mainSyllables.length) {
|
|
1565
|
+
mainSyllables.forEach((syl, mapIdx) => {
|
|
1566
|
+
// eslint-disable-next-line no-param-reassign
|
|
1567
|
+
syl.romanizedText = romanWords[mapIdx];
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
else if (isCJK) {
|
|
1571
|
+
let romanIdx = 0;
|
|
1572
|
+
for (const group of syllableGroups) {
|
|
1573
|
+
const syl = mainSyllables[group[0]];
|
|
1574
|
+
const sylText = group
|
|
1575
|
+
.map(gIndex => mainSyllables[gIndex].text)
|
|
1576
|
+
.join('');
|
|
1577
|
+
const validChars = sylText.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7afA-Za-z0-9]/g) || [];
|
|
1578
|
+
const needed = validChars.length;
|
|
1579
|
+
if (needed > 0 && romanIdx < romanWords.length) {
|
|
1580
|
+
// eslint-disable-next-line no-param-reassign
|
|
1581
|
+
syl.romanizedText = romanWords
|
|
1582
|
+
.slice(romanIdx, romanIdx + needed)
|
|
1583
|
+
.join(' ');
|
|
1584
|
+
romanIdx += needed;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1441
1590
|
lines.push({
|
|
1442
1591
|
text: mainSyllables,
|
|
1443
1592
|
background: bgSyllables.length > 0,
|
|
@@ -1448,7 +1597,7 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1448
1597
|
alignment,
|
|
1449
1598
|
songPart,
|
|
1450
1599
|
translation: key ? translations[key] : undefined,
|
|
1451
|
-
romanizedText:
|
|
1600
|
+
romanizedText: lineTransliterationItem?.text,
|
|
1452
1601
|
oppositeTurn: alignment === 'end',
|
|
1453
1602
|
});
|
|
1454
1603
|
}
|
|
@@ -1593,8 +1742,8 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1593
1742
|
// Use wheel/touchmove which are guaranteed to be user initiated,
|
|
1594
1743
|
// unlike 'scroll' which fires for both user and programmatic/inertia
|
|
1595
1744
|
if (this.lyricsContainer) {
|
|
1596
|
-
this.lyricsContainer.addEventListener('wheel', this.
|
|
1597
|
-
this.lyricsContainer.addEventListener('touchmove', this.
|
|
1745
|
+
this.lyricsContainer.addEventListener('wheel', this._boundHandleUserScroll, { passive: true });
|
|
1746
|
+
this.lyricsContainer.addEventListener('touchmove', this._boundHandleUserScroll, { passive: true });
|
|
1598
1747
|
}
|
|
1599
1748
|
}
|
|
1600
1749
|
/**
|
|
@@ -1605,12 +1754,12 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1605
1754
|
*/
|
|
1606
1755
|
_onTimeChanged(oldTime, newTime) {
|
|
1607
1756
|
const timeDiff = Math.abs(newTime - oldTime);
|
|
1757
|
+
const isSeek = timeDiff > SEEK_THRESHOLD_MS;
|
|
1608
1758
|
const newActiveLines = this.findActiveLineIndices(newTime);
|
|
1609
1759
|
const oldActiveLines = this.activeLineIndices;
|
|
1610
1760
|
// Reset animation if active lines change or if we skip time.
|
|
1611
|
-
// A threshold of 0.5s (500ms) is used to detect a "skip".
|
|
1612
1761
|
const linesChanged = !AmLyrics.arraysEqual(newActiveLines, oldActiveLines);
|
|
1613
|
-
if (linesChanged ||
|
|
1762
|
+
if (linesChanged || isSeek) {
|
|
1614
1763
|
// Imperatively manage 'active' class so that scroll-animate and other
|
|
1615
1764
|
// imperative classes are never clobbered.
|
|
1616
1765
|
if (this.lyricsContainer) {
|
|
@@ -1634,20 +1783,13 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1634
1783
|
}
|
|
1635
1784
|
}
|
|
1636
1785
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
// Update position classes BEFORE scrolling so currentPrimaryActiveLine is current
|
|
1640
|
-
if (this.lyricsContainer && this.activeLineIndices.length > 0) {
|
|
1641
|
-
const primaryLineIndex = this.activeLineIndices[0];
|
|
1642
|
-
const primaryLine = this.lyricsContainer.querySelector(`#lyrics-line-${primaryLineIndex}`);
|
|
1643
|
-
if (primaryLine && primaryLine !== this.currentPrimaryActiveLine) {
|
|
1644
|
-
this.lastPrimaryActiveLine = this.currentPrimaryActiveLine;
|
|
1645
|
-
this.currentPrimaryActiveLine = primaryLine;
|
|
1646
|
-
this.updatePositionClasses(primaryLine);
|
|
1786
|
+
if (newActiveLines.length > 0) {
|
|
1787
|
+
this.clearPreActiveClasses();
|
|
1647
1788
|
}
|
|
1648
1789
|
}
|
|
1790
|
+
this.startAnimationFromTime(newTime);
|
|
1649
1791
|
// Trigger scroll imperatively (was previously in updated() via @state)
|
|
1650
|
-
this._handleActiveLineScroll(oldActiveLines);
|
|
1792
|
+
this._handleActiveLineScroll(oldActiveLines, isSeek);
|
|
1651
1793
|
}
|
|
1652
1794
|
// YouLyPlus-style syllable animation updates
|
|
1653
1795
|
if (this.lyricsContainer) {
|
|
@@ -1672,7 +1814,7 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1672
1814
|
const isActive = gap.classList.contains('active');
|
|
1673
1815
|
const isExiting = gap.classList.contains('gap-exiting');
|
|
1674
1816
|
// Start exit animation early so it completes before the next lyric
|
|
1675
|
-
const exitLeadMs =
|
|
1817
|
+
const exitLeadMs = GAP_EXIT_LEAD_MS;
|
|
1676
1818
|
const shouldStartExiting = isActive && !isExiting && newTime >= gapEndTime - exitLeadMs;
|
|
1677
1819
|
if (shouldBeActive && !isActive && !isExiting) {
|
|
1678
1820
|
// Entering gap: remove any leftover exit state, add active
|
|
@@ -1697,13 +1839,6 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1697
1839
|
AmLyrics.updateSyllableAnimation(dot);
|
|
1698
1840
|
}
|
|
1699
1841
|
});
|
|
1700
|
-
// Scroll to the gap element so dots animate in with
|
|
1701
|
-
// the staggered scroll rather than popping in.
|
|
1702
|
-
if (this.autoScroll &&
|
|
1703
|
-
!this.isUserScrolling &&
|
|
1704
|
-
!this.isClickSeeking) {
|
|
1705
|
-
this.scrollToActiveLineYouLy(gap);
|
|
1706
|
-
}
|
|
1707
1842
|
}
|
|
1708
1843
|
else if (shouldStartExiting) {
|
|
1709
1844
|
// Exiting gap: keep visible while dots animate out
|
|
@@ -1712,7 +1847,7 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1712
1847
|
// After exit animation completes, remove gap-exiting to collapse
|
|
1713
1848
|
setTimeout(() => {
|
|
1714
1849
|
gap.classList.remove('gap-exiting');
|
|
1715
|
-
},
|
|
1850
|
+
}, GAP_EXIT_LEAD_MS);
|
|
1716
1851
|
}
|
|
1717
1852
|
else if (isActive && !shouldBeActive) {
|
|
1718
1853
|
// NEW: Immediate cleanup if we seeked out of valid range
|
|
@@ -1732,48 +1867,32 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1732
1867
|
else if (this.lastInstrumentalIndex !== null) {
|
|
1733
1868
|
this.lastInstrumentalIndex = null;
|
|
1734
1869
|
}
|
|
1735
|
-
// Update position classes for YouLyPlus blur/opacity effect
|
|
1736
|
-
// (only needed when lines didn't change — when they DID change,
|
|
1737
|
-
// position classes are already updated above before scrolling)
|
|
1738
|
-
if (!linesChanged && this.activeLineIndices.length > 0) {
|
|
1739
|
-
const primaryLineIndex = this.activeLineIndices[0];
|
|
1740
|
-
const primaryLine = this.lyricsContainer.querySelector(`#lyrics-line-${primaryLineIndex}`);
|
|
1741
|
-
if (primaryLine && primaryLine !== this.currentPrimaryActiveLine) {
|
|
1742
|
-
this.lastPrimaryActiveLine = this.currentPrimaryActiveLine;
|
|
1743
|
-
this.currentPrimaryActiveLine = primaryLine;
|
|
1744
|
-
this.updatePositionClasses(primaryLine);
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
1870
|
// Pre-scroll: scroll to upcoming line ~0.5s before it starts
|
|
1748
1871
|
if (this.autoScroll &&
|
|
1749
1872
|
!this.isUserScrolling &&
|
|
1750
1873
|
!this.isClickSeeking &&
|
|
1751
1874
|
this.lyrics) {
|
|
1752
|
-
const preScrollLeadMs = 500; // 500ms lead time
|
|
1753
1875
|
// Condition: ONLY pre-scroll if no other lyric is currently playing.
|
|
1754
1876
|
// If a lyric is playing, we must wait for it to finish (handled by updated()).
|
|
1755
1877
|
if (this.activeLineIndices.length === 0) {
|
|
1878
|
+
let preActiveLineIndex = null;
|
|
1756
1879
|
for (let i = 0; i < this.lyrics.length; i += 1) {
|
|
1757
1880
|
const line = this.lyrics[i];
|
|
1758
1881
|
const timeUntilStart = line.timestamp - newTime;
|
|
1759
1882
|
const nextLineEl = this.lyricsContainer.querySelector(`#lyrics-line-${i}`);
|
|
1760
|
-
if (timeUntilStart > 0 && timeUntilStart <=
|
|
1883
|
+
if (timeUntilStart > 0 && timeUntilStart <= PRE_SCROLL_LEAD_MS) {
|
|
1761
1884
|
// Time to pre-scroll and pre-activate!
|
|
1762
1885
|
if (nextLineEl) {
|
|
1763
1886
|
// Apply unblur & zoom effect ahead of lyric start
|
|
1887
|
+
preActiveLineIndex = i;
|
|
1764
1888
|
nextLineEl.classList.add('pre-active');
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
this.scrollToActiveLineYouLy(nextLineEl);
|
|
1768
|
-
}
|
|
1889
|
+
this.clearPreActiveClasses(i);
|
|
1890
|
+
this.focusLine(nextLineEl);
|
|
1769
1891
|
}
|
|
1770
1892
|
break;
|
|
1771
1893
|
}
|
|
1772
|
-
else if (nextLineEl) {
|
|
1773
|
-
// Ensure lines outside the pre-scroll window don't stay pre-active
|
|
1774
|
-
nextLineEl.classList.remove('pre-active');
|
|
1775
|
-
}
|
|
1776
1894
|
}
|
|
1895
|
+
this.clearPreActiveClasses(preActiveLineIndex);
|
|
1777
1896
|
}
|
|
1778
1897
|
}
|
|
1779
1898
|
}
|
|
@@ -1837,32 +1956,16 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1837
1956
|
* Handle scrolling when active line indices change.
|
|
1838
1957
|
* Called imperatively from _onTimeChanged instead of from updated().
|
|
1839
1958
|
*/
|
|
1840
|
-
_handleActiveLineScroll(
|
|
1841
|
-
if (!this.
|
|
1842
|
-
this.isUserScrolling ||
|
|
1843
|
-
this.isClickSeeking ||
|
|
1844
|
-
this.activeLineIndices.length === 0) {
|
|
1959
|
+
_handleActiveLineScroll(_oldActiveIndices, forceScroll = false) {
|
|
1960
|
+
if (this.activeLineIndices.length === 0 || !this.lyricsContainer) {
|
|
1845
1961
|
return;
|
|
1846
1962
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
if (newlyAdded.length === 0) {
|
|
1850
|
-
// Only lost lines (an overlap resolved) — don't scroll
|
|
1963
|
+
const targetLineIndex = this.getPrimaryActiveLineIndex(this.activeLineIndices);
|
|
1964
|
+
if (targetLineIndex === null)
|
|
1851
1965
|
return;
|
|
1852
|
-
}
|
|
1853
|
-
// New lines were added — scroll to the latest newly-added line.
|
|
1854
|
-
// Previous overlap logic skipped every other line for songs with tiny
|
|
1855
|
-
// timing overlaps between consecutive lines, causing a visible glitch.
|
|
1856
|
-
const latestNewIndex = newlyAdded[newlyAdded.length - 1];
|
|
1857
|
-
const targetLine = this.lyricsContainer?.querySelector(`#lyrics-line-${latestNewIndex}`);
|
|
1966
|
+
const targetLine = this.lyricsContainer.querySelector(`#lyrics-line-${targetLineIndex}`);
|
|
1858
1967
|
if (targetLine) {
|
|
1859
|
-
this.
|
|
1860
|
-
}
|
|
1861
|
-
else if (this.currentPrimaryActiveLine) {
|
|
1862
|
-
this.scrollToActiveLineYouLy(this.currentPrimaryActiveLine);
|
|
1863
|
-
}
|
|
1864
|
-
else {
|
|
1865
|
-
this.scrollToActiveLine();
|
|
1968
|
+
this.focusLine(targetLine, forceScroll);
|
|
1866
1969
|
}
|
|
1867
1970
|
}
|
|
1868
1971
|
_getTextWidth(text, font) {
|
|
@@ -1936,6 +2039,62 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
1936
2039
|
static arraysEqual(a, b) {
|
|
1937
2040
|
return a.length === b.length && a.every((val, i) => val === b[i]);
|
|
1938
2041
|
}
|
|
2042
|
+
static getLineIndexFromElement(lineElement) {
|
|
2043
|
+
if (!lineElement)
|
|
2044
|
+
return null;
|
|
2045
|
+
const match = lineElement.id.match(/^lyrics-line-(\d+)$/);
|
|
2046
|
+
return match ? parseInt(match[1], 10) : null;
|
|
2047
|
+
}
|
|
2048
|
+
static getGapLoopDelay(gapDuration) {
|
|
2049
|
+
const desiredPhase = GAP_PULSE_DURATION_MS;
|
|
2050
|
+
const targetTime = gapDuration - GAP_EXIT_LEAD_MS;
|
|
2051
|
+
const normalizedTarget = ((targetTime % GAP_PULSE_CYCLE_MS) + GAP_PULSE_CYCLE_MS) %
|
|
2052
|
+
GAP_PULSE_CYCLE_MS;
|
|
2053
|
+
return ((((desiredPhase - normalizedTarget) % GAP_PULSE_CYCLE_MS) +
|
|
2054
|
+
GAP_PULSE_CYCLE_MS) %
|
|
2055
|
+
GAP_PULSE_CYCLE_MS);
|
|
2056
|
+
}
|
|
2057
|
+
clearPreActiveClasses(exceptLineIndex = null) {
|
|
2058
|
+
if (!this.lyricsContainer)
|
|
2059
|
+
return;
|
|
2060
|
+
this.lyricsContainer
|
|
2061
|
+
.querySelectorAll('.lyrics-line.pre-active')
|
|
2062
|
+
.forEach(element => {
|
|
2063
|
+
const lineElement = element;
|
|
2064
|
+
const lineIndex = AmLyrics.getLineIndexFromElement(lineElement);
|
|
2065
|
+
if (lineIndex !== exceptLineIndex) {
|
|
2066
|
+
lineElement.classList.remove('pre-active');
|
|
2067
|
+
}
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
getPrimaryActiveLineIndex(activeIndices) {
|
|
2071
|
+
if (activeIndices.length === 0)
|
|
2072
|
+
return null;
|
|
2073
|
+
const groupStart = activeIndices[0];
|
|
2074
|
+
const groupEnd = activeIndices[activeIndices.length - 1];
|
|
2075
|
+
let candidateIndex = Math.max(groupStart, groupEnd - 2);
|
|
2076
|
+
const currentPrimaryIndex = AmLyrics.getLineIndexFromElement(this.currentPrimaryActiveLine);
|
|
2077
|
+
if (currentPrimaryIndex !== null &&
|
|
2078
|
+
activeIndices.includes(currentPrimaryIndex) &&
|
|
2079
|
+
candidateIndex < currentPrimaryIndex) {
|
|
2080
|
+
candidateIndex = currentPrimaryIndex;
|
|
2081
|
+
}
|
|
2082
|
+
return candidateIndex;
|
|
2083
|
+
}
|
|
2084
|
+
focusLine(lineElement, forceScroll = false) {
|
|
2085
|
+
const primaryChanged = lineElement !== this.currentPrimaryActiveLine;
|
|
2086
|
+
if (primaryChanged) {
|
|
2087
|
+
this.lastPrimaryActiveLine = this.currentPrimaryActiveLine;
|
|
2088
|
+
this.currentPrimaryActiveLine = lineElement;
|
|
2089
|
+
}
|
|
2090
|
+
this.updatePositionClasses(lineElement);
|
|
2091
|
+
if ((forceScroll || primaryChanged) &&
|
|
2092
|
+
this.autoScroll &&
|
|
2093
|
+
!this.isUserScrolling &&
|
|
2094
|
+
!this.isClickSeeking) {
|
|
2095
|
+
this.scrollToActiveLineYouLy(lineElement, forceScroll);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
1939
2098
|
handleUserScroll() {
|
|
1940
2099
|
// Ignore programmatic scrolls and click-seek scrolls
|
|
1941
2100
|
if (this.isProgrammaticScroll || this.isClickSeeking) {
|
|
@@ -2205,7 +2364,8 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
2205
2364
|
this.activeLineIds.clear();
|
|
2206
2365
|
this.animatingLines = [];
|
|
2207
2366
|
// Find the clicked line element and scroll to it with forceScroll (like YouLyPlus)
|
|
2208
|
-
|
|
2367
|
+
// Timestamps are already in milliseconds — match the data-start-time attribute directly
|
|
2368
|
+
const clickedLineElement = this.lyricsContainer?.querySelector(`.lyrics-line[data-start-time="${line.text[0]?.timestamp || 0}"]`);
|
|
2209
2369
|
if (clickedLineElement && this.lyricsContainer) {
|
|
2210
2370
|
// Update active line reference to the clicked line
|
|
2211
2371
|
this.currentPrimaryActiveLine = clickedLineElement;
|
|
@@ -2331,11 +2491,10 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
2331
2491
|
}
|
|
2332
2492
|
const { animatingLines } = this;
|
|
2333
2493
|
const targetTop = Math.max(0, -newTranslateY);
|
|
2334
|
-
|
|
2335
|
-
// The || operator treats 0 as falsy, which caused bounce when scrollTop was 0
|
|
2494
|
+
const appliedTranslateY = -targetTop;
|
|
2336
2495
|
const prevOffset = -parent.scrollTop;
|
|
2337
|
-
const delta = prevOffset -
|
|
2338
|
-
this.currentScrollOffset =
|
|
2496
|
+
const delta = prevOffset - appliedTranslateY;
|
|
2497
|
+
this.currentScrollOffset = appliedTranslateY;
|
|
2339
2498
|
// Skip animation if already at the target position (e.g., first lines at top)
|
|
2340
2499
|
if (Math.abs(parent.scrollTop - targetTop) < 1 && Math.abs(delta) < 1) {
|
|
2341
2500
|
animState.isAnimating = false;
|
|
@@ -2348,6 +2507,7 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
2348
2507
|
line.classList.remove('scroll-animate');
|
|
2349
2508
|
line.style.removeProperty('--scroll-delta');
|
|
2350
2509
|
line.style.removeProperty('--lyrics-line-delay');
|
|
2510
|
+
line.style.removeProperty('--scroll-duration');
|
|
2351
2511
|
}
|
|
2352
2512
|
animatingLines.length = 0;
|
|
2353
2513
|
parent.scrollTo({ top: targetTop, behavior: 'smooth' });
|
|
@@ -2371,7 +2531,7 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
2371
2531
|
const referenceIndex = lineArray.indexOf(referenceLine);
|
|
2372
2532
|
if (referenceIndex === -1)
|
|
2373
2533
|
return;
|
|
2374
|
-
const delayIncrement =
|
|
2534
|
+
const delayIncrement = SCROLL_DELAY_INCREMENT_MS;
|
|
2375
2535
|
const lookBehind = 10;
|
|
2376
2536
|
const lookAhead = 15;
|
|
2377
2537
|
const len = lineArray.length;
|
|
@@ -2388,8 +2548,9 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
2388
2548
|
const delay = i >= referenceIndex ? (delayCounter - 1) * delayIncrement : 0;
|
|
2389
2549
|
line.style.setProperty('--scroll-delta', `${delta}px`);
|
|
2390
2550
|
line.style.setProperty('--lyrics-line-delay', `${delay}ms`);
|
|
2551
|
+
line.style.setProperty('--scroll-duration', `${SCROLL_ANIMATION_DURATION_MS}ms`);
|
|
2391
2552
|
newAnimatingLines.push(line);
|
|
2392
|
-
const lineDuration =
|
|
2553
|
+
const lineDuration = SCROLL_ANIMATION_DURATION_MS + delay;
|
|
2393
2554
|
if (lineDuration > maxAnimationDuration) {
|
|
2394
2555
|
maxAnimationDuration = lineDuration;
|
|
2395
2556
|
}
|
|
@@ -2404,7 +2565,7 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
2404
2565
|
animatingLines.push(line);
|
|
2405
2566
|
}
|
|
2406
2567
|
animState.isAnimating = true;
|
|
2407
|
-
const BASE_DURATION =
|
|
2568
|
+
const BASE_DURATION = SCROLL_ANIMATION_DURATION_MS;
|
|
2408
2569
|
this.scrollUnlockTimeout = setTimeout(() => {
|
|
2409
2570
|
animState.isAnimating = false;
|
|
2410
2571
|
if (animState.pendingUpdate !== null) {
|
|
@@ -2419,6 +2580,7 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
2419
2580
|
line.classList.remove('scroll-animate');
|
|
2420
2581
|
line.style.removeProperty('--scroll-delta');
|
|
2421
2582
|
line.style.removeProperty('--lyrics-line-delay');
|
|
2583
|
+
line.style.removeProperty('--scroll-duration');
|
|
2422
2584
|
}
|
|
2423
2585
|
animatingLines.length = 0;
|
|
2424
2586
|
this.scrollAnimationTimeout = undefined;
|
|
@@ -2500,7 +2662,7 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
2500
2662
|
}
|
|
2501
2663
|
setTimeout(() => {
|
|
2502
2664
|
this.isProgrammaticScroll = false;
|
|
2503
|
-
},
|
|
2665
|
+
}, SCROLL_ANIMATION_DURATION_MS + 160);
|
|
2504
2666
|
this.animateScrollYouLy(targetTranslateY, forceScroll);
|
|
2505
2667
|
}
|
|
2506
2668
|
/**
|
|
@@ -2529,26 +2691,46 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
2529
2691
|
// Use a Map to collect animations like YouLyPlus
|
|
2530
2692
|
const charAnimationsMap = new Map();
|
|
2531
2693
|
const styleUpdates = [];
|
|
2532
|
-
// Step 1
|
|
2694
|
+
// Step 1 & 2: Apply animations
|
|
2533
2695
|
if (isGrowable && isFirstSyllable && allWordCharSpans.length > 0) {
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
const
|
|
2537
|
-
allWordCharSpans.forEach(span => {
|
|
2696
|
+
// Glow AND wipe applied to ALL characters simultaneously from the first syllable
|
|
2697
|
+
// This prevents CSS animation restarts because the `animation` property is set once.
|
|
2698
|
+
const firstSyllableStartTime = parseFloat(syllable.getAttribute('data-start-time') || '0');
|
|
2699
|
+
allWordCharSpans.forEach((span, charIndexInWord) => {
|
|
2538
2700
|
const horizontalOffset = parseFloat(span.dataset.horizontalOffset || '0');
|
|
2539
|
-
// Use syllableCharIndex like YouLyPlus, not loop index
|
|
2540
|
-
const charIndex = parseFloat(span.dataset.syllableCharIndex || '0');
|
|
2541
|
-
const growDelay = baseDelayPerChar * charIndex;
|
|
2542
|
-
// READ DATA ATTRIBUTES for style values
|
|
2543
2701
|
const maxScale = span.dataset.maxScale || '1.1';
|
|
2544
2702
|
const shadowIntensity = span.dataset.shadowIntensity || '0.6';
|
|
2545
2703
|
const translateYPeak = span.dataset.translateYPeak || '-2';
|
|
2546
|
-
|
|
2547
|
-
|
|
2704
|
+
const animationParts = [];
|
|
2705
|
+
const parentSyllable = span.closest('.lyrics-syllable');
|
|
2706
|
+
if (parentSyllable) {
|
|
2707
|
+
const parentDuration = parseFloat(parentSyllable.getAttribute('data-duration') || '0');
|
|
2708
|
+
const parentStartTime = parseFloat(parentSyllable.getAttribute('data-start-time') || '0');
|
|
2709
|
+
const startPct = parseFloat(span.dataset.wipeStart || '0');
|
|
2710
|
+
const durationPct = parseFloat(span.dataset.wipeDuration || '0');
|
|
2711
|
+
const relativeStartOffset = Math.max(0, parentStartTime - firstSyllableStartTime);
|
|
2712
|
+
const wipeDelay = relativeStartOffset + parentDuration * startPct;
|
|
2713
|
+
const wipeDuration = parentDuration * durationPct;
|
|
2714
|
+
const useStartAnimation = isFirstInContainer && charIndexInWord === 0;
|
|
2715
|
+
let charWipeAnimation = 'wipe';
|
|
2716
|
+
if (useStartAnimation)
|
|
2717
|
+
charWipeAnimation = isRTL ? 'start-wipe-rtl' : 'start-wipe';
|
|
2718
|
+
else
|
|
2719
|
+
charWipeAnimation = isRTL ? 'wipe-rtl' : 'wipe';
|
|
2720
|
+
// Blend word and syllable durations to let the gradient flow smoothly
|
|
2721
|
+
// while still responding to syllable pacing (no strict exactness, just natural flow)
|
|
2722
|
+
const growDelay = wipeDelay;
|
|
2723
|
+
const growDurationMs = Math.max(600, wordDurationMs * 0.8 + parentDuration * 1.5);
|
|
2724
|
+
animationParts.push(`grow-dynamic ${growDurationMs}ms ease-in-out ${growDelay}ms forwards`);
|
|
2725
|
+
if (wipeDuration > 0) {
|
|
2726
|
+
animationParts.push(`${charWipeAnimation} ${wipeDuration}ms linear ${wipeDelay}ms forwards`);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
charAnimationsMap.set(span, animationParts.join(', '));
|
|
2548
2730
|
styleUpdates.push({
|
|
2549
2731
|
element: span,
|
|
2550
2732
|
property: '--char-offset-x',
|
|
2551
|
-
value: `${horizontalOffset}`,
|
|
2733
|
+
value: `${horizontalOffset}`,
|
|
2552
2734
|
});
|
|
2553
2735
|
styleUpdates.push({
|
|
2554
2736
|
element: span,
|
|
@@ -2563,58 +2745,64 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
2563
2745
|
styleUpdates.push({
|
|
2564
2746
|
element: span,
|
|
2565
2747
|
property: '--translate-y-peak',
|
|
2566
|
-
value: `${translateYPeak}`,
|
|
2748
|
+
value: `${translateYPeak}`,
|
|
2567
2749
|
});
|
|
2568
2750
|
});
|
|
2569
2751
|
}
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
//
|
|
2752
|
+
else if (isGrowable && !isFirstSyllable && charSpans.length > 0) {
|
|
2753
|
+
// For subsequent syllables of a growable word:
|
|
2754
|
+
// If they already have `grow-dynamic`, it means the first syllable correctly took care of BOTH grow and wipe!
|
|
2755
|
+
// Otherwise, they scrubbed directly into this syllable, so let's at least do the wipe.
|
|
2756
|
+
charSpans.forEach(span => {
|
|
2757
|
+
const existingAnimation = charAnimationsMap.get(span) || span.style.animation || '';
|
|
2758
|
+
if (existingAnimation.includes('grow-dynamic'))
|
|
2759
|
+
return;
|
|
2760
|
+
const startPct = parseFloat(span.dataset.wipeStart || '0');
|
|
2761
|
+
const durationPct = parseFloat(span.dataset.wipeDuration || '0');
|
|
2762
|
+
const wipeDelay = syllableDurationMs * startPct;
|
|
2763
|
+
const wipeDuration = syllableDurationMs * durationPct;
|
|
2764
|
+
const charWipeAnimation = isRTL ? 'wipe-rtl' : 'wipe';
|
|
2765
|
+
if (wipeDuration > 0) {
|
|
2766
|
+
charAnimationsMap.set(span, `${charWipeAnimation} ${wipeDuration}ms linear ${wipeDelay}ms forwards`);
|
|
2767
|
+
}
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
else if (charSpans.length > 0) {
|
|
2771
|
+
// Per-character wipe for non-growable words (matching YouLyPlus)
|
|
2573
2772
|
charSpans.forEach((span, charIndex) => {
|
|
2574
2773
|
const startPct = parseFloat(span.dataset.wipeStart || '0');
|
|
2575
2774
|
const durationPct = parseFloat(span.dataset.wipeDuration || '0');
|
|
2576
2775
|
const wipeDelay = syllableDurationMs * startPct;
|
|
2577
2776
|
const wipeDuration = syllableDurationMs * durationPct;
|
|
2578
2777
|
const useStartAnimation = isFirstInContainer && charIndex === 0;
|
|
2579
|
-
let charWipeAnimation;
|
|
2778
|
+
let charWipeAnimation = 'wipe';
|
|
2580
2779
|
if (useStartAnimation) {
|
|
2581
2780
|
charWipeAnimation = isRTL ? 'start-wipe-rtl' : 'start-wipe';
|
|
2582
2781
|
}
|
|
2583
2782
|
else {
|
|
2584
2783
|
charWipeAnimation = isRTL ? 'wipe-rtl' : 'wipe';
|
|
2585
2784
|
}
|
|
2586
|
-
// Get existing animation from map (grow-dynamic) and combine with wipe
|
|
2587
|
-
const existingAnimation = charAnimationsMap.get(span) || span.style.animation || '';
|
|
2588
|
-
const animationParts = [];
|
|
2589
|
-
if (existingAnimation && existingAnimation.includes('grow-dynamic')) {
|
|
2590
|
-
animationParts.push(existingAnimation.split(',')[0].trim());
|
|
2591
|
-
}
|
|
2592
2785
|
if (wipeDuration > 0) {
|
|
2593
|
-
|
|
2786
|
+
charAnimationsMap.set(span, `${charWipeAnimation} ${wipeDuration}ms linear ${wipeDelay}ms forwards`);
|
|
2594
2787
|
}
|
|
2595
|
-
charAnimationsMap.set(span, animationParts.join(', '));
|
|
2596
2788
|
});
|
|
2597
2789
|
}
|
|
2598
2790
|
else {
|
|
2599
|
-
// Syllable-level wipe for regular (non-growable) words
|
|
2791
|
+
// Syllable-level wipe for regular (non-growable) words without chars
|
|
2600
2792
|
const wipeRatio = parseFloat(syllable.getAttribute('data-wipe-ratio') || '1');
|
|
2601
2793
|
const visualDuration = syllableDurationMs * wipeRatio;
|
|
2602
|
-
let wipeAnimation;
|
|
2794
|
+
let wipeAnimation = 'wipe';
|
|
2603
2795
|
if (isFirstInContainer) {
|
|
2604
2796
|
wipeAnimation = isRTL ? 'start-wipe-rtl' : 'start-wipe';
|
|
2605
2797
|
}
|
|
2606
2798
|
else {
|
|
2607
2799
|
wipeAnimation = isRTL ? 'wipe-rtl' : 'wipe';
|
|
2608
2800
|
}
|
|
2609
|
-
if (syllable.classList.contains('line-synced'))
|
|
2610
|
-
// If line-synced, just add the class for CSS animation, or ensure valid state
|
|
2611
|
-
// The CSS rule .lyrics-syllable.line-synced handles the fade
|
|
2801
|
+
if (syllable.classList.contains('line-synced'))
|
|
2612
2802
|
return;
|
|
2613
|
-
}
|
|
2614
2803
|
const currentWipeAnimation = isGap ? 'fade-gap' : wipeAnimation;
|
|
2615
|
-
const syllableAnimation = `${currentWipeAnimation} ${visualDuration}ms ${isGap ? 'ease-out' : 'linear'} forwards`;
|
|
2616
2804
|
// eslint-disable-next-line no-param-reassign
|
|
2617
|
-
syllable.style.animation =
|
|
2805
|
+
syllable.style.animation = `${currentWipeAnimation} ${visualDuration}ms ${isGap ? 'ease-out' : 'linear'} forwards`;
|
|
2618
2806
|
}
|
|
2619
2807
|
// --- WRITE PHASE ---
|
|
2620
2808
|
classList.remove('pre-highlight');
|
|
@@ -2834,7 +3022,7 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
2834
3022
|
}
|
|
2835
3023
|
}
|
|
2836
3024
|
if (running) {
|
|
2837
|
-
this.animationFrameId = requestAnimationFrame(this.
|
|
3025
|
+
this.animationFrameId = requestAnimationFrame(this._boundAnimateProgress);
|
|
2838
3026
|
}
|
|
2839
3027
|
else if (this.animationFrameId) {
|
|
2840
3028
|
// Stop animation if no words are running
|
|
@@ -3069,6 +3257,7 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
3069
3257
|
// the result to each individual group so it renders through the
|
|
3070
3258
|
// single-syllable path (which supports char-level glow).
|
|
3071
3259
|
const groupGrowable = new Array(wordGroups.length).fill(false);
|
|
3260
|
+
const groupGlowing = new Array(wordGroups.length).fill(false);
|
|
3072
3261
|
// Visual word info for growable char-level glow:
|
|
3073
3262
|
// Each group stores the combined visual word's text, duration, and
|
|
3074
3263
|
// the char offset of this group within the visual word.
|
|
@@ -3101,18 +3290,26 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
3101
3290
|
const isCJK = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(combinedText);
|
|
3102
3291
|
const isRTL = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u0590-\u05FF]/.test(combinedText);
|
|
3103
3292
|
const hasHyphen = combinedText.includes('-');
|
|
3104
|
-
const
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3293
|
+
const wordLen = combinedText.length;
|
|
3294
|
+
let isGrowableVW = !isCJK && !isRTL && !hasHyphen && wordLen > 0 && wordLen <= 12;
|
|
3295
|
+
if (isGrowableVW) {
|
|
3296
|
+
if (wordLen < 3) {
|
|
3297
|
+
isGrowableVW =
|
|
3298
|
+
combinedDuration >= 1110 && combinedDuration >= wordLen * 550;
|
|
3299
|
+
}
|
|
3300
|
+
else {
|
|
3301
|
+
isGrowableVW =
|
|
3302
|
+
combinedDuration >= 800 && combinedDuration >= wordLen * 180;
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
// Glow requirement (more strict)
|
|
3306
|
+
const isGlowingVW = isGrowableVW &&
|
|
3307
|
+
combinedDuration >= 1200 &&
|
|
3308
|
+
combinedDuration >= combinedText.length * 300;
|
|
3113
3309
|
let charOff = 0;
|
|
3114
3310
|
for (let gi = vwStart; gi <= vwEnd; gi += 1) {
|
|
3115
3311
|
groupGrowable[gi] = isGrowableVW;
|
|
3312
|
+
groupGlowing[gi] = isGlowingVW;
|
|
3116
3313
|
vwFullText[gi] = combinedText;
|
|
3117
3314
|
vwFullDuration[gi] = combinedDuration;
|
|
3118
3315
|
vwCharOffset[gi] = charOff;
|
|
@@ -3128,224 +3325,126 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
3128
3325
|
const mainVocalElement = b `<p class="main-vocal-container">
|
|
3129
3326
|
${wordGroups.map((group, groupIdx) => {
|
|
3130
3327
|
const isGrowable = groupGrowable[groupIdx];
|
|
3131
|
-
|
|
3132
|
-
// skip continuation groups (rendered by the first group)
|
|
3133
|
-
if (isGrowable && vwCharOffset[groupIdx] > 0) {
|
|
3134
|
-
return '';
|
|
3135
|
-
}
|
|
3136
|
-
// Check if ANY syllable in group is line-synced
|
|
3328
|
+
const isGlowing = groupGlowing[groupIdx];
|
|
3137
3329
|
const groupLineSynced = group.some(s => s.lineSynced);
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
for (let gi = groupIdx; gi < wordGroups.length; gi += 1) {
|
|
3150
|
-
if (gi > groupIdx && vwCharOffset[gi] === 0)
|
|
3151
|
-
break;
|
|
3152
|
-
if (gi > groupIdx && !groupGrowable[gi])
|
|
3153
|
-
break;
|
|
3154
|
-
combinedRawText += wordGroups[gi].map(s => s.text).join('');
|
|
3155
|
-
}
|
|
3156
|
-
const syllableContent = b `${combinedRawText
|
|
3157
|
-
.split('')
|
|
3158
|
-
.map((char, charIndex) => {
|
|
3159
|
-
if (char === ' ') {
|
|
3160
|
-
return ' ';
|
|
3161
|
-
}
|
|
3162
|
-
const charStartPercent = charIndex / numChars;
|
|
3163
|
-
const minDuration = 1000;
|
|
3164
|
-
const maxDuration = 5000;
|
|
3165
|
-
const easingPower = 3;
|
|
3166
|
-
const progress = Math.min(1, Math.max(0, (wordDuration - minDuration) /
|
|
3167
|
-
(maxDuration - minDuration)));
|
|
3168
|
-
const easedProgress = progress ** easingPower;
|
|
3169
|
-
const isLongWord = numChars > 5;
|
|
3170
|
-
const isShortDuration = wordDuration < 1500;
|
|
3171
|
-
let maxDecayRate = 0;
|
|
3172
|
-
if (isLongWord || isShortDuration) {
|
|
3173
|
-
let decayStrength = 0;
|
|
3174
|
-
if (isLongWord)
|
|
3175
|
-
decayStrength += Math.min((numChars - 5) / 3, 1.0) * 0.4;
|
|
3176
|
-
if (isShortDuration)
|
|
3177
|
-
decayStrength +=
|
|
3178
|
-
Math.max(0, 1.0 - (wordDuration - 1000) / 500) * 0.4;
|
|
3179
|
-
maxDecayRate = Math.min(decayStrength, 0.85);
|
|
3180
|
-
}
|
|
3181
|
-
const positionInWord = numChars > 1 ? charIndex / (numChars - 1) : 0;
|
|
3182
|
-
const decayFactor = 1.0 - positionInWord * maxDecayRate;
|
|
3183
|
-
const charProgress = easedProgress * decayFactor;
|
|
3184
|
-
const baseGrowth = numChars <= 3 ? 0.07 : 0.05;
|
|
3185
|
-
const charMaxScale = 1.0 + baseGrowth + charProgress * 0.1;
|
|
3186
|
-
const charShadowIntensity = 0.4 + charProgress * 0.4;
|
|
3187
|
-
const normalizedGrowth = (charMaxScale - 1.0) / 0.13;
|
|
3188
|
-
const charTranslateYPeak = -normalizedGrowth * 6;
|
|
3189
|
-
const position = (charIndex + 0.5) / numChars;
|
|
3190
|
-
const horizontalOffset = (position - 0.5) * 2 * ((charMaxScale - 1.0) * 25);
|
|
3191
|
-
return b `<span
|
|
3192
|
-
class="char"
|
|
3193
|
-
data-char-index="${charIndex}"
|
|
3194
|
-
data-syllable-char-index="${charIndex}"
|
|
3195
|
-
data-wipe-start="${charStartPercent.toFixed(4)}"
|
|
3196
|
-
data-wipe-duration="${(1 / numChars).toFixed(4)}"
|
|
3197
|
-
data-horizontal-offset="${horizontalOffset.toFixed(2)}"
|
|
3198
|
-
data-max-scale="${charMaxScale.toFixed(3)}"
|
|
3199
|
-
data-shadow-intensity="${charShadowIntensity.toFixed(3)}"
|
|
3200
|
-
data-translate-y-peak="${charTranslateYPeak.toFixed(3)}"
|
|
3201
|
-
>${char}</span
|
|
3202
|
-
>`;
|
|
3203
|
-
})}`;
|
|
3204
|
-
return b `<span class="lyrics-word growable">
|
|
3205
|
-
<span class="lyrics-syllable-wrap">
|
|
3206
|
-
<span
|
|
3207
|
-
class="lyrics-syllable ${groupLineSynced
|
|
3208
|
-
? 'line-synced'
|
|
3209
|
-
: ''}"
|
|
3210
|
-
data-start-time="${startTimeMs}"
|
|
3211
|
-
data-end-time="${endTimeMs}"
|
|
3212
|
-
data-duration="${wordDuration}"
|
|
3213
|
-
data-syllable-index="0"
|
|
3214
|
-
data-wipe-ratio="1"
|
|
3215
|
-
>${syllableContent}</span
|
|
3216
|
-
>
|
|
3217
|
-
</span>
|
|
3218
|
-
</span>`;
|
|
3219
|
-
}
|
|
3220
|
-
// For single-syllable groups, use original logic
|
|
3221
|
-
if (group.length === 1) {
|
|
3222
|
-
const syllable = group[0];
|
|
3330
|
+
const wordText = isGrowable ? vwFullText[groupIdx] : '';
|
|
3331
|
+
const wordDuration = isGrowable ? vwFullDuration[groupIdx] : 0;
|
|
3332
|
+
const wordNumChars = wordText.length;
|
|
3333
|
+
const groupCharOffset = isGrowable ? vwCharOffset[groupIdx] : 0;
|
|
3334
|
+
let sylCharAccumulator = 0;
|
|
3335
|
+
return b `<span
|
|
3336
|
+
class="lyrics-word ${isGrowable ? 'growable' : ''} ${isGlowing
|
|
3337
|
+
? 'glowing'
|
|
3338
|
+
: ''} ${group.length > 1 ? 'allow-break' : ''}"
|
|
3339
|
+
>
|
|
3340
|
+
${group.map((syllable, sylIdx) => {
|
|
3223
3341
|
const startTimeMs = syllable.timestamp;
|
|
3224
3342
|
const endTimeMs = syllable.endtime;
|
|
3225
3343
|
const durationMs = endTimeMs - startTimeMs;
|
|
3226
3344
|
const text = syllable.text || '';
|
|
3227
|
-
const trimmedText = text.trim();
|
|
3228
|
-
// Optional romanization per syllable (hide if same as the original text)
|
|
3229
3345
|
const romanizedText = this.showRomanization &&
|
|
3230
3346
|
syllable.romanizedText &&
|
|
3231
3347
|
syllable.romanizedText.trim() !== syllable.text.trim()
|
|
3232
3348
|
? b `<span
|
|
3233
|
-
|
|
3349
|
+
class="lyrics-syllable transliteration ${groupLineSynced
|
|
3234
3350
|
? 'line-synced'
|
|
3235
3351
|
: ''}"
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3352
|
+
data-start-time="${startTimeMs}"
|
|
3353
|
+
data-end-time="${endTimeMs}"
|
|
3354
|
+
data-duration="${durationMs}"
|
|
3355
|
+
data-syllable-index="0"
|
|
3356
|
+
data-wipe-ratio="1"
|
|
3357
|
+
>${syllable.romanizedText}</span
|
|
3358
|
+
>`
|
|
3243
3359
|
: '';
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3360
|
+
let syllableContent = text;
|
|
3361
|
+
if (isGrowable) {
|
|
3362
|
+
let charIndexInsideSyllable = 0;
|
|
3363
|
+
const numCharsInSyllable = text.replace(/\s/g, '').length || 1;
|
|
3364
|
+
syllableContent = b `${text.split('').map(char => {
|
|
3365
|
+
if (char === ' ')
|
|
3248
3366
|
return ' ';
|
|
3249
|
-
|
|
3250
|
-
const
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
const
|
|
3367
|
+
const charIndexInsideWord = groupCharOffset + sylCharAccumulator;
|
|
3368
|
+
const charStartPercentVal = charIndexInsideSyllable / numCharsInSyllable;
|
|
3369
|
+
sylCharAccumulator += 1;
|
|
3370
|
+
charIndexInsideSyllable += 1;
|
|
3371
|
+
const minDuration = 400;
|
|
3372
|
+
const maxDuration = 3000;
|
|
3254
3373
|
const easingPower = 3;
|
|
3255
|
-
const progress = Math.min(1, Math.max(0, (
|
|
3374
|
+
const progress = Math.min(1, Math.max(0, (wordDuration - minDuration) /
|
|
3256
3375
|
(maxDuration - minDuration)));
|
|
3257
3376
|
const easedProgress = progress ** easingPower;
|
|
3258
|
-
const isLongWord =
|
|
3259
|
-
const isShortDuration =
|
|
3377
|
+
const isLongWord = wordNumChars > 5;
|
|
3378
|
+
const isShortDuration = wordDuration < 1200;
|
|
3260
3379
|
let maxDecayRate = 0;
|
|
3261
3380
|
if (isLongWord || isShortDuration) {
|
|
3262
3381
|
let decayStrength = 0;
|
|
3263
3382
|
if (isLongWord)
|
|
3264
3383
|
decayStrength +=
|
|
3265
|
-
Math.min((
|
|
3266
|
-
if (isShortDuration)
|
|
3384
|
+
Math.min((wordNumChars - 5) / 5, 1.0) * 0.4;
|
|
3385
|
+
if (isShortDuration && wordNumChars > 3)
|
|
3386
|
+
decayStrength +=
|
|
3387
|
+
Math.max(0, 1.0 - (wordDuration - 800) / 400) * 0.3;
|
|
3388
|
+
else if (isShortDuration && wordNumChars <= 3)
|
|
3267
3389
|
decayStrength +=
|
|
3268
|
-
Math.max(0, 1.0 - (
|
|
3269
|
-
maxDecayRate = Math.min(decayStrength, 0.
|
|
3390
|
+
Math.max(0, 1.0 - (wordDuration - 800) / 400) * 0.1;
|
|
3391
|
+
maxDecayRate = Math.min(decayStrength, 0.7);
|
|
3270
3392
|
}
|
|
3271
|
-
const positionInWord =
|
|
3393
|
+
const positionInWord = wordNumChars > 1
|
|
3394
|
+
? charIndexInsideWord / (wordNumChars - 1)
|
|
3395
|
+
: 0;
|
|
3272
3396
|
const decayFactor = 1.0 - positionInWord * maxDecayRate;
|
|
3273
3397
|
const charProgress = easedProgress * decayFactor;
|
|
3274
|
-
const baseGrowth =
|
|
3275
|
-
const charMaxScale = 1.0 + baseGrowth + charProgress * 0.
|
|
3276
|
-
const
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3398
|
+
const baseGrowth = wordNumChars <= 3 ? 0.05 : 0.04;
|
|
3399
|
+
const charMaxScale = 1.0 + baseGrowth + charProgress * 0.08;
|
|
3400
|
+
const glowDurFactor = Math.min(1.1, wordDuration / 1500);
|
|
3401
|
+
let glowLenFactor = 1.0;
|
|
3402
|
+
if (wordNumChars <= 3) {
|
|
3403
|
+
glowLenFactor = 0.85;
|
|
3404
|
+
}
|
|
3405
|
+
else if (wordNumChars >= 6) {
|
|
3406
|
+
glowLenFactor = 1.1;
|
|
3407
|
+
}
|
|
3408
|
+
const glowIntensityScale = glowDurFactor * glowLenFactor;
|
|
3409
|
+
const charShadowIntensity = isGlowing
|
|
3410
|
+
? (0.35 + charProgress * 0.45) * glowIntensityScale
|
|
3411
|
+
: 0;
|
|
3412
|
+
const normalizedGrowth = (charMaxScale - 1.0) / 0.1;
|
|
3413
|
+
const effectiveDuration = (wordDuration + durationMs * 2) / 3;
|
|
3414
|
+
const peakMultiplier = Math.min(1, Math.max(0.3, effectiveDuration / 2000));
|
|
3415
|
+
const charTranslateYPeak = -normalizedGrowth * (2 * peakMultiplier); // Further dampened lift peak
|
|
3416
|
+
const position = (charIndexInsideWord + 0.5) / wordNumChars;
|
|
3280
3417
|
const horizontalOffset = (position - 0.5) * 2 * ((charMaxScale - 1.0) * 25);
|
|
3281
3418
|
return b `<span
|
|
3282
3419
|
class="char"
|
|
3283
|
-
data-char-index="${
|
|
3284
|
-
data-syllable-char-index="${
|
|
3285
|
-
data-wipe-start="${
|
|
3286
|
-
data-wipe-duration="${(1 /
|
|
3420
|
+
data-char-index="${charIndexInsideWord}"
|
|
3421
|
+
data-syllable-char-index="${charIndexInsideWord}"
|
|
3422
|
+
data-wipe-start="${charStartPercentVal.toFixed(4)}"
|
|
3423
|
+
data-wipe-duration="${(1 / numCharsInSyllable).toFixed(4)}"
|
|
3287
3424
|
data-horizontal-offset="${horizontalOffset.toFixed(2)}"
|
|
3288
3425
|
data-max-scale="${charMaxScale.toFixed(3)}"
|
|
3289
3426
|
data-shadow-intensity="${charShadowIntensity.toFixed(3)}"
|
|
3290
3427
|
data-translate-y-peak="${charTranslateYPeak.toFixed(3)}"
|
|
3291
3428
|
>${char}</span
|
|
3292
3429
|
>`;
|
|
3293
|
-
})}
|
|
3294
|
-
|
|
3295
|
-
return b `<span
|
|
3296
|
-
class="lyrics-word ${isGrowable ? 'growable' : ''}"
|
|
3297
|
-
>
|
|
3298
|
-
<span class="lyrics-syllable-wrap">
|
|
3430
|
+
})}`;
|
|
3431
|
+
}
|
|
3432
|
+
return b `<span class="lyrics-syllable-wrap">
|
|
3299
3433
|
<span
|
|
3300
|
-
class="lyrics-syllable ${
|
|
3434
|
+
class="lyrics-syllable ${groupLineSynced
|
|
3301
3435
|
? 'line-synced'
|
|
3302
3436
|
: ''}"
|
|
3303
3437
|
data-start-time="${startTimeMs}"
|
|
3304
3438
|
data-end-time="${endTimeMs}"
|
|
3305
3439
|
data-duration="${durationMs}"
|
|
3306
|
-
data-
|
|
3440
|
+
data-word-duration="${wordDuration}"
|
|
3441
|
+
data-syllable-index="${sylIdx}"
|
|
3307
3442
|
data-wipe-ratio="1"
|
|
3308
3443
|
>${syllableContent}</span
|
|
3309
3444
|
>
|
|
3310
3445
|
${romanizedText}
|
|
3311
|
-
</span
|
|
3312
|
-
|
|
3313
|
-
}
|
|
3314
|
-
// Multi-syllable group (part=true): render all syllables inside one lyrics-word
|
|
3315
|
-
return b `<span
|
|
3316
|
-
class="lyrics-word ${isGrowable ? 'growable' : ''} allow-break"
|
|
3317
|
-
>
|
|
3318
|
-
${group.map((syllable, sylIdx) => b `
|
|
3319
|
-
<span class="lyrics-syllable-wrap">
|
|
3320
|
-
<span
|
|
3321
|
-
class="lyrics-syllable ${groupLineSynced
|
|
3322
|
-
? 'line-synced'
|
|
3323
|
-
: ''}"
|
|
3324
|
-
data-start-time="${syllable.timestamp}"
|
|
3325
|
-
data-end-time="${syllable.endtime}"
|
|
3326
|
-
data-duration="${syllable.endtime - syllable.timestamp}"
|
|
3327
|
-
data-syllable-index="${sylIdx}"
|
|
3328
|
-
data-wipe-ratio="1"
|
|
3329
|
-
>${syllable.text}</span
|
|
3330
|
-
>
|
|
3331
|
-
${this.showRomanization &&
|
|
3332
|
-
syllable.romanizedText &&
|
|
3333
|
-
syllable.romanizedText.trim() !== syllable.text.trim()
|
|
3334
|
-
? b `<span
|
|
3335
|
-
class="lyrics-syllable transliteration ${groupLineSynced
|
|
3336
|
-
? 'line-synced'
|
|
3337
|
-
: ''}"
|
|
3338
|
-
data-start-time="${syllable.timestamp}"
|
|
3339
|
-
data-end-time="${syllable.endtime}"
|
|
3340
|
-
data-duration="${syllable.endtime -
|
|
3341
|
-
syllable.timestamp}"
|
|
3342
|
-
data-syllable-index="0"
|
|
3343
|
-
data-wipe-ratio="1"
|
|
3344
|
-
>${syllable.romanizedText}</span
|
|
3345
|
-
>`
|
|
3346
|
-
: ''}
|
|
3347
|
-
</span>
|
|
3348
|
-
`)}
|
|
3446
|
+
</span>`;
|
|
3447
|
+
})}
|
|
3349
3448
|
</span>`;
|
|
3350
3449
|
})}
|
|
3351
3450
|
</p>`;
|
|
@@ -3376,14 +3475,17 @@ let AmLyrics$1 = class AmLyrics extends i {
|
|
|
3376
3475
|
let maybeInstrumentalBlock = null;
|
|
3377
3476
|
const gapForLine = gapByIndex.get(lineIndex);
|
|
3378
3477
|
if (gapForLine) {
|
|
3478
|
+
const gapDuration = gapForLine.gapEnd - gapForLine.gapStart;
|
|
3379
3479
|
// Calculate dot timing for fill-up animation (3 dots)
|
|
3380
|
-
const dotDuration =
|
|
3480
|
+
const dotDuration = gapDuration / 3;
|
|
3481
|
+
const gapLoopDelay = AmLyrics.getGapLoopDelay(gapDuration);
|
|
3381
3482
|
// Gap starts without 'active' — _onTimeChanged toggles it imperatively
|
|
3382
3483
|
maybeInstrumentalBlock = b `<div
|
|
3383
3484
|
id="gap-${lineIndex}"
|
|
3384
3485
|
class="lyrics-line lyrics-gap"
|
|
3385
3486
|
data-start-time="${gapForLine.gapStart}"
|
|
3386
3487
|
data-end-time="${gapForLine.gapEnd}"
|
|
3488
|
+
style="--gap-pulse-duration: ${GAP_PULSE_DURATION_MS}ms; --gap-loop-delay: -${gapLoopDelay}ms; --gap-exit-duration: ${GAP_EXIT_LEAD_MS}ms; --gap-exit-scale: ${GAP_MIN_SCALE};"
|
|
3387
3489
|
>
|
|
3388
3490
|
<div class="lyrics-line-container">
|
|
3389
3491
|
<p class="main-vocal-container">
|
|
@@ -3706,7 +3808,7 @@ AmLyrics$1.styles = i$3 `
|
|
|
3706
3808
|
.lyrics-line.scroll-animate {
|
|
3707
3809
|
transition: none !important; /* Prevent conflict with scroll animation */
|
|
3708
3810
|
animation-name: lyrics-scroll;
|
|
3709
|
-
animation-duration:
|
|
3811
|
+
animation-duration: var(--scroll-duration, 280ms);
|
|
3710
3812
|
animation-timing-function: cubic-bezier(0.41, 0, 0.12, 0.99);
|
|
3711
3813
|
animation-fill-mode: both;
|
|
3712
3814
|
animation-delay: var(--lyrics-line-delay, 0ms);
|
|
@@ -3778,19 +3880,20 @@ AmLyrics$1.styles = i$3 `
|
|
|
3778
3880
|
opacity: 0;
|
|
3779
3881
|
font-size: var(--lyplus-font-size-subtext);
|
|
3780
3882
|
transition:
|
|
3781
|
-
max-height 0.
|
|
3782
|
-
opacity
|
|
3783
|
-
padding 0.
|
|
3883
|
+
max-height 350ms cubic-bezier(0.33, 1, 0.68, 1),
|
|
3884
|
+
opacity 300ms ease-out,
|
|
3885
|
+
padding 350ms cubic-bezier(0.33, 1, 0.68, 1);
|
|
3784
3886
|
margin: 0;
|
|
3785
3887
|
}
|
|
3786
3888
|
|
|
3787
|
-
.lyrics-line.active .background-vocal-container
|
|
3889
|
+
.lyrics-line.active .background-vocal-container,
|
|
3890
|
+
.lyrics-line.pre-active .background-vocal-container {
|
|
3788
3891
|
max-height: 4em;
|
|
3789
3892
|
opacity: 1;
|
|
3790
3893
|
transition:
|
|
3791
|
-
max-height 0.
|
|
3792
|
-
opacity
|
|
3793
|
-
padding 0.
|
|
3894
|
+
max-height 350ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
3895
|
+
opacity 300ms ease-out,
|
|
3896
|
+
padding 350ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
3794
3897
|
will-change: max-height, opacity, padding;
|
|
3795
3898
|
}
|
|
3796
3899
|
|
|
@@ -3801,6 +3904,11 @@ AmLyrics$1.styles = i$3 `
|
|
|
3801
3904
|
will-change: transform, opacity;
|
|
3802
3905
|
}
|
|
3803
3906
|
|
|
3907
|
+
.lyrics-line.pre-active {
|
|
3908
|
+
opacity: 1;
|
|
3909
|
+
will-change: transform, opacity;
|
|
3910
|
+
}
|
|
3911
|
+
|
|
3804
3912
|
.lyrics-line.singer-right {
|
|
3805
3913
|
text-align: end;
|
|
3806
3914
|
}
|
|
@@ -3880,7 +3988,7 @@ AmLyrics$1.styles = i$3 `
|
|
|
3880
3988
|
/* Unblur early for pre-active lines */
|
|
3881
3989
|
.lyrics-container.blur-inactive-enabled .lyrics-line.pre-active {
|
|
3882
3990
|
filter: blur(0px) !important;
|
|
3883
|
-
opacity:
|
|
3991
|
+
opacity: 1;
|
|
3884
3992
|
}
|
|
3885
3993
|
|
|
3886
3994
|
/* ==========================================================================
|
|
@@ -4118,43 +4226,45 @@ AmLyrics$1.styles = i$3 `
|
|
|
4118
4226
|
INSTRUMENTAL GAP STYLES
|
|
4119
4227
|
========================================================================== */
|
|
4120
4228
|
.lyrics-gap {
|
|
4121
|
-
height: 0;
|
|
4229
|
+
max-height: 0;
|
|
4122
4230
|
padding: 0 var(--lyplus-padding-gap);
|
|
4123
4231
|
overflow: hidden;
|
|
4124
4232
|
opacity: 0;
|
|
4125
4233
|
box-sizing: content-box;
|
|
4126
4234
|
background-clip: unset;
|
|
4235
|
+
transform-origin: top;
|
|
4127
4236
|
transition:
|
|
4128
|
-
padding 0.
|
|
4129
|
-
height 0.
|
|
4130
|
-
opacity
|
|
4131
|
-
transform
|
|
4237
|
+
padding 220ms cubic-bezier(0.33, 1, 0.68, 1),
|
|
4238
|
+
max-height 220ms cubic-bezier(0.33, 1, 0.68, 1),
|
|
4239
|
+
opacity 160ms ease-out,
|
|
4240
|
+
transform var(--scroll-duration, 280ms) var(--lyrics-line-delay, 0ms);
|
|
4132
4241
|
}
|
|
4133
4242
|
|
|
4134
4243
|
.lyrics-gap.active {
|
|
4135
|
-
height: 1.
|
|
4244
|
+
max-height: 1.6em;
|
|
4136
4245
|
padding: var(--lyplus-padding-gap);
|
|
4137
4246
|
opacity: 1;
|
|
4138
4247
|
overflow: visible;
|
|
4139
4248
|
transition:
|
|
4140
|
-
padding 0.
|
|
4141
|
-
height 0.
|
|
4142
|
-
opacity
|
|
4143
|
-
transform
|
|
4144
|
-
will-change: height, opacity, padding;
|
|
4249
|
+
padding 220ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
4250
|
+
max-height 220ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
4251
|
+
opacity 160ms ease-out,
|
|
4252
|
+
transform var(--scroll-duration, 280ms);
|
|
4253
|
+
will-change: max-height, opacity, padding;
|
|
4145
4254
|
}
|
|
4146
4255
|
|
|
4147
|
-
/* Exiting state:
|
|
4256
|
+
/* Exiting state: quickly collapse width and height so dots don't distort page, or remove max-height transition */
|
|
4148
4257
|
.lyrics-gap.gap-exiting {
|
|
4149
|
-
height:
|
|
4150
|
-
padding: var(--lyplus-padding-gap);
|
|
4151
|
-
opacity:
|
|
4258
|
+
max-height: 0;
|
|
4259
|
+
padding: 0 var(--lyplus-padding-gap);
|
|
4260
|
+
opacity: 0;
|
|
4152
4261
|
overflow: visible;
|
|
4153
4262
|
transition:
|
|
4154
|
-
padding 0.
|
|
4155
|
-
height
|
|
4156
|
-
|
|
4157
|
-
|
|
4263
|
+
padding var(--gap-exit-duration, 360ms) cubic-bezier(0.33, 1, 0.68, 1),
|
|
4264
|
+
max-height var(--gap-exit-duration, 360ms)
|
|
4265
|
+
cubic-bezier(0.33, 1, 0.68, 1),
|
|
4266
|
+
opacity 160ms ease-out,
|
|
4267
|
+
transform var(--scroll-duration, 280ms);
|
|
4158
4268
|
}
|
|
4159
4269
|
|
|
4160
4270
|
.lyrics-gap .main-vocal-container {
|
|
@@ -4163,7 +4273,8 @@ AmLyrics$1.styles = i$3 `
|
|
|
4163
4273
|
|
|
4164
4274
|
/* Jump animation plays during exit */
|
|
4165
4275
|
.lyrics-gap.gap-exiting .main-vocal-container {
|
|
4166
|
-
animation: gap-ended
|
|
4276
|
+
animation: gap-ended var(--gap-exit-duration, 360ms)
|
|
4277
|
+
cubic-bezier(0.33, 1, 0.68, 1) forwards;
|
|
4167
4278
|
}
|
|
4168
4279
|
|
|
4169
4280
|
.lyrics-gap:not(.active):not(.gap-exiting) .main-vocal-container {
|
|
@@ -4177,7 +4288,9 @@ AmLyrics$1.styles = i$3 `
|
|
|
4177
4288
|
}
|
|
4178
4289
|
|
|
4179
4290
|
.lyrics-gap.active .main-vocal-container .lyrics-word {
|
|
4180
|
-
animation: gap-loop
|
|
4291
|
+
animation: gap-loop var(--gap-pulse-duration, 4000ms) ease-in-out infinite
|
|
4292
|
+
alternate;
|
|
4293
|
+
animation-delay: var(--gap-loop-delay, 0ms);
|
|
4181
4294
|
will-change: transform;
|
|
4182
4295
|
}
|
|
4183
4296
|
|
|
@@ -4201,6 +4314,18 @@ AmLyrics$1.styles = i$3 `
|
|
|
4201
4314
|
color: var(--lyplus-text-primary) !important;
|
|
4202
4315
|
}
|
|
4203
4316
|
|
|
4317
|
+
.lyrics-line.pre-active .lyrics-syllable.line-synced {
|
|
4318
|
+
animation: fade-in-line 0.14s ease-out forwards !important;
|
|
4319
|
+
color: var(--lyplus-text-primary) !important;
|
|
4320
|
+
}
|
|
4321
|
+
|
|
4322
|
+
.lyrics-line.active .lyrics-syllable.line-synced span.char,
|
|
4323
|
+
.lyrics-line.pre-active .lyrics-syllable.line-synced span.char {
|
|
4324
|
+
background-image: none !important;
|
|
4325
|
+
background-color: var(--lyplus-text-primary) !important;
|
|
4326
|
+
transition: background-color 120ms ease-out !important;
|
|
4327
|
+
}
|
|
4328
|
+
|
|
4204
4329
|
@keyframes fade-in-line {
|
|
4205
4330
|
from {
|
|
4206
4331
|
opacity: 0.5;
|
|
@@ -4614,19 +4739,20 @@ AmLyrics$1.styles = i$3 `
|
|
|
4614
4739
|
/* Gap dot animations */
|
|
4615
4740
|
@keyframes gap-loop {
|
|
4616
4741
|
from {
|
|
4617
|
-
transform: scale(1.
|
|
4742
|
+
transform: scale(1.12);
|
|
4618
4743
|
}
|
|
4619
4744
|
to {
|
|
4620
|
-
transform: scale(0.85);
|
|
4745
|
+
transform: scale(var(--gap-exit-scale, 0.85));
|
|
4621
4746
|
}
|
|
4622
4747
|
}
|
|
4623
4748
|
|
|
4624
4749
|
@keyframes gap-ended {
|
|
4625
4750
|
0% {
|
|
4626
|
-
transform: translateY(-25%) scale(
|
|
4751
|
+
transform: translateY(-25%) scale(var(--gap-exit-scale, 0.85))
|
|
4752
|
+
translateZ(0);
|
|
4627
4753
|
}
|
|
4628
4754
|
35% {
|
|
4629
|
-
transform: translateY(-
|
|
4755
|
+
transform: translateY(-5%) scale(1.08) translateZ(0);
|
|
4630
4756
|
}
|
|
4631
4757
|
100% {
|
|
4632
4758
|
transform: translateY(-25%) scale(0) translateZ(0);
|