@uimaxbai/am-lyrics 1.3.0 → 1.4.1

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.
@@ -319,7 +319,7 @@ class GoogleService {
319
319
  }
320
320
  }
321
321
 
322
- const VERSION = '1.3.0';
322
+ const VERSION = '1.4.1';
323
323
  const INSTRUMENTAL_THRESHOLD_MS = 7000; // Show dots for gaps >= 7s
324
324
  const FETCH_TIMEOUT_MS = 8000; // Timeout for all lyrics fetch requests
325
325
  const SEEK_THRESHOLD_MS = 500;
@@ -342,32 +342,17 @@ function fetchWithTimeout(url, options = {}, timeoutMs = FETCH_TIMEOUT_MS) {
342
342
  }
343
343
  const KPOE_SERVERS = [
344
344
  'https://lyricsplus.binimum.org',
345
- 'https://lyricsplus.atomix.one',
346
345
  'https://lyricsplus-seven.vercel.app',
347
346
  'https://lyricsplus.prjktla.workers.dev',
348
347
  'https://lyrics-plus-backend.vercel.app',
349
348
  ];
350
349
  const DEFAULT_KPOE_SOURCE_ORDER = 'apple,lyricsplus,musixmatch,spotify,qq,deezer,musixmatch-word';
351
- const TIDAL_SERVERS = [
352
- 'https://arran.monochrome.tf',
353
- 'https://api.monochrome.tf/',
354
- 'https://triton.squid.wtf',
355
- 'https://wolf.qqdl.site',
356
- 'https://maus.qqdl.site',
357
- 'https://vogel.qqdl.site',
358
- 'https://katze.qqdl.site',
359
- 'https://hund.qqdl.site',
360
- 'https://tidal.kinoplus.online',
361
- 'https://hifi-one.spotisaver.net',
362
- 'https://hifi-two.spotisaver.net',
363
- ];
364
350
  const GENIUS_WORKER_URL = 'https://fetch-genius.samidy.workers.dev/';
365
351
  class AmLyrics extends i {
366
352
  constructor() {
367
353
  super(...arguments);
368
354
  this.downloadFormat = 'auto';
369
355
  this.highlightColor = '#ffffff';
370
- this.hoverBackgroundColor = 'rgba(255, 255, 255, 0.13)';
371
356
  this.autoScroll = true;
372
357
  this.interpolate = true;
373
358
  this.showRomanization = false;
@@ -412,6 +397,10 @@ class AmLyrics extends i {
412
397
  // Syllable animation tracking
413
398
  this.lastActiveIndex = 0;
414
399
  this.visibleLineIds = new Set();
400
+ // Cached element tracking to avoid repeated querySelectorAll calls
401
+ this.preActiveLineElements = [];
402
+ this.positionedLineElements = [];
403
+ this.activeGapLineElements = [];
415
404
  // Bound handler references for proper event listener removal
416
405
  this._boundHandleUserScroll = this.handleUserScroll.bind(this);
417
406
  this._boundAnimateProgress = this.animateProgress.bind(this);
@@ -484,6 +473,31 @@ class AmLyrics extends i {
484
473
  }
485
474
  set currentTime(value) {
486
475
  const oldValue = this._currentTime;
476
+ // If the new time is significantly smaller than the old time (e.g. song looped)
477
+ if (value < oldValue && oldValue - value > 1000 && this.lyrics) {
478
+ this.activeLineIndices = [];
479
+ this.activeMainWordIndices.clear();
480
+ this.activeBackgroundWordIndices.clear();
481
+ this.mainWordProgress.clear();
482
+ this.backgroundWordProgress.clear();
483
+ this.mainWordAnimations.clear();
484
+ this.backgroundWordAnimations.clear();
485
+ this.preActiveLineElements = [];
486
+ this.positionedLineElements = [];
487
+ this.activeGapLineElements = [];
488
+ // Stop all running animations and clear highlights immediately
489
+ if (this.lyricsContainer) {
490
+ const activeLines = this.lyricsContainer.querySelectorAll('.lyrics-line.active, .lyrics-line.pre-active');
491
+ activeLines.forEach(line => {
492
+ line.classList.remove('active', 'pre-active');
493
+ AmLyrics.resetSyllables(line);
494
+ });
495
+ const activeGaps = this.lyricsContainer.querySelectorAll('.lyrics-gap.active, .lyrics-gap.gap-exiting');
496
+ activeGaps.forEach(gap => gap.classList.remove('active', 'gap-exiting'));
497
+ // Reset gap cache since we manually messed with the elements
498
+ this.gapElementCache.clear();
499
+ }
500
+ }
487
501
  this._currentTime = value;
488
502
  if (oldValue !== value && this.lyrics) {
489
503
  this._onTimeChanged(oldValue, value);
@@ -545,6 +559,9 @@ class AmLyrics extends i {
545
559
  this.lyricsContainer.removeEventListener('wheel', this._boundHandleUserScroll);
546
560
  this.lyricsContainer.removeEventListener('touchmove', this._boundHandleUserScroll);
547
561
  }
562
+ this.preActiveLineElements = [];
563
+ this.positionedLineElements = [];
564
+ this.activeGapLineElements = [];
548
565
  }
549
566
  async fetchLyrics() {
550
567
  // Cancel any in-flight fetch to prevent stale results from racing
@@ -579,16 +596,7 @@ class AmLyrics extends i {
579
596
  }
580
597
  }
581
598
  if (collectedSources.length === 0 && resolvedMetadata?.metadata) {
582
- const tidalResult = await AmLyrics.fetchLyricsFromTidal(resolvedMetadata.metadata, resolvedMetadata.catalogIsrc);
583
- if (tidalResult && tidalResult.lines.length > 0) {
584
- collectedSources.push({
585
- lines: tidalResult.lines,
586
- source: 'Tidal',
587
- });
588
- }
589
- }
590
- // Fallback: LRCLIB
591
- if (collectedSources.length === 0 && resolvedMetadata?.metadata) {
599
+ // Fallback: LRCLIB
592
600
  const lrclibResult = await AmLyrics.fetchLyricsFromLrclib(resolvedMetadata.metadata);
593
601
  if (lrclibResult && lrclibResult.lines.length > 0) {
594
602
  collectedSources.push({
@@ -608,15 +616,17 @@ class AmLyrics extends i {
608
616
  }
609
617
  this.hasFetchedAllProviders =
610
618
  collectedSources.length === 0 ||
611
- collectedSources.some(s => s.source === 'LRCLIB' ||
612
- s.source === 'Tidal' ||
613
- s.source === 'Genius');
619
+ collectedSources.some(s => s.source === 'LRCLIB' || s.source === 'Genius');
614
620
  this._updateFooter();
615
621
  if (collectedSources.length > 0) {
616
622
  this.availableSources = AmLyrics.mergeAndSortSources(collectedSources);
617
623
  this.currentSourceIndex = 0;
618
- this.lyrics = this.availableSources[0].lines;
619
- this.lyricsSource = this.availableSources[0].source;
624
+ const sourceResult = this.availableSources[0];
625
+ this.lyrics = sourceResult.lines;
626
+ this.lyricsSource = sourceResult.source;
627
+ if (sourceResult.songwriters) {
628
+ this.songwriters = sourceResult.songwriters;
629
+ }
620
630
  await this.onLyricsLoaded();
621
631
  return;
622
632
  }
@@ -638,6 +648,9 @@ class AmLyrics extends i {
638
648
  this.backgroundWordProgress.clear();
639
649
  this.mainWordAnimations.clear();
640
650
  this.backgroundWordAnimations.clear();
651
+ this.preActiveLineElements = [];
652
+ this.positionedLineElements = [];
653
+ this.activeGapLineElements = [];
641
654
  if (this.lyricsContainer) {
642
655
  this.isProgrammaticScroll = true;
643
656
  this.lyricsContainer.scrollTop = 0;
@@ -667,36 +680,30 @@ class AmLyrics extends i {
667
680
  return 2;
668
681
  if (lower.includes('musixmatch') && hasWordSync)
669
682
  return 3;
670
- if (lower.includes('tidal') && hasWordSync)
671
- return 4;
672
683
  if (lower.includes('lrclib') && hasWordSync)
673
- return 5;
684
+ return 4;
674
685
  if (hasWordSync)
675
- return 6;
686
+ return 5;
676
687
  if (lower.includes('apple') && !hasWordSync && !isUnsynced)
677
- return 7;
688
+ return 6;
678
689
  if (isQQ && !hasWordSync && !isUnsynced)
679
- return 8;
690
+ return 7;
680
691
  if (lower.includes('musixmatch') && !hasWordSync && !isUnsynced)
681
- return 9;
682
- if (lower.includes('tidal') && !hasWordSync && !isUnsynced)
683
- return 10;
692
+ return 8;
684
693
  if (lower.includes('lrclib') && !hasWordSync && !isUnsynced)
685
- return 11;
694
+ return 9;
686
695
  if (!hasWordSync && !isUnsynced)
687
- return 12;
696
+ return 10;
688
697
  if (lower.includes('apple') && isUnsynced)
689
- return 13;
698
+ return 11;
690
699
  if (isQQ && isUnsynced)
691
- return 14;
700
+ return 12;
692
701
  if (lower.includes('musixmatch') && isUnsynced)
693
- return 15;
694
- if (lower.includes('tidal') && isUnsynced)
695
- return 16;
702
+ return 13;
696
703
  if (lower.includes('lrclib') && isUnsynced)
697
- return 17;
704
+ return 14;
698
705
  if (lower.includes('genius'))
699
- return 18;
706
+ return 15;
700
707
  return 20;
701
708
  }
702
709
  static mergeAndSortSources(collectedSources) {
@@ -727,13 +734,6 @@ class AmLyrics extends i {
727
734
  const resolvedMetadata = await this.resolveSongMetadata();
728
735
  if (resolvedMetadata?.metadata) {
729
736
  const newSources = [];
730
- // Try Tidal if not fetched
731
- if (!this.availableSources.some(s => s.source.toLowerCase().includes('tidal'))) {
732
- const tidalResult = await AmLyrics.fetchLyricsFromTidal(resolvedMetadata.metadata, resolvedMetadata.catalogIsrc);
733
- if (tidalResult && tidalResult.lines.length > 0) {
734
- newSources.push({ lines: tidalResult.lines, source: 'Tidal' });
735
- }
736
- }
737
737
  // Try LRCLIB if not fetched
738
738
  if (!this.availableSources.some(s => s.source.toLowerCase().includes('lrclib'))) {
739
739
  const lrclibResult = await AmLyrics.fetchLyricsFromLrclib(resolvedMetadata.metadata);
@@ -768,8 +768,12 @@ class AmLyrics extends i {
768
768
  if (this.availableSources.length > 1) {
769
769
  this.currentSourceIndex =
770
770
  (this.currentSourceIndex + 1) % this.availableSources.length;
771
- this.lyrics = this.availableSources[this.currentSourceIndex].lines;
772
- this.lyricsSource = this.availableSources[this.currentSourceIndex].source;
771
+ const sourceResult = this.availableSources[this.currentSourceIndex];
772
+ this.lyrics = sourceResult.lines;
773
+ this.lyricsSource = sourceResult.source;
774
+ if (sourceResult.songwriters) {
775
+ this.songwriters = sourceResult.songwriters;
776
+ }
773
777
  await this.onLyricsLoaded();
774
778
  }
775
779
  }
@@ -778,6 +782,7 @@ class AmLyrics extends i {
778
782
  title: this.songTitle?.trim() ?? '',
779
783
  artist: this.songArtist?.trim() ?? '',
780
784
  album: this.songAlbum?.trim() || undefined,
785
+ songwriters: this.songwriters?.trim() || undefined,
781
786
  durationMs: undefined,
782
787
  };
783
788
  if (typeof this.songDurationMs === 'number' && this.songDurationMs > 0) {
@@ -817,6 +822,9 @@ class AmLyrics extends i {
817
822
  if (!metadata.album && catalogResult.album) {
818
823
  metadata.album = catalogResult.album;
819
824
  }
825
+ if (!metadata.songwriters && catalogResult.songwriters) {
826
+ metadata.songwriters = catalogResult.songwriters;
827
+ }
820
828
  if (metadata.durationMs == null &&
821
829
  typeof catalogResult.durationMs === 'number' &&
822
830
  catalogResult.durationMs > 0) {
@@ -1007,9 +1015,13 @@ class AmLyrics extends i {
1007
1015
  const ttmlRes = await fetchWithTimeout(result.lyricsUrl);
1008
1016
  if (ttmlRes.ok) {
1009
1017
  const ttmlText = await ttmlRes.text();
1010
- const lines = AmLyrics.parseTTML(ttmlText);
1011
- if (lines && lines.length > 0) {
1012
- allResults.push({ lines, source: 'BiniLyrics' });
1018
+ const parseResult = AmLyrics.parseTTML(ttmlText);
1019
+ if (parseResult && parseResult.lines.length > 0) {
1020
+ allResults.push({
1021
+ lines: parseResult.lines,
1022
+ source: 'BiniLyrics',
1023
+ songwriters: parseResult.songwriters,
1024
+ });
1013
1025
  return allResults;
1014
1026
  }
1015
1027
  }
@@ -1041,11 +1053,12 @@ class AmLyrics extends i {
1041
1053
  const ttmlRes = await fetchWithTimeout(result.lyricsUrl);
1042
1054
  if (ttmlRes.ok) {
1043
1055
  const ttmlText = await ttmlRes.text();
1044
- const lines = AmLyrics.parseTTML(ttmlText);
1045
- if (lines && lines.length > 0) {
1056
+ const parseResult = AmLyrics.parseTTML(ttmlText);
1057
+ if (parseResult && parseResult.lines.length > 0) {
1046
1058
  allResults.push({
1047
- lines,
1059
+ lines: parseResult.lines,
1048
1060
  source: 'BiniLyrics',
1061
+ songwriters: parseResult.songwriters,
1049
1062
  });
1050
1063
  return allResults;
1051
1064
  }
@@ -1178,77 +1191,6 @@ class AmLyrics extends i {
1178
1191
  }
1179
1192
  return lines;
1180
1193
  }
1181
- /**
1182
- * Fetch lyrics from Tidal API.
1183
- * Picks 2 random servers, tries search + lyrics on each.
1184
- */
1185
- static async fetchLyricsFromTidal(metadata, isrc) {
1186
- const title = metadata.title?.trim();
1187
- const artist = metadata.artist?.trim();
1188
- if (!title || !artist)
1189
- return null;
1190
- // Pick 3 random unique servers for better reliability
1191
- const shuffled = [...TIDAL_SERVERS].sort(() => Math.random() - 0.5);
1192
- const serversToTry = shuffled.slice(0, 3);
1193
- for (const base of serversToTry) {
1194
- try {
1195
- const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base;
1196
- // Step 1: Search for the track
1197
- const searchQuery = `${title} ${artist}`;
1198
- const searchParams = new URLSearchParams({ s: searchQuery });
1199
- // eslint-disable-next-line no-await-in-loop
1200
- const searchResponse = await fetchWithTimeout(`${normalizedBase}/search/?${searchParams.toString()}`);
1201
- if (!searchResponse.ok) {
1202
- // eslint-disable-next-line no-continue
1203
- continue;
1204
- }
1205
- // eslint-disable-next-line no-await-in-loop
1206
- const searchData = await searchResponse.json();
1207
- const items = searchData?.data?.items;
1208
- if (!Array.isArray(items) || items.length === 0) {
1209
- // eslint-disable-next-line no-continue
1210
- continue;
1211
- }
1212
- // Find best match: prefer ISRC match, then first result
1213
- let bestTrack = items[0];
1214
- if (isrc) {
1215
- const isrcMatch = items.find((item) => item.isrc && item.isrc.toLowerCase() === isrc.toLowerCase());
1216
- if (isrcMatch) {
1217
- bestTrack = isrcMatch;
1218
- }
1219
- }
1220
- const trackId = bestTrack?.id;
1221
- if (!trackId) {
1222
- // eslint-disable-next-line no-continue
1223
- continue;
1224
- }
1225
- // Step 2: Fetch lyrics
1226
- // eslint-disable-next-line no-await-in-loop
1227
- const lyricsResponse = await fetchWithTimeout(`${normalizedBase}/lyrics/?id=${trackId}`);
1228
- if (!lyricsResponse.ok) {
1229
- // eslint-disable-next-line no-continue
1230
- continue;
1231
- }
1232
- // eslint-disable-next-line no-await-in-loop
1233
- const lyricsData = await lyricsResponse.json();
1234
- const subtitles = lyricsData?.lyrics?.subtitles;
1235
- if (subtitles && typeof subtitles === 'string') {
1236
- const lines = AmLyrics.parseLrcSubtitles(subtitles);
1237
- if (lines.length > 0) {
1238
- const provider = lyricsData?.lyrics?.lyricsProvider || 'Tidal';
1239
- return {
1240
- lines,
1241
- source: `Tidal (${provider})`,
1242
- };
1243
- }
1244
- }
1245
- }
1246
- catch {
1247
- // Try next server
1248
- }
1249
- }
1250
- return null;
1251
- }
1252
1194
  /**
1253
1195
  * Fetch lyrics from LRCLIB.
1254
1196
  * Uses search endpoint, prefers synced lyrics.
@@ -1431,6 +1373,19 @@ class AmLyrics extends i {
1431
1373
  agentMap[id] = type;
1432
1374
  }
1433
1375
  }
1376
+ let songwriters;
1377
+ const songwritersNodes = doc.getElementsByTagName('songwriter');
1378
+ if (songwritersNodes.length > 0) {
1379
+ const names = [];
1380
+ for (let i = 0; i < songwritersNodes.length; i += 1) {
1381
+ if (songwritersNodes[i].textContent) {
1382
+ names.push(songwritersNodes[i].textContent);
1383
+ }
1384
+ }
1385
+ if (names.length > 0) {
1386
+ songwriters = names.join(', ');
1387
+ }
1388
+ }
1434
1389
  const translationNodes = doc.getElementsByTagName('translation');
1435
1390
  for (let i = 0; i < translationNodes.length; i += 1) {
1436
1391
  const texts = translationNodes[i].getElementsByTagName('text');
@@ -1547,7 +1502,7 @@ class AmLyrics extends i {
1547
1502
  text: bgText,
1548
1503
  timestamp: timeToMs(bgSpan.getAttribute('begin')),
1549
1504
  endtime: timeToMs(bgSpan.getAttribute('end')),
1550
- part: false,
1505
+ part: !/\s$/.test(bgText),
1551
1506
  });
1552
1507
  }
1553
1508
  // eslint-disable-next-line no-continue
@@ -1570,7 +1525,7 @@ class AmLyrics extends i {
1570
1525
  text,
1571
1526
  timestamp: timeToMs(span.getAttribute('begin')),
1572
1527
  endtime: timeToMs(span.getAttribute('end')),
1573
- part: false,
1528
+ part: !/\s$/.test(text),
1574
1529
  });
1575
1530
  }
1576
1531
  }
@@ -1656,7 +1611,7 @@ class AmLyrics extends i {
1656
1611
  oppositeTurn: alignment === 'end',
1657
1612
  });
1658
1613
  }
1659
- return lines;
1614
+ return { lines, songwriters };
1660
1615
  }
1661
1616
  catch (e) {
1662
1617
  // eslint-disable-next-line no-console
@@ -1821,7 +1776,10 @@ class AmLyrics extends i {
1821
1776
  if (!newActiveLines.includes(lineIndex)) {
1822
1777
  const lineElement = this._getLineElement(lineIndex);
1823
1778
  if (lineElement) {
1824
- lineElement.classList.remove('active');
1779
+ lineElement.classList.remove('active', 'pre-active');
1780
+ const preIdx = this.preActiveLineElements.indexOf(lineElement);
1781
+ if (preIdx !== -1)
1782
+ this.preActiveLineElements.splice(preIdx, 1);
1825
1783
  AmLyrics.resetSyllables(lineElement);
1826
1784
  }
1827
1785
  }
@@ -1833,6 +1791,9 @@ class AmLyrics extends i {
1833
1791
  if (lineElement) {
1834
1792
  lineElement.classList.add('active');
1835
1793
  lineElement.classList.remove('pre-active');
1794
+ const preIdx = this.preActiveLineElements.indexOf(lineElement);
1795
+ if (preIdx !== -1)
1796
+ this.preActiveLineElements.splice(preIdx, 1);
1836
1797
  }
1837
1798
  }
1838
1799
  }
@@ -1852,10 +1813,9 @@ class AmLyrics extends i {
1852
1813
  }
1853
1814
  }
1854
1815
  // Also update syllables in active gap lines (breathing dots)
1855
- const activeGaps = this.lyricsContainer.querySelectorAll('.lyrics-gap.active');
1856
- activeGaps.forEach(gapLine => {
1816
+ for (const gapLine of this.activeGapLineElements) {
1857
1817
  AmLyrics.updateSyllablesForLine(gapLine, newTime);
1858
- });
1818
+ }
1859
1819
  // Imperatively manage gap active state
1860
1820
  if (this.gapElementCache.size > 0) {
1861
1821
  for (const [, gap] of this.gapElementCache) {
@@ -1866,9 +1826,21 @@ class AmLyrics extends i {
1866
1826
  const isExiting = gap.classList.contains('gap-exiting');
1867
1827
  const exitLeadMs = GAP_EXIT_LEAD_MS;
1868
1828
  const shouldStartExiting = isActive && !isExiting && newTime >= gapEndTime - exitLeadMs;
1869
- if (shouldBeActive && !isActive && !isExiting) {
1829
+ if (shouldBeActive && (!isActive || isSeek) && !isExiting) {
1870
1830
  gap.classList.remove('gap-exiting');
1831
+ if (isSeek && isActive) {
1832
+ gap.classList.remove('active');
1833
+ // eslint-disable-next-line no-void
1834
+ void gap.offsetWidth; // Force reflow
1835
+ }
1836
+ const gapDuration = gapEndTime - gapStartTime;
1837
+ const baseLoopDelay = AmLyrics.getGapLoopDelay(gapDuration);
1838
+ const totalDelay = baseLoopDelay + (newTime - gapStartTime);
1839
+ gap.style.setProperty('--gap-loop-delay', `-${totalDelay}ms`);
1871
1840
  gap.classList.add('active');
1841
+ if (!this.activeGapLineElements.includes(gap)) {
1842
+ this.activeGapLineElements.push(gap);
1843
+ }
1872
1844
  const dotSyllables = gap.querySelectorAll('.lyrics-syllable');
1873
1845
  dotSyllables.forEach(dot => {
1874
1846
  const dotStart = parseFloat(dot.getAttribute('data-start-time') || '0');
@@ -1876,24 +1848,34 @@ class AmLyrics extends i {
1876
1848
  if (newTime > dotEnd) {
1877
1849
  dot.classList.add('finished');
1878
1850
  if (!dot.classList.contains('highlight')) {
1879
- AmLyrics.updateSyllableAnimation(dot);
1851
+ AmLyrics.updateSyllableAnimation(dot, newTime - dotStart);
1880
1852
  }
1881
1853
  }
1882
1854
  else if (newTime >= dotStart && newTime <= dotEnd) {
1883
- AmLyrics.updateSyllableAnimation(dot);
1855
+ AmLyrics.updateSyllableAnimation(dot, newTime - dotStart);
1884
1856
  }
1885
1857
  });
1886
1858
  }
1887
1859
  else if (shouldStartExiting) {
1888
- gap.classList.add('gap-exiting');
1860
+ // Cancel gap-loop first, force reflow, then start gap-ended
1861
+ // so the browser sees a clean animation swap
1889
1862
  gap.classList.remove('active');
1863
+ // eslint-disable-next-line no-void
1864
+ void gap.offsetWidth;
1865
+ gap.classList.add('gap-exiting');
1866
+ const gapIdx = this.activeGapLineElements.indexOf(gap);
1867
+ if (gapIdx !== -1)
1868
+ this.activeGapLineElements.splice(gapIdx, 1);
1890
1869
  setTimeout(() => {
1891
1870
  gap.classList.remove('gap-exiting');
1892
1871
  }, GAP_EXIT_LEAD_MS);
1893
1872
  }
1894
- else if (isActive && !shouldBeActive) {
1873
+ else if (!shouldBeActive && (isActive || isExiting)) {
1895
1874
  gap.classList.remove('active');
1896
1875
  gap.classList.remove('gap-exiting');
1876
+ const gapIdx = this.activeGapLineElements.indexOf(gap);
1877
+ if (gapIdx !== -1)
1878
+ this.activeGapLineElements.splice(gapIdx, 1);
1897
1879
  }
1898
1880
  else if (isExiting && newTime < gapEndTime - exitLeadMs) {
1899
1881
  gap.classList.remove('gap-exiting');
@@ -1911,20 +1893,41 @@ class AmLyrics extends i {
1911
1893
  const isExiting = gap.classList.contains('gap-exiting');
1912
1894
  const exitLeadMs = GAP_EXIT_LEAD_MS;
1913
1895
  const shouldStartExiting = isActive && !isExiting && newTime >= gapEndTime - exitLeadMs;
1914
- if (shouldBeActive && !isActive && !isExiting) {
1896
+ if (shouldBeActive && (!isActive || isSeek) && !isExiting) {
1915
1897
  gap.classList.remove('gap-exiting');
1898
+ if (isSeek && isActive) {
1899
+ gap.classList.remove('active');
1900
+ // eslint-disable-next-line no-void
1901
+ void gap.offsetWidth; // Force reflow
1902
+ }
1903
+ const gapDuration = gapEndTime - gapStartTime;
1904
+ const baseLoopDelay = AmLyrics.getGapLoopDelay(gapDuration);
1905
+ const totalDelay = baseLoopDelay + (newTime - gapStartTime);
1906
+ gap.style.setProperty('--gap-loop-delay', `-${totalDelay}ms`);
1916
1907
  gap.classList.add('active');
1908
+ if (!this.activeGapLineElements.includes(gap)) {
1909
+ this.activeGapLineElements.push(gap);
1910
+ }
1917
1911
  }
1918
1912
  else if (shouldStartExiting) {
1919
- gap.classList.add('gap-exiting');
1913
+ // Cancel gap-loop first, force reflow, then start gap-ended
1920
1914
  gap.classList.remove('active');
1915
+ // eslint-disable-next-line no-void
1916
+ void gap.offsetWidth;
1917
+ gap.classList.add('gap-exiting');
1918
+ const gapIdx = this.activeGapLineElements.indexOf(gap);
1919
+ if (gapIdx !== -1)
1920
+ this.activeGapLineElements.splice(gapIdx, 1);
1921
1921
  setTimeout(() => {
1922
1922
  gap.classList.remove('gap-exiting');
1923
1923
  }, GAP_EXIT_LEAD_MS);
1924
1924
  }
1925
- else if (isActive && !shouldBeActive) {
1925
+ else if (!shouldBeActive && (isActive || isExiting)) {
1926
1926
  gap.classList.remove('active');
1927
1927
  gap.classList.remove('gap-exiting');
1928
+ const gapIdx = this.activeGapLineElements.indexOf(gap);
1929
+ if (gapIdx !== -1)
1930
+ this.activeGapLineElements.splice(gapIdx, 1);
1928
1931
  }
1929
1932
  else if (isExiting && newTime < gapEndTime - exitLeadMs) {
1930
1933
  gap.classList.remove('gap-exiting');
@@ -1939,6 +1942,25 @@ class AmLyrics extends i {
1939
1942
  else if (this.lastInstrumentalIndex !== null) {
1940
1943
  this.lastInstrumentalIndex = null;
1941
1944
  }
1945
+ // Check footer active state
1946
+ const lastLyric = this.lyrics && this.lyrics.length > 0
1947
+ ? this.lyrics[this.lyrics.length - 1]
1948
+ : null;
1949
+ const footer = this.lyricsContainer.querySelector('.lyrics-footer');
1950
+ if (footer && lastLyric && lastLyric.endtime > 0) {
1951
+ const isFooterActive = newTime > lastLyric.endtime + 200; // Snappier 200ms buffer
1952
+ if (isFooterActive && !footer.classList.contains('active')) {
1953
+ footer.classList.add('active');
1954
+ if (this.autoScroll &&
1955
+ !this.isUserScrolling &&
1956
+ !this.isClickSeeking) {
1957
+ this.focusLine(footer);
1958
+ }
1959
+ }
1960
+ else if (!isFooterActive && footer.classList.contains('active')) {
1961
+ footer.classList.remove('active');
1962
+ }
1963
+ }
1942
1964
  // Pre-scroll: scroll to upcoming line ~0.5s before it starts
1943
1965
  if (this.autoScroll &&
1944
1966
  !this.isUserScrolling &&
@@ -1961,6 +1983,9 @@ class AmLyrics extends i {
1961
1983
  preActiveLineIndex = i;
1962
1984
  if (!isBackToBack) {
1963
1985
  nextLineEl.classList.add('pre-active');
1986
+ if (!this.preActiveLineElements.includes(nextLineEl)) {
1987
+ this.preActiveLineElements.push(nextLineEl);
1988
+ }
1964
1989
  }
1965
1990
  this.clearPreActiveClasses(i);
1966
1991
  const slowScrollDuration = Math.max(SCROLL_ANIMATION_DURATION_MS, timeUntilStart);
@@ -1990,6 +2015,9 @@ class AmLyrics extends i {
1990
2015
  if (lineEl)
1991
2016
  lineEl.classList.add('active');
1992
2017
  }
2018
+ // Trigger a faux time-change so that updateSyllablesForLine fires
2019
+ // to setup inline syllable CSS wipe animations for whatever the current time is
2020
+ this._onTimeChanged(0, this.currentTime);
1993
2021
  }
1994
2022
  }
1995
2023
  // Handle duration reset (-1 stops playback and resets currentTime to 0)
@@ -2002,6 +2030,9 @@ class AmLyrics extends i {
2002
2030
  this.backgroundWordProgress.clear();
2003
2031
  this.mainWordAnimations.clear();
2004
2032
  this.backgroundWordAnimations.clear();
2033
+ this.preActiveLineElements = [];
2034
+ this.positionedLineElements = [];
2035
+ this.activeGapLineElements = [];
2005
2036
  this.setUserScrolling(false);
2006
2037
  // Cancel any running animations
2007
2038
  if (this.animationFrameId) {
@@ -2055,7 +2086,7 @@ class AmLyrics extends i {
2055
2086
  const gap = this.lyrics[targetLineIndex].timestamp -
2056
2087
  this.lyrics[prevPrimaryIndex].endtime;
2057
2088
  if (gap > 200) {
2058
- scrollDuration = Math.min(Math.max(gap * 0.6, SCROLL_ANIMATION_DURATION_MS), 2000);
2089
+ scrollDuration = Math.min(Math.max(gap * 0.85, SCROLL_ANIMATION_DURATION_MS), 4000);
2059
2090
  }
2060
2091
  }
2061
2092
  this.focusLine(targetLine, forceScroll, scrollDuration);
@@ -2117,6 +2148,9 @@ class AmLyrics extends i {
2117
2148
  this.cachedLineData = null;
2118
2149
  this.lineElementCache.clear();
2119
2150
  this.gapElementCache.clear();
2151
+ this.preActiveLineElements = [];
2152
+ this.positionedLineElements = [];
2153
+ this.activeGapLineElements = [];
2120
2154
  }
2121
2155
  _updateCachedIsUnsynced() {
2122
2156
  this.cachedIsUnsynced =
@@ -2129,13 +2163,23 @@ class AmLyrics extends i {
2129
2163
  return;
2130
2164
  this.cachedLineData = this.lyrics.map(line => {
2131
2165
  const wordGroups = [];
2132
- for (const syllable of line.text) {
2133
- if (syllable.part && wordGroups.length > 0) {
2134
- wordGroups[wordGroups.length - 1].push(syllable);
2135
- }
2136
- else {
2137
- wordGroups.push([syllable]);
2166
+ let currentGroupBuffer = [];
2167
+ line.text.forEach((syllable, idx) => {
2168
+ currentGroupBuffer.push(syllable);
2169
+ const nextSyllable = line.text[idx + 1];
2170
+ const endsWithDelimiter = !nextSyllable ||
2171
+ syllable.part === false ||
2172
+ /\s$/.test(syllable.text) ||
2173
+ (nextSyllable &&
2174
+ syllable.isBackground !==
2175
+ nextSyllable.isBackground);
2176
+ if (endsWithDelimiter) {
2177
+ wordGroups.push(currentGroupBuffer);
2178
+ currentGroupBuffer = [];
2138
2179
  }
2180
+ });
2181
+ if (currentGroupBuffer.length > 0) {
2182
+ wordGroups.push(currentGroupBuffer);
2139
2183
  }
2140
2184
  const groupGrowable = new Array(wordGroups.length).fill(false);
2141
2185
  const groupGlowing = new Array(wordGroups.length).fill(false);
@@ -2144,6 +2188,7 @@ class AmLyrics extends i {
2144
2188
  const vwCharOffset = new Array(wordGroups.length).fill(0);
2145
2189
  const vwStartMs = new Array(wordGroups.length).fill(0);
2146
2190
  const vwEndMs = new Array(wordGroups.length).fill(0);
2191
+ let lineIsRTL = false;
2147
2192
  let vwStart = 0;
2148
2193
  while (vwStart < wordGroups.length) {
2149
2194
  let vwEnd = vwStart;
@@ -2165,9 +2210,11 @@ class AmLyrics extends i {
2165
2210
  const combinedDuration = combinedEnd - combinedStart;
2166
2211
  const isCJK = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(combinedText);
2167
2212
  const isRTL = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u0590-\u05FF]/.test(combinedText);
2213
+ if (isRTL)
2214
+ lineIsRTL = true;
2168
2215
  const hasHyphen = combinedText.includes('-');
2169
2216
  const wordLen = combinedText.length;
2170
- let isGrowableVW = !isCJK && !isRTL && !hasHyphen && wordLen > 0 && wordLen <= 12;
2217
+ let isGrowableVW = !isCJK && !isRTL && !hasHyphen && wordLen > 0 && wordLen <= 7;
2171
2218
  if (isGrowableVW) {
2172
2219
  if (wordLen < 3) {
2173
2220
  isGrowableVW =
@@ -2178,7 +2225,8 @@ class AmLyrics extends i {
2178
2225
  combinedDuration >= 850 && combinedDuration >= wordLen * 190;
2179
2226
  }
2180
2227
  }
2181
- const isGlowingVW = isGrowableVW;
2228
+ const isLineSynced = line.isWordSynced === false || line.text.some(s => s.lineSynced);
2229
+ const isGlowingVW = isGrowableVW && !isLineSynced;
2182
2230
  let charOff = 0;
2183
2231
  for (let gi = vwStart; gi <= vwEnd; gi += 1) {
2184
2232
  groupGrowable[gi] = isGrowableVW;
@@ -2202,6 +2250,7 @@ class AmLyrics extends i {
2202
2250
  vwCharOffset,
2203
2251
  vwStartMs,
2204
2252
  vwEndMs,
2253
+ lineIsRTL,
2205
2254
  };
2206
2255
  });
2207
2256
  }
@@ -2282,15 +2331,17 @@ class AmLyrics extends i {
2282
2331
  clearPreActiveClasses(exceptLineIndex = null) {
2283
2332
  if (!this.lyricsContainer)
2284
2333
  return;
2285
- this.lyricsContainer
2286
- .querySelectorAll('.lyrics-line.pre-active')
2287
- .forEach(element => {
2288
- const lineElement = element;
2334
+ const keptLines = [];
2335
+ for (const lineElement of this.preActiveLineElements) {
2289
2336
  const lineIndex = AmLyrics.getLineIndexFromElement(lineElement);
2290
- if (lineIndex !== exceptLineIndex) {
2337
+ if (lineIndex === exceptLineIndex) {
2338
+ keptLines.push(lineElement);
2339
+ }
2340
+ else {
2291
2341
  lineElement.classList.remove('pre-active');
2292
2342
  }
2293
- });
2343
+ }
2344
+ this.preActiveLineElements = keptLines;
2294
2345
  }
2295
2346
  getPrimaryActiveLineIndex(activeIndices) {
2296
2347
  if (activeIndices.length === 0)
@@ -2740,6 +2791,7 @@ class AmLyrics extends i {
2740
2791
  // Clean up any lingering scroll animations before smooth scroll
2741
2792
  for (const line of animatingLines) {
2742
2793
  line.classList.remove('scroll-animate');
2794
+ line.style.removeProperty('will-change');
2743
2795
  line.style.removeProperty('--scroll-delta');
2744
2796
  line.style.removeProperty('--lyrics-line-delay');
2745
2797
  line.style.removeProperty('--scroll-duration');
@@ -2798,6 +2850,7 @@ class AmLyrics extends i {
2798
2850
  // --- Step 4: Re-add scroll-animate class to start fresh animations ---
2799
2851
  for (const line of newAnimatingLines) {
2800
2852
  line.classList.add('scroll-animate');
2853
+ line.style.willChange = 'transform';
2801
2854
  animatingLines.push(line);
2802
2855
  }
2803
2856
  animState.isAnimating = true;
@@ -2814,6 +2867,7 @@ class AmLyrics extends i {
2814
2867
  for (let i = 0; i < animatingLines.length; i += 1) {
2815
2868
  const line = animatingLines[i];
2816
2869
  line.classList.remove('scroll-animate');
2870
+ line.style.removeProperty('will-change');
2817
2871
  line.style.removeProperty('--scroll-delta');
2818
2872
  line.style.removeProperty('--lyrics-line-delay');
2819
2873
  line.style.removeProperty('--scroll-duration');
@@ -2842,12 +2896,14 @@ class AmLyrics extends i {
2842
2896
  'next-3',
2843
2897
  'next-4',
2844
2898
  ];
2845
- // Remove old position classes
2846
- this.lyricsContainer
2847
- .querySelectorAll(`.${positionClasses.join(', .')}`)
2848
- .forEach(el => el.classList.remove(...positionClasses));
2899
+ // Remove old position classes from tracked elements
2900
+ for (const el of this.positionedLineElements) {
2901
+ el.classList.remove(...positionClasses);
2902
+ }
2903
+ this.positionedLineElements = [];
2849
2904
  // Add new position classes
2850
2905
  lineToScroll.classList.add('lyrics-activest');
2906
+ this.positionedLineElements.push(lineToScroll);
2851
2907
  const lineElements = Array.from(this.lyricsContainer.querySelectorAll('.lyrics-line'));
2852
2908
  const scrollLineIndex = lineElements.indexOf(lineToScroll);
2853
2909
  for (let i = Math.max(0, scrollLineIndex - 4); i <= Math.min(lineElements.length - 1, scrollLineIndex + 4); i += 1) {
@@ -2862,6 +2918,7 @@ class AmLyrics extends i {
2862
2918
  element.classList.add(`prev-${Math.abs(position)}`);
2863
2919
  else
2864
2920
  element.classList.add(`next-${position}`);
2921
+ this.positionedLineElements.push(element);
2865
2922
  }
2866
2923
  }
2867
2924
  }
@@ -2881,11 +2938,12 @@ class AmLyrics extends i {
2881
2938
  paddingTop) < 1) {
2882
2939
  return;
2883
2940
  }
2884
- // Skip scroll if near the bottom of content (prevents footer jitter)
2885
- if (!forceScroll) {
2941
+ // Skip scroll if near the bottom of content and we aren't trying to scroll back up
2942
+ if (!forceScroll && !activeLine.classList.contains('lyrics-footer')) {
2886
2943
  const parent = this.lyricsContainer;
2887
2944
  const atBottom = parent.scrollTop + parent.clientHeight >= parent.scrollHeight - 50;
2888
- if (atBottom) {
2945
+ const targetTop = Math.max(0, -(paddingTop - activeLine.offsetTop));
2946
+ if (atBottom && targetTop > parent.scrollTop - 50) {
2889
2947
  return;
2890
2948
  }
2891
2949
  }
@@ -2906,7 +2964,7 @@ class AmLyrics extends i {
2906
2964
  * Update syllable highlight animation - apply CSS wipe animation
2907
2965
  * (Exact copy from YouLyPlus _updateSyllableAnimation)
2908
2966
  */
2909
- static updateSyllableAnimation(syllable) {
2967
+ static updateSyllableAnimation(syllable, elapsedTimeMs = 0) {
2910
2968
  if (syllable.classList.contains('highlight'))
2911
2969
  return;
2912
2970
  const { classList } = syllable;
@@ -2934,8 +2992,8 @@ class AmLyrics extends i {
2934
2992
  const baseDelayPerChar = finalDuration * 0.09;
2935
2993
  const growDurationMs = finalDuration * 1.5;
2936
2994
  allWordCharSpans.forEach(span => {
2937
- const horizontalOffset = parseFloat(span.dataset.horizontalOffset || '0');
2938
- const maxScale = span.dataset.maxScale || '1.1';
2995
+ const matrixScale = span.dataset.matrixScale || '1.1';
2996
+ const charOffsetX = span.dataset.charOffsetX || '0';
2939
2997
  const shadowIntensity = span.dataset.shadowIntensity || '0.6';
2940
2998
  const translateYPeak = span.dataset.translateYPeak || '-2';
2941
2999
  const syllableCharIndex = parseFloat(span.dataset.syllableCharIndex || '0');
@@ -2943,13 +3001,13 @@ class AmLyrics extends i {
2943
3001
  charAnimationsMap.set(span, `grow-dynamic ${growDurationMs}ms ease-in-out ${growDelay}ms forwards`);
2944
3002
  styleUpdates.push({
2945
3003
  element: span,
2946
- property: '--char-offset-x',
2947
- value: `${horizontalOffset}`,
3004
+ property: '--matrix-scale',
3005
+ value: matrixScale,
2948
3006
  });
2949
3007
  styleUpdates.push({
2950
3008
  element: span,
2951
- property: '--max-scale',
2952
- value: maxScale,
3009
+ property: '--char-offset-x',
3010
+ value: `${charOffsetX}px`,
2953
3011
  });
2954
3012
  styleUpdates.push({
2955
3013
  element: span,
@@ -2959,7 +3017,7 @@ class AmLyrics extends i {
2959
3017
  styleUpdates.push({
2960
3018
  element: span,
2961
3019
  property: '--translate-y-peak',
2962
- value: `${translateYPeak}`,
3020
+ value: `${translateYPeak}px`,
2963
3021
  });
2964
3022
  });
2965
3023
  }
@@ -2968,7 +3026,7 @@ class AmLyrics extends i {
2968
3026
  charSpans.forEach((span, charIndex) => {
2969
3027
  const startPct = parseFloat(span.dataset.wipeStart || '0');
2970
3028
  const durationPct = parseFloat(span.dataset.wipeDuration || '0');
2971
- const wipeDelay = syllableDurationMs * startPct;
3029
+ const wipeDelay = syllableDurationMs * startPct - elapsedTimeMs;
2972
3030
  const wipeDuration = syllableDurationMs * durationPct;
2973
3031
  const useStartAnimation = isFirstInContainer && charIndex === 0;
2974
3032
  let charWipeAnimation = 'wipe';
@@ -2984,9 +3042,9 @@ class AmLyrics extends i {
2984
3042
  animationParts.push(existingAnimation.split(',')[0].trim());
2985
3043
  }
2986
3044
  if (charIndex > 0) {
2987
- const arrivalTime = span.dataset.preWipeArrival
3045
+ const arrivalTime = (span.dataset.preWipeArrival
2988
3046
  ? parseFloat(span.dataset.preWipeArrival)
2989
- : wipeDelay;
3047
+ : syllableDurationMs * startPct) - elapsedTimeMs;
2990
3048
  const constantDuration = parseFloat(span.dataset.preWipeDuration || '100');
2991
3049
  const animDelay = arrivalTime - constantDuration;
2992
3050
  if (constantDuration > 0) {
@@ -3016,12 +3074,13 @@ class AmLyrics extends i {
3016
3074
  return;
3017
3075
  const currentWipeAnimation = isGap ? 'fade-gap' : wipeAnimation;
3018
3076
  // eslint-disable-next-line no-param-reassign
3019
- syllable.style.animation = `${currentWipeAnimation} ${visualDuration}ms ${isGap ? 'ease-out' : 'linear'} forwards`;
3077
+ syllable.style.animation = `${currentWipeAnimation} ${visualDuration}ms ${isGap ? 'ease-out' : 'linear'} ${-elapsedTimeMs}ms forwards`;
3020
3078
  }
3021
3079
  // --- WRITE PHASE ---
3022
3080
  classList.remove('pre-highlight');
3023
3081
  classList.add('highlight');
3024
3082
  for (const [span, animationString] of charAnimationsMap.entries()) {
3083
+ span.style.willChange = 'transform';
3025
3084
  span.style.animation = animationString;
3026
3085
  }
3027
3086
  // Apply style updates
@@ -3048,6 +3107,7 @@ class AmLyrics extends i {
3048
3107
  syllable.querySelectorAll('span.char').forEach(span => {
3049
3108
  const el = span;
3050
3109
  el.style.animation = '';
3110
+ el.style.willChange = '';
3051
3111
  el.style.transition = 'none';
3052
3112
  el.style.backgroundColor = 'var(--lyplus-text-secondary)';
3053
3113
  });
@@ -3061,6 +3121,7 @@ class AmLyrics extends i {
3061
3121
  const el = span;
3062
3122
  el.style.removeProperty('background-color');
3063
3123
  el.style.removeProperty('transition');
3124
+ el.style.removeProperty('will-change');
3064
3125
  });
3065
3126
  });
3066
3127
  }
@@ -3090,7 +3151,7 @@ class AmLyrics extends i {
3090
3151
  const syllable = syllables[i];
3091
3152
  const startTime = parseFloat(syllable.getAttribute('data-start-time') || '0');
3092
3153
  const endTime = parseFloat(syllable.getAttribute('data-end-time') || '0');
3093
- if (startTime) {
3154
+ if (Number.isFinite(startTime) && Number.isFinite(endTime)) {
3094
3155
  const { classList } = syllable;
3095
3156
  const hasHighlight = classList.contains('highlight');
3096
3157
  const hasFinished = classList.contains('finished');
@@ -3114,7 +3175,7 @@ class AmLyrics extends i {
3114
3175
  if (currentTimeMs >= startTime && currentTimeMs <= endTime) {
3115
3176
  // Currently active
3116
3177
  if (!hasHighlight) {
3117
- AmLyrics.updateSyllableAnimation(syllable);
3178
+ AmLyrics.updateSyllableAnimation(syllable, currentTimeMs - startTime);
3118
3179
  }
3119
3180
  if (hasFinished) {
3120
3181
  classList.remove('finished');
@@ -3124,7 +3185,7 @@ class AmLyrics extends i {
3124
3185
  // Finished
3125
3186
  if (!hasFinished) {
3126
3187
  if (!hasHighlight) {
3127
- AmLyrics.updateSyllableAnimation(syllable);
3188
+ AmLyrics.updateSyllableAnimation(syllable, currentTimeMs - startTime);
3128
3189
  }
3129
3190
  classList.add('finished');
3130
3191
  }
@@ -3376,7 +3437,6 @@ class AmLyrics extends i {
3376
3437
  }
3377
3438
  // Set both old internal CSS variables (for backward compatibility)
3378
3439
  // and new public CSS variables (which take precedence)
3379
- this.style.setProperty('--hover-background-color', this.hoverBackgroundColor);
3380
3440
  this.style.setProperty('--highlight-color', this.highlightColor);
3381
3441
  const sourceLabel = this.lyricsSource ?? 'Unavailable';
3382
3442
  const isUnsynced = this.cachedIsUnsynced;
@@ -3418,7 +3478,7 @@ class AmLyrics extends i {
3418
3478
  syllable.romanizedText &&
3419
3479
  syllable.romanizedText.trim() !== syllable.text.trim()
3420
3480
  ? b `<span
3421
- class="lyrics-syllable transliteration ${syllable.lineSynced
3481
+ class="lyrics-syllable transliteration no-chars ${syllable.lineSynced
3422
3482
  ? 'line-synced'
3423
3483
  : ''}"
3424
3484
  data-start-time="${startTimeMs}"
@@ -3429,21 +3489,24 @@ class AmLyrics extends i {
3429
3489
  >${syllable.romanizedText}</span
3430
3490
  >`
3431
3491
  : '';
3432
- return b `<span class="lyrics-word">
3433
- <span class="lyrics-syllable-wrap">
3434
- <span
3435
- class="lyrics-syllable ${syllable.lineSynced
3436
- ? 'line-synced'
3492
+ return b `<span class="lyrics-word"
3493
+ ><span
3494
+ class="lyrics-syllable-wrap${bgRomanizedText
3495
+ ? ' has-transliteration'
3496
+ : ''}"
3497
+ ><span
3498
+ class="lyrics-syllable no-chars${syllable.lineSynced
3499
+ ? ' line-synced'
3437
3500
  : ''}"
3438
3501
  data-start-time="${startTimeMs}"
3439
3502
  data-end-time="${endTimeMs}"
3440
3503
  data-duration="${durationMs}"
3441
3504
  data-syllable-index="${syllableIndex}"
3505
+ data-wipe-ratio="1"
3442
3506
  >${syllable.text}</span
3443
- >
3444
- ${bgRomanizedText}
3445
- </span>
3446
- </span>`;
3507
+ >${bgRomanizedText}</span
3508
+ ></span
3509
+ >`;
3447
3510
  })}
3448
3511
  </p>`
3449
3512
  : '';
@@ -3461,8 +3524,11 @@ class AmLyrics extends i {
3461
3524
  const vwFullText = lineData?.vwFullText ?? [];
3462
3525
  const vwFullDuration = lineData?.vwFullDuration ?? [];
3463
3526
  const vwCharOffset = lineData?.vwCharOffset ?? [];
3527
+ const lineIsRTL = lineData?.lineIsRTL ?? false;
3464
3528
  // Create main vocals using YouLyPlus syllable structure
3465
- const mainVocalElement = b `<p class="main-vocal-container">
3529
+ const mainVocalElement = b `<p
3530
+ class="main-vocal-container ${lineIsRTL ? 'rtl-text' : ''}"
3531
+ >
3466
3532
  ${wordGroups.map((group, groupIdx) => {
3467
3533
  const isGrowable = groupGrowable[groupIdx];
3468
3534
  const isGlowing = groupGlowing[groupIdx];
@@ -3472,12 +3538,21 @@ class AmLyrics extends i {
3472
3538
  const wordNumChars = wordText.length;
3473
3539
  const groupCharOffset = isGrowable ? vwCharOffset[groupIdx] : 0;
3474
3540
  let sylCharAccumulator = 0;
3541
+ const groupText = group.map(s => s.text).join('');
3542
+ const shouldAllowBreak = groupText.trim().length >= 16 ||
3543
+ /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(groupText);
3544
+ // Calculate dynamic rise duration based on the audio duration of the word
3545
+ const wordStartTimeMs = group[0].timestamp;
3546
+ const wordEndTimeMs = group[group.length - 1].endtime;
3547
+ const actualDurationMs = wordEndTimeMs - wordStartTimeMs;
3548
+ // Base float is 0.8s, plus a portion of the audio duration, capped between 1.0s and 2.5s
3549
+ const riseDuration = Math.max(1.2, Math.min(2.5, 1.2 + (actualDurationMs / 1000) * 0.6));
3475
3550
  return b `<span
3476
- class="lyrics-word ${isGrowable ? 'growable' : ''} ${isGlowing
3477
- ? 'glowing'
3478
- : ''} ${group.length > 1 ? 'allow-break' : ''}"
3479
- >
3480
- ${group.map((syllable, sylIdx) => {
3551
+ class="lyrics-word${isGrowable ? ' growable' : ''}${isGlowing
3552
+ ? ' glowing'
3553
+ : ''}${shouldAllowBreak ? ' allow-break' : ''}"
3554
+ style="--rise-duration: ${riseDuration}s"
3555
+ >${group.map((syllable, sylIdx) => {
3481
3556
  const startTimeMs = syllable.timestamp;
3482
3557
  const endTimeMs = syllable.endtime;
3483
3558
  const durationMs = endTimeMs - startTimeMs;
@@ -3486,7 +3561,7 @@ class AmLyrics extends i {
3486
3561
  syllable.romanizedText &&
3487
3562
  syllable.romanizedText.trim() !== syllable.text.trim()
3488
3563
  ? b `<span
3489
- class="lyrics-syllable transliteration ${groupLineSynced
3564
+ class="lyrics-syllable transliteration no-chars ${groupLineSynced
3490
3565
  ? 'line-synced'
3491
3566
  : ''}"
3492
3567
  data-start-time="${startTimeMs}"
@@ -3563,17 +3638,22 @@ class AmLyrics extends i {
3563
3638
  data-wipe-duration="${(1 / numCharsInSyllable).toFixed(4)}"
3564
3639
  data-horizontal-offset="${horizontalOffset.toFixed(2)}"
3565
3640
  data-max-scale="${charMaxScale.toFixed(3)}"
3641
+ data-matrix-scale="${(charMaxScale * 0.98).toFixed(3)}"
3642
+ data-char-offset-x="${(horizontalOffset * 0.98).toFixed(2)}"
3566
3643
  data-shadow-intensity="${charShadowIntensity.toFixed(3)}"
3567
3644
  data-translate-y-peak="${charTranslateYPeak.toFixed(3)}"
3568
3645
  >${char}</span
3569
3646
  >`;
3570
3647
  })}`;
3571
3648
  }
3572
- return b `<span class="lyrics-syllable-wrap">
3573
- <span
3574
- class="lyrics-syllable ${groupLineSynced
3575
- ? 'line-synced'
3649
+ return b `<span
3650
+ class="lyrics-syllable-wrap${romanizedText
3651
+ ? ' has-transliteration'
3576
3652
  : ''}"
3653
+ ><span
3654
+ class="lyrics-syllable${groupLineSynced
3655
+ ? ' line-synced'
3656
+ : ''}${isGrowable ? ' has-chars' : ' no-chars'}"
3577
3657
  data-start-time="${startTimeMs}"
3578
3658
  data-end-time="${endTimeMs}"
3579
3659
  data-duration="${durationMs}"
@@ -3581,11 +3661,10 @@ class AmLyrics extends i {
3581
3661
  data-syllable-index="${sylIdx}"
3582
3662
  data-wipe-ratio="1"
3583
3663
  >${syllableContent}</span
3584
- >
3585
- ${romanizedText}
3586
- </span>`;
3587
- })}
3588
- </span>`;
3664
+ >${romanizedText}</span
3665
+ >`;
3666
+ })}</span
3667
+ >`;
3589
3668
  })}
3590
3669
  </p>`;
3591
3670
  // Translation container (if enabled)
@@ -3607,7 +3686,11 @@ class AmLyrics extends i {
3607
3686
  line.romanizedText &&
3608
3687
  !line.text.some(s => s.romanizedText) &&
3609
3688
  line.romanizedText.trim() !== fullLineText
3610
- ? b `<div class="lyrics-romanization-container">
3689
+ ? b `<div
3690
+ class="lyrics-romanization-container ${lineIsRTL
3691
+ ? 'rtl-text'
3692
+ : ''}"
3693
+ >
3611
3694
  ${line.romanizedText}
3612
3695
  </div>`
3613
3696
  : '';
@@ -3627,42 +3710,37 @@ class AmLyrics extends i {
3627
3710
  data-end-time="${gapForLine.gapEnd}"
3628
3711
  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};"
3629
3712
  >
3630
- <div class="lyrics-line-container">
3631
- <p class="main-vocal-container">
3632
- <span class="lyrics-word">
3633
- <span class="lyrics-syllable-wrap">
3634
- <span
3635
- class="lyrics-syllable"
3636
- data-start-time="${gapForLine.gapStart}"
3637
- data-end-time="${gapForLine.gapStart + dotDuration}"
3638
- data-duration="${dotDuration}"
3639
- data-wipe-ratio="1"
3640
- data-syllable-index="0"
3641
- ></span>
3642
- </span>
3643
- <span class="lyrics-syllable-wrap">
3644
- <span
3645
- class="lyrics-syllable"
3646
- data-start-time="${gapForLine.gapStart + dotDuration}"
3647
- data-end-time="${gapForLine.gapStart + dotDuration * 2}"
3648
- data-duration="${dotDuration}"
3649
- data-wipe-ratio="1"
3650
- data-syllable-index="1"
3651
- ></span>
3652
- </span>
3653
- <span class="lyrics-syllable-wrap">
3654
- <span
3655
- class="lyrics-syllable"
3656
- data-start-time="${gapForLine.gapStart + dotDuration * 2}"
3657
- data-end-time="${gapForLine.gapEnd}"
3658
- data-duration="${dotDuration}"
3659
- data-wipe-ratio="1"
3660
- data-syllable-index="2"
3661
- ></span>
3662
- </span>
3663
- </span>
3664
- </p>
3665
- </div>
3713
+ <p class="main-vocal-container">
3714
+ <span class="lyrics-word"
3715
+ ><span class="lyrics-syllable-wrap"
3716
+ ><span
3717
+ class="lyrics-syllable"
3718
+ data-start-time="${gapForLine.gapStart}"
3719
+ data-end-time="${gapForLine.gapStart + dotDuration}"
3720
+ data-duration="${dotDuration}"
3721
+ data-wipe-ratio="1"
3722
+ data-syllable-index="0"
3723
+ ></span></span
3724
+ ><span class="lyrics-syllable-wrap"
3725
+ ><span
3726
+ class="lyrics-syllable"
3727
+ data-start-time="${gapForLine.gapStart + dotDuration}"
3728
+ data-end-time="${gapForLine.gapStart + dotDuration * 2}"
3729
+ data-duration="${dotDuration}"
3730
+ data-wipe-ratio="1"
3731
+ data-syllable-index="1"
3732
+ ></span></span
3733
+ ><span class="lyrics-syllable-wrap"
3734
+ ><span
3735
+ class="lyrics-syllable"
3736
+ data-start-time="${gapForLine.gapStart + dotDuration * 2}"
3737
+ data-end-time="${gapForLine.gapEnd}"
3738
+ data-duration="${dotDuration}"
3739
+ data-wipe-ratio="1"
3740
+ data-syllable-index="2"
3741
+ ></span></span
3742
+ ></span>
3743
+ </p>
3666
3744
  </div>`;
3667
3745
  }
3668
3746
  return b `
@@ -3671,7 +3749,7 @@ class AmLyrics extends i {
3671
3749
  id="${lineId}"
3672
3750
  class="lyrics-line ${line.alignment === 'end'
3673
3751
  ? 'singer-right'
3674
- : 'singer-left'}"
3752
+ : 'singer-left'} ${lineIsRTL ? 'rtl-text' : ''}"
3675
3753
  data-start-time="${lineStartTime}"
3676
3754
  data-end-time="${lineEndTime}"
3677
3755
  @click=${() => this.handleLineClick(line)}
@@ -3682,11 +3760,11 @@ class AmLyrics extends i {
3682
3760
  }
3683
3761
  }}
3684
3762
  >
3685
- <div class="lyrics-line-container">
3763
+ <div class="lyrics-line-container ${lineIsRTL ? 'rtl-text' : ''}">
3686
3764
  ${bgPlacement === 'before' ? backgroundVocalElement : ''}
3687
3765
  ${mainVocalElement}
3688
3766
  ${bgPlacement === 'after' ? backgroundVocalElement : ''}
3689
- ${translationElement} ${lineRomanizationElement}
3767
+ ${lineRomanizationElement} ${translationElement}
3690
3768
  </div>
3691
3769
  </div>
3692
3770
  `;
@@ -3799,13 +3877,13 @@ class AmLyrics extends i {
3799
3877
  ${renderContent()}
3800
3878
  ${!this.isLoading
3801
3879
  ? b `
3802
- <footer class="lyrics-footer">
3880
+ <footer class="lyrics-footer lyrics-line">
3803
3881
  <div class="footer-content">
3804
3882
  <span
3805
3883
  class="source-info"
3806
3884
  style="display: flex; align-items: center; gap: 8px;"
3807
3885
  >
3808
- Source: ${sourceLabel}
3886
+ <b style="font-weight: 750;">Source</b> ${sourceLabel}
3809
3887
  ${(this.availableSources &&
3810
3888
  this.availableSources.length > 1) ||
3811
3889
  !this.hasFetchedAllProviders
@@ -3848,15 +3926,25 @@ class AmLyrics extends i {
3848
3926
  `
3849
3927
  : ''}
3850
3928
  </span>
3851
- <span class="version-info">
3852
- v${VERSION}
3929
+ ${this.songwriters
3930
+ ? b `<span
3931
+ class="songwriters-info"
3932
+ style="margin-top: 4px; font-weight: normal; font-size: 0.9em;"
3933
+ >
3934
+ <b style="font-weight: 750;">Songwriters</b> ${this
3935
+ .songwriters}
3936
+ </span>`
3937
+ : ''}
3938
+ <span class="version-info" style="margin-top: 8px;">
3939
+ <b style="font-weight: 750;">am-lyrics</b> v${VERSION} •
3853
3940
 
3854
3941
  <a
3855
3942
  href="https://github.com/uimaxbai/apple-music-web-components"
3856
3943
  target="_blank"
3857
3944
  rel="noopener noreferrer"
3858
- >Star me on GitHub</a
3859
- >
3945
+ style="display: inline-flex; align-items: center; gap: 4px;"
3946
+ >Star me on GitHub
3947
+ </a>
3860
3948
  </span>
3861
3949
  </div>
3862
3950
  </footer>
@@ -3893,6 +3981,7 @@ AmLyrics.styles = i$3 `
3893
3981
  --lyplus-font-size-base: 32px;
3894
3982
  --lyplus-font-size-base-grow: 24.5;
3895
3983
  --lyplus-font-size-subtext: 0.6em;
3984
+ --char-rise-y: calc(-0.035 * var(--lyplus-font-size-base));
3896
3985
 
3897
3986
  --lyplus-blur-amount: 0.07em;
3898
3987
  --lyplus-blur-amount-near: 0.035em;
@@ -3926,7 +4015,6 @@ AmLyrics.styles = i$3 `
3926
4015
  -webkit-overflow-scrolling: touch;
3927
4016
  box-sizing: border-box;
3928
4017
  scrollbar-width: none;
3929
- transform: translateZ(0);
3930
4018
  }
3931
4019
 
3932
4020
  .lyrics-container::-webkit-scrollbar {
@@ -3937,11 +4025,13 @@ AmLyrics.styles = i$3 `
3937
4025
  .lyrics-container.touch-scrolling .lyrics-line,
3938
4026
  .lyrics-container.touch-scrolling .lyrics-plus-metadata {
3939
4027
  transition: none !important;
4028
+ filter: none !important;
3940
4029
  }
3941
4030
 
3942
4031
  /* Apply smooth gliding transition for mouse-wheel scrolling */
3943
4032
  .lyrics-container.wheel-scrolling .lyrics-line {
3944
4033
  transition: transform 0.3s ease-out !important;
4034
+ filter: none !important;
3945
4035
  }
3946
4036
 
3947
4037
  .lyrics-line.scroll-animate {
@@ -3968,18 +4058,13 @@ AmLyrics.styles = i$3 `
3968
4058
  font-size: var(--lyplus-font-size-base);
3969
4059
  cursor: pointer;
3970
4060
  transform-origin: left;
3971
- transform: translateZ(1px);
3972
4061
  transition:
3973
4062
  opacity 0.3s ease,
3974
4063
  transform 0.4s cubic-bezier(0.41, 0, 0.12, 0.99)
3975
4064
  var(--lyrics-line-delay, 0ms),
3976
4065
  filter 0.3s ease;
3977
- will-change: transform, filter, opacity;
3978
4066
  content-visibility: auto;
3979
4067
  text-rendering: optimizeLegibility;
3980
- overflow-wrap: break-word;
3981
- mix-blend-mode: lighten;
3982
- border-radius: var(--lyplus-border-radius-base);
3983
4068
  }
3984
4069
 
3985
4070
  .lyrics-line:not(.scroll-animate) {
@@ -3999,8 +4084,7 @@ AmLyrics.styles = i$3 `
3999
4084
 
4000
4085
  .lyrics-line.active .lyrics-line-container,
4001
4086
  .lyrics-line.pre-active .lyrics-line-container {
4002
- transform: scale3d(1.001, 1.001, 1);
4003
- will-change: transform;
4087
+ transform: scale3d(1.001, 1.001, 1) translateZ(0);
4004
4088
  transition:
4005
4089
  transform 0.5s ease,
4006
4090
  background-color 0.18s,
@@ -4045,12 +4129,10 @@ AmLyrics.styles = i$3 `
4045
4129
  .lyrics-line.active {
4046
4130
  opacity: 1;
4047
4131
  color: var(--lyplus-text-primary);
4048
- will-change: transform, opacity;
4049
4132
  }
4050
4133
 
4051
4134
  .lyrics-line.pre-active {
4052
4135
  opacity: 1;
4053
- will-change: transform, opacity;
4054
4136
  }
4055
4137
 
4056
4138
  .lyrics-line.singer-right {
@@ -4064,6 +4146,18 @@ AmLyrics.styles = i$3 `
4064
4146
 
4065
4147
  .lyrics-line.rtl-text {
4066
4148
  direction: rtl;
4149
+ text-align: right !important;
4150
+ transform-origin: right;
4151
+ }
4152
+
4153
+ .lyrics-line.rtl-text .lyrics-line-container,
4154
+ .lyrics-line.rtl-text .main-vocal-container {
4155
+ transform-origin: right;
4156
+ }
4157
+
4158
+ .lyrics-line.rtl-text .lyrics-romanization-container,
4159
+ .lyrics-line.rtl-text .lyrics-translation-container {
4160
+ text-align: right;
4067
4161
  }
4068
4162
 
4069
4163
  /* --- Unsynced (Plain Text) Lyrics Overrides --- */
@@ -4095,7 +4189,8 @@ AmLyrics.styles = i$3 `
4095
4189
 
4096
4190
  @media (hover: hover) and (pointer: fine) {
4097
4191
  .lyrics-line:hover {
4098
- background: var(--hover-background-color, rgba(255, 255, 255, 0.13));
4192
+ filter: none !important;
4193
+ opacity: 1 !important;
4099
4194
  }
4100
4195
  .lyrics-container.is-unsynced .lyrics-line:hover {
4101
4196
  background: transparent !important;
@@ -4125,6 +4220,7 @@ AmLyrics.styles = i$3 `
4125
4220
 
4126
4221
  /* Unblur all lines when user is scrolling */
4127
4222
  .lyrics-container.user-scrolling .lyrics-line {
4223
+ transition: none !important;
4128
4224
  filter: none !important;
4129
4225
  opacity: 0.8 !important;
4130
4226
  }
@@ -4141,6 +4237,7 @@ AmLyrics.styles = i$3 `
4141
4237
  .lyrics-word:not(.allow-break) {
4142
4238
  display: inline-block;
4143
4239
  vertical-align: baseline;
4240
+ white-space: nowrap;
4144
4241
  }
4145
4242
 
4146
4243
  .lyrics-word.allow-break {
@@ -4151,7 +4248,7 @@ AmLyrics.styles = i$3 `
4151
4248
  display: inline;
4152
4249
  }
4153
4250
 
4154
- .lyrics-syllable-wrap:has(.lyrics-syllable.transliteration) {
4251
+ .lyrics-syllable-wrap.has-transliteration {
4155
4252
  display: inline-flex;
4156
4253
  flex-direction: column;
4157
4254
  align-items: start;
@@ -4179,7 +4276,7 @@ AmLyrics.styles = i$3 `
4179
4276
  transition: transform 1s ease !important;
4180
4277
  }
4181
4278
 
4182
- .lyrics-syllable.finished:has(.char) {
4279
+ .lyrics-syllable.finished.has-chars {
4183
4280
  background-color: transparent;
4184
4281
  }
4185
4282
 
@@ -4188,19 +4285,16 @@ AmLyrics.styles = i$3 `
4188
4285
  }
4189
4286
 
4190
4287
  .lyrics-line.active:not(.lyrics-gap) .lyrics-syllable {
4191
- transform: translateY(0.001%) translateZ(1px);
4192
4288
  transition:
4193
4289
  transform 1s ease,
4194
4290
  background-color 0.5s,
4195
4291
  color 0.5s;
4196
- will-change: transform, background;
4197
4292
  }
4198
4293
 
4199
4294
  /* --- Wipe Highlight Effect --- */
4295
+ .lyrics-line.active:not(.lyrics-gap) .lyrics-syllable.highlight.no-chars,
4200
4296
  .lyrics-line.active:not(.lyrics-gap)
4201
- .lyrics-syllable.highlight:not(:has(.char)),
4202
- .lyrics-line.active:not(.lyrics-gap)
4203
- .lyrics-syllable.pre-highlight:not(:has(.char)) {
4297
+ .lyrics-syllable.pre-highlight.no-chars {
4204
4298
  background-repeat: no-repeat;
4205
4299
  background-image:
4206
4300
  linear-gradient(
@@ -4242,11 +4336,19 @@ AmLyrics.styles = i$3 `
4242
4336
  right;
4243
4337
  }
4244
4338
 
4339
+ /* Non-growable words float up with a gentle curve */
4245
4340
  .lyrics-line.active:not(.lyrics-gap)
4246
4341
  .lyrics-word:not(.growable)
4247
- .lyrics-syllable.highlight,
4342
+ .lyrics-syllable.highlight {
4343
+ transform: translateY(-3.5%);
4344
+ transition:
4345
+ transform var(--rise-duration, 1.5s) cubic-bezier(0.22, 1, 0.36, 1),
4346
+ background-color 0.5s,
4347
+ color 0.5s;
4348
+ }
4349
+
4248
4350
  .lyrics-word.growable .lyrics-syllable.cleanup .char {
4249
- transform: translateY(-3.5%) translateZ(1px);
4351
+ transform: translateY(-3.5%);
4250
4352
  }
4251
4353
 
4252
4354
  .lyrics-line.active:not(.lyrics-gap) .lyrics-syllable.highlight.finished {
@@ -4273,7 +4375,7 @@ AmLyrics.styles = i$3 `
4273
4375
  }
4274
4376
 
4275
4377
  /* Syllable with chars: make syllable transparent, chars handle color */
4276
- .lyrics-line .lyrics-syllable:has(span.char):not(.finished) {
4378
+ .lyrics-line .lyrics-syllable.has-chars:not(.finished) {
4277
4379
  background-color: transparent;
4278
4380
  color: transparent;
4279
4381
  }
@@ -4286,6 +4388,7 @@ AmLyrics.styles = i$3 `
4286
4388
  font-feature-settings: 'liga' 0;
4287
4389
  background-clip: text;
4288
4390
  -webkit-background-clip: text;
4391
+ backface-visibility: hidden;
4289
4392
  transition:
4290
4393
  color 0.7s,
4291
4394
  background-color 0.7s,
@@ -4321,11 +4424,9 @@ AmLyrics.styles = i$3 `
4321
4424
  -0.5em 0%,
4322
4425
  -0.25em 0%;
4323
4426
  transform-origin: 50% 80%;
4324
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
4325
4427
  transition:
4326
4428
  transform 0.7s ease,
4327
4429
  color 0.18s;
4328
- will-change: background, transform;
4329
4430
  }
4330
4431
 
4331
4432
  .lyrics-line.active .lyrics-syllable span.char.highlight {
@@ -4377,6 +4478,8 @@ AmLyrics.styles = i$3 `
4377
4478
  box-sizing: content-box;
4378
4479
  background-clip: unset;
4379
4480
  transform-origin: top;
4481
+ content-visibility: visible !important;
4482
+ contain: none !important;
4380
4483
  transition:
4381
4484
  opacity 160ms ease-out,
4382
4485
  transform var(--scroll-duration, 280ms) var(--lyrics-line-delay, 0ms);
@@ -4387,41 +4490,35 @@ AmLyrics.styles = i$3 `
4387
4490
  transition:
4388
4491
  opacity 160ms ease-out,
4389
4492
  transform var(--scroll-duration, 280ms);
4390
- will-change: opacity;
4391
4493
  }
4392
4494
 
4393
4495
  /* Exiting state: quickly collapse width and height so dots don't distort page, or remove max-height transition */
4394
4496
  .lyrics-gap.gap-exiting {
4395
4497
  opacity: 1;
4396
- transition: transform var(--scroll-duration, 280ms);
4397
4498
  }
4398
4499
 
4399
4500
  .lyrics-gap .main-vocal-container {
4400
- transform: translateY(-25%) scale(1) translateZ(0);
4501
+ transform: translateY(-25%) scale(1);
4401
4502
  transition: transform 400ms cubic-bezier(0.22, 1, 0.36, 1);
4402
4503
  }
4403
4504
 
4404
- /* Jump animation plays during exit */
4405
- .lyrics-gap.gap-exiting .main-vocal-container {
4406
- animation: gap-ended var(--gap-exit-duration, 360ms)
4407
- cubic-bezier(0.33, 1, 0.68, 1) forwards;
4408
- }
4409
-
4410
4505
  .lyrics-gap:not(.active):not(.gap-exiting) .main-vocal-container {
4411
- transform: translateY(-25%) scale(0) translateZ(0);
4412
- }
4413
-
4414
- .lyrics-gap:not(.active):not(.gap-exiting)
4415
- .main-vocal-container
4416
- .lyrics-word {
4417
- animation-play-state: paused;
4506
+ transform: translateY(-25%) scale(0);
4418
4507
  }
4419
4508
 
4420
- .lyrics-gap.active .main-vocal-container .lyrics-word {
4509
+ /* Pulse — must come BEFORE .gap-exiting so exiting wins via specificity+order */
4510
+ .lyrics-gap.active .main-vocal-container {
4421
4511
  animation: gap-loop var(--gap-pulse-duration, 4000ms) ease-in-out infinite
4422
4512
  alternate;
4423
4513
  animation-delay: var(--gap-loop-delay, 0ms);
4424
- will-change: transform;
4514
+ }
4515
+
4516
+ /* Jump animation plays during exit — disable transition so animation wins.
4517
+ Placed AFTER .active so it wins when both classes are present briefly. */
4518
+ .lyrics-gap.gap-exiting .main-vocal-container {
4519
+ animation: gap-ended var(--gap-exit-duration, 360ms)
4520
+ cubic-bezier(0.33, 1, 0.68, 1) forwards;
4521
+ transition: none !important;
4425
4522
  }
4426
4523
 
4427
4524
  .lyrics-gap .lyrics-syllable {
@@ -4472,20 +4569,17 @@ AmLyrics.styles = i$3 `
4472
4569
  background-clip: unset;
4473
4570
  }
4474
4571
 
4475
- .lyrics-gap.active .lyrics-syllable.highlight,
4476
4572
  .lyrics-gap.active .lyrics-syllable.finished,
4477
- .lyrics-gap.gap-exiting .lyrics-syllable,
4478
- .lyrics-gap:not(.active).post-active-line .lyrics-syllable,
4479
- .lyrics-gap:not(.active).lyrics-activest .lyrics-syllable {
4573
+ .lyrics-gap.gap-exiting .lyrics-syllable.finished,
4574
+ .lyrics-gap:not(.active):not(.gap-exiting).post-active-line
4575
+ .lyrics-syllable,
4576
+ .lyrics-gap:not(.active):not(.gap-exiting).lyrics-activest
4577
+ .lyrics-syllable {
4480
4578
  background-color: var(--lyplus-text-primary);
4481
4579
  animation: none !important;
4482
4580
  opacity: 1;
4483
4581
  }
4484
4582
 
4485
- .lyrics-gap.active .lyrics-syllable.finished {
4486
- animation: none !important;
4487
- }
4488
-
4489
4583
  /* ==========================================================================
4490
4584
  METADATA & FOOTER STYLES
4491
4585
  ========================================================================== */
@@ -4514,12 +4608,49 @@ AmLyrics.styles = i$3 `
4514
4608
  align-items: center;
4515
4609
  flex-wrap: wrap;
4516
4610
  text-align: left;
4517
- font-size: 0.8em;
4518
- color: rgba(255, 255, 255, 0.5);
4519
- padding: 10px 0;
4520
- border-top: 1px solid rgba(255, 255, 255, 0.1);
4611
+ font-size: 1.2em;
4612
+ color: rgba(255, 255, 255, 0.6);
4613
+ padding: 20px 0 50vh 0;
4521
4614
  margin-top: 10px;
4522
- font-weight: normal;
4615
+ font-weight: 400;
4616
+ opacity: 0.8;
4617
+ transition:
4618
+ opacity 0.3s ease,
4619
+ transform 0.5s cubic-bezier(0.41, 0, 0.12, 0.99),
4620
+ filter 0.3s ease;
4621
+ transform-origin: left;
4622
+ }
4623
+
4624
+ .lyrics-footer.lyrics-line {
4625
+ font-size: 1.2em;
4626
+ padding: 20px var(--lyplus-padding-line) 50vh var(--lyplus-padding-line);
4627
+ cursor: default;
4628
+ }
4629
+
4630
+ .lyrics-footer.active {
4631
+ opacity: 1;
4632
+ color: rgba(255, 255, 255, 0.5); /* Grey instead of primary */
4633
+ }
4634
+
4635
+ .lyrics-footer.scroll-animate {
4636
+ transition: none !important;
4637
+ animation-name: lyrics-scroll;
4638
+ animation-duration: var(--scroll-duration, 280ms);
4639
+ animation-timing-function: cubic-bezier(0.41, 0, 0.12, 0.99);
4640
+ animation-fill-mode: both;
4641
+ animation-delay: var(--lyrics-line-delay, 0ms);
4642
+ }
4643
+
4644
+ .lyrics-container.blur-inactive-enabled:not(.not-focused)
4645
+ .lyrics-footer:not(.active) {
4646
+ filter: blur(var(--lyplus-blur-amount));
4647
+ opacity: 0.5;
4648
+ }
4649
+
4650
+ .lyrics-container.user-scrolling .lyrics-footer {
4651
+ transition: none !important;
4652
+ filter: none !important;
4653
+ opacity: 0.8 !important;
4523
4654
  }
4524
4655
 
4525
4656
  .lyrics-footer p {
@@ -4527,12 +4658,14 @@ AmLyrics.styles = i$3 `
4527
4658
  }
4528
4659
 
4529
4660
  .lyrics-footer a {
4530
- color: rgba(255, 255, 255, 0.7);
4531
- text-decoration: none;
4661
+ color: var(--lyplus-text-primary); /* Stand out using primary color */
4662
+ text-underline-offset: 2px;
4663
+ opacity: 0.8;
4664
+ transition: opacity 0.2s;
4532
4665
  }
4533
4666
 
4534
4667
  .lyrics-footer a:hover {
4535
- text-decoration: underline;
4668
+ opacity: 1;
4536
4669
  }
4537
4670
 
4538
4671
  .footer-content {
@@ -4656,6 +4789,7 @@ AmLyrics.styles = i$3 `
4656
4789
 
4657
4790
  .lyrics-romanization-container.rtl-text {
4658
4791
  direction: rtl !important;
4792
+ text-align: right;
4659
4793
  }
4660
4794
 
4661
4795
  .lyrics-romanization-container .lyrics-syllable {
@@ -4869,23 +5003,22 @@ AmLyrics.styles = i$3 `
4869
5003
  /* Gap dot animations */
4870
5004
  @keyframes gap-loop {
4871
5005
  from {
4872
- transform: scale(1.12);
5006
+ transform: translateY(-25%) scale(1.12);
4873
5007
  }
4874
5008
  to {
4875
- transform: scale(var(--gap-exit-scale, 0.85));
5009
+ transform: translateY(-25%) scale(var(--gap-exit-scale, 0.85));
4876
5010
  }
4877
5011
  }
4878
5012
 
4879
5013
  @keyframes gap-ended {
4880
5014
  0% {
4881
- transform: translateY(-25%) scale(var(--gap-exit-scale, 0.85))
4882
- translateZ(0);
5015
+ transform: translateY(-25%) scale(var(--gap-exit-scale, 0.85));
4883
5016
  }
4884
5017
  35% {
4885
- transform: translateY(-5%) scale(1.08) translateZ(0);
5018
+ transform: translateY(-25%) scale(1.2);
4886
5019
  }
4887
5020
  100% {
4888
- transform: translateY(-25%) scale(0) translateZ(0);
5021
+ transform: translateY(-25%) scale(0);
4889
5022
  }
4890
5023
  }
4891
5024
 
@@ -4902,17 +5035,18 @@ AmLyrics.styles = i$3 `
4902
5035
  reflow in between) to reliably restart the animation each time */
4903
5036
  @keyframes lyrics-scroll {
4904
5037
  from {
4905
- transform: translateY(var(--scroll-delta)) translateZ(1px);
5038
+ transform: translate3d(0, var(--scroll-delta), 0);
4906
5039
  }
4907
5040
  to {
4908
- transform: translateY(0) translateZ(1px);
5041
+ transform: translate3d(0, 0, 0);
4909
5042
  }
4910
5043
  }
4911
5044
 
4912
- /* Character grow animation - exact copy from YouLyPlus */
5045
+ /* Character grow animation translate3d+scale3d for smooth transform,
5046
+ drop-shadow for glow (text-shadow doesn't work with background-clip:text) */
4913
5047
  @keyframes grow-dynamic {
4914
5048
  0% {
4915
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
5049
+ transform: translate3d(0, 0, 0) scale3d(1, 1, 1);
4916
5050
  filter: drop-shadow(
4917
5051
  0 0 0
4918
5052
  color-mix(in srgb, var(--lyplus-lyrics-palette), transparent 100%)
@@ -4920,27 +5054,12 @@ AmLyrics.styles = i$3 `
4920
5054
  }
4921
5055
  25%,
4922
5056
  30% {
4923
- transform: matrix3d(
4924
- calc(var(--max-scale) * calc(var(--lyplus-font-size-base-grow) / 25)),
4925
- 0,
4926
- 0,
4927
- 0,
4928
- 0,
4929
- calc(var(--max-scale) * calc(var(--lyplus-font-size-base-grow) / 25)),
4930
- 0,
4931
- 0,
4932
- 0,
4933
- 0,
4934
- 1,
4935
- 0,
4936
- calc(
4937
- var(--char-offset-x, 0) *
4938
- calc(var(--lyplus-font-size-base-grow) / 25)
4939
- ),
4940
- var(--translate-y-peak, -2),
4941
- 0,
4942
- 1
4943
- );
5057
+ transform: translate3d(
5058
+ var(--char-offset-x, 0px),
5059
+ var(--translate-y-peak, -2px),
5060
+ 0
5061
+ )
5062
+ scale3d(var(--matrix-scale, 1.1), var(--matrix-scale, 1.1), 1);
4944
5063
  filter: drop-shadow(
4945
5064
  0 0 0.1em
4946
5065
  color-mix(
@@ -4950,8 +5069,10 @@ AmLyrics.styles = i$3 `
4950
5069
  )
4951
5070
  );
4952
5071
  }
5072
+ 75%,
4953
5073
  100% {
4954
- transform: translateY(-3.5%) translateZ(1px);
5074
+ transform: translate3d(0, var(--char-rise-y, -1.12px), 0)
5075
+ scale3d(1, 1, 1);
4955
5076
  filter: drop-shadow(
4956
5077
  0 0 0
4957
5078
  color-mix(in srgb, var(--lyplus-lyrics-palette), transparent 100%)
@@ -5083,15 +5204,15 @@ __decorate([
5083
5204
  __decorate([
5084
5205
  n({ type: String, attribute: 'song-album' })
5085
5206
  ], AmLyrics.prototype, "songAlbum", void 0);
5207
+ __decorate([
5208
+ n({ type: String, attribute: 'songwriters' })
5209
+ ], AmLyrics.prototype, "songwriters", void 0);
5086
5210
  __decorate([
5087
5211
  n({ type: Number, attribute: 'song-duration' })
5088
5212
  ], AmLyrics.prototype, "songDurationMs", void 0);
5089
5213
  __decorate([
5090
5214
  n({ type: String, attribute: 'highlight-color' })
5091
5215
  ], AmLyrics.prototype, "highlightColor", void 0);
5092
- __decorate([
5093
- n({ type: String, attribute: 'hover-background-color' })
5094
- ], AmLyrics.prototype, "hoverBackgroundColor", void 0);
5095
5216
  __decorate([
5096
5217
  n({ type: String, attribute: 'font-family' })
5097
5218
  ], AmLyrics.prototype, "fontFamily", void 0);