@uimaxbai/am-lyrics 1.3.0 → 1.4.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.
@@ -319,7 +319,7 @@ class GoogleService {
319
319
  }
320
320
  }
321
321
 
322
- const VERSION = '1.3.0';
322
+ const VERSION = '1.4.0';
323
323
  const INSTRUMENTAL_THRESHOLD_MS = 7000; // Show dots for gaps >= 7s
324
324
  const FETCH_TIMEOUT_MS = 8000; // Timeout for all lyrics fetch requests
325
325
  const SEEK_THRESHOLD_MS = 500;
@@ -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
  }
@@ -2882,7 +2939,7 @@ class AmLyrics extends i {
2882
2939
  return;
2883
2940
  }
2884
2941
  // Skip scroll if near the bottom of content (prevents footer jitter)
2885
- if (!forceScroll) {
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
2945
  if (atBottom) {
@@ -2906,7 +2963,7 @@ class AmLyrics extends i {
2906
2963
  * Update syllable highlight animation - apply CSS wipe animation
2907
2964
  * (Exact copy from YouLyPlus _updateSyllableAnimation)
2908
2965
  */
2909
- static updateSyllableAnimation(syllable) {
2966
+ static updateSyllableAnimation(syllable, elapsedTimeMs = 0) {
2910
2967
  if (syllable.classList.contains('highlight'))
2911
2968
  return;
2912
2969
  const { classList } = syllable;
@@ -2934,8 +2991,8 @@ class AmLyrics extends i {
2934
2991
  const baseDelayPerChar = finalDuration * 0.09;
2935
2992
  const growDurationMs = finalDuration * 1.5;
2936
2993
  allWordCharSpans.forEach(span => {
2937
- const horizontalOffset = parseFloat(span.dataset.horizontalOffset || '0');
2938
- const maxScale = span.dataset.maxScale || '1.1';
2994
+ const matrixScale = span.dataset.matrixScale || '1.1';
2995
+ const charOffsetX = span.dataset.charOffsetX || '0';
2939
2996
  const shadowIntensity = span.dataset.shadowIntensity || '0.6';
2940
2997
  const translateYPeak = span.dataset.translateYPeak || '-2';
2941
2998
  const syllableCharIndex = parseFloat(span.dataset.syllableCharIndex || '0');
@@ -2943,13 +3000,13 @@ class AmLyrics extends i {
2943
3000
  charAnimationsMap.set(span, `grow-dynamic ${growDurationMs}ms ease-in-out ${growDelay}ms forwards`);
2944
3001
  styleUpdates.push({
2945
3002
  element: span,
2946
- property: '--char-offset-x',
2947
- value: `${horizontalOffset}`,
3003
+ property: '--matrix-scale',
3004
+ value: matrixScale,
2948
3005
  });
2949
3006
  styleUpdates.push({
2950
3007
  element: span,
2951
- property: '--max-scale',
2952
- value: maxScale,
3008
+ property: '--char-offset-x',
3009
+ value: `${charOffsetX}px`,
2953
3010
  });
2954
3011
  styleUpdates.push({
2955
3012
  element: span,
@@ -2959,7 +3016,7 @@ class AmLyrics extends i {
2959
3016
  styleUpdates.push({
2960
3017
  element: span,
2961
3018
  property: '--translate-y-peak',
2962
- value: `${translateYPeak}`,
3019
+ value: `${translateYPeak}px`,
2963
3020
  });
2964
3021
  });
2965
3022
  }
@@ -2968,7 +3025,7 @@ class AmLyrics extends i {
2968
3025
  charSpans.forEach((span, charIndex) => {
2969
3026
  const startPct = parseFloat(span.dataset.wipeStart || '0');
2970
3027
  const durationPct = parseFloat(span.dataset.wipeDuration || '0');
2971
- const wipeDelay = syllableDurationMs * startPct;
3028
+ const wipeDelay = syllableDurationMs * startPct - elapsedTimeMs;
2972
3029
  const wipeDuration = syllableDurationMs * durationPct;
2973
3030
  const useStartAnimation = isFirstInContainer && charIndex === 0;
2974
3031
  let charWipeAnimation = 'wipe';
@@ -2984,9 +3041,9 @@ class AmLyrics extends i {
2984
3041
  animationParts.push(existingAnimation.split(',')[0].trim());
2985
3042
  }
2986
3043
  if (charIndex > 0) {
2987
- const arrivalTime = span.dataset.preWipeArrival
3044
+ const arrivalTime = (span.dataset.preWipeArrival
2988
3045
  ? parseFloat(span.dataset.preWipeArrival)
2989
- : wipeDelay;
3046
+ : syllableDurationMs * startPct) - elapsedTimeMs;
2990
3047
  const constantDuration = parseFloat(span.dataset.preWipeDuration || '100');
2991
3048
  const animDelay = arrivalTime - constantDuration;
2992
3049
  if (constantDuration > 0) {
@@ -3016,12 +3073,13 @@ class AmLyrics extends i {
3016
3073
  return;
3017
3074
  const currentWipeAnimation = isGap ? 'fade-gap' : wipeAnimation;
3018
3075
  // eslint-disable-next-line no-param-reassign
3019
- syllable.style.animation = `${currentWipeAnimation} ${visualDuration}ms ${isGap ? 'ease-out' : 'linear'} forwards`;
3076
+ syllable.style.animation = `${currentWipeAnimation} ${visualDuration}ms ${isGap ? 'ease-out' : 'linear'} ${-elapsedTimeMs}ms forwards`;
3020
3077
  }
3021
3078
  // --- WRITE PHASE ---
3022
3079
  classList.remove('pre-highlight');
3023
3080
  classList.add('highlight');
3024
3081
  for (const [span, animationString] of charAnimationsMap.entries()) {
3082
+ span.style.willChange = 'transform';
3025
3083
  span.style.animation = animationString;
3026
3084
  }
3027
3085
  // Apply style updates
@@ -3048,6 +3106,7 @@ class AmLyrics extends i {
3048
3106
  syllable.querySelectorAll('span.char').forEach(span => {
3049
3107
  const el = span;
3050
3108
  el.style.animation = '';
3109
+ el.style.willChange = '';
3051
3110
  el.style.transition = 'none';
3052
3111
  el.style.backgroundColor = 'var(--lyplus-text-secondary)';
3053
3112
  });
@@ -3061,6 +3120,7 @@ class AmLyrics extends i {
3061
3120
  const el = span;
3062
3121
  el.style.removeProperty('background-color');
3063
3122
  el.style.removeProperty('transition');
3123
+ el.style.removeProperty('will-change');
3064
3124
  });
3065
3125
  });
3066
3126
  }
@@ -3090,7 +3150,7 @@ class AmLyrics extends i {
3090
3150
  const syllable = syllables[i];
3091
3151
  const startTime = parseFloat(syllable.getAttribute('data-start-time') || '0');
3092
3152
  const endTime = parseFloat(syllable.getAttribute('data-end-time') || '0');
3093
- if (startTime) {
3153
+ if (Number.isFinite(startTime) && Number.isFinite(endTime)) {
3094
3154
  const { classList } = syllable;
3095
3155
  const hasHighlight = classList.contains('highlight');
3096
3156
  const hasFinished = classList.contains('finished');
@@ -3114,7 +3174,7 @@ class AmLyrics extends i {
3114
3174
  if (currentTimeMs >= startTime && currentTimeMs <= endTime) {
3115
3175
  // Currently active
3116
3176
  if (!hasHighlight) {
3117
- AmLyrics.updateSyllableAnimation(syllable);
3177
+ AmLyrics.updateSyllableAnimation(syllable, currentTimeMs - startTime);
3118
3178
  }
3119
3179
  if (hasFinished) {
3120
3180
  classList.remove('finished');
@@ -3124,7 +3184,7 @@ class AmLyrics extends i {
3124
3184
  // Finished
3125
3185
  if (!hasFinished) {
3126
3186
  if (!hasHighlight) {
3127
- AmLyrics.updateSyllableAnimation(syllable);
3187
+ AmLyrics.updateSyllableAnimation(syllable, currentTimeMs - startTime);
3128
3188
  }
3129
3189
  classList.add('finished');
3130
3190
  }
@@ -3376,7 +3436,6 @@ class AmLyrics extends i {
3376
3436
  }
3377
3437
  // Set both old internal CSS variables (for backward compatibility)
3378
3438
  // and new public CSS variables (which take precedence)
3379
- this.style.setProperty('--hover-background-color', this.hoverBackgroundColor);
3380
3439
  this.style.setProperty('--highlight-color', this.highlightColor);
3381
3440
  const sourceLabel = this.lyricsSource ?? 'Unavailable';
3382
3441
  const isUnsynced = this.cachedIsUnsynced;
@@ -3429,21 +3488,24 @@ class AmLyrics extends i {
3429
3488
  >${syllable.romanizedText}</span
3430
3489
  >`
3431
3490
  : '';
3432
- return b `<span class="lyrics-word">
3433
- <span class="lyrics-syllable-wrap">
3434
- <span
3435
- class="lyrics-syllable ${syllable.lineSynced
3436
- ? 'line-synced'
3491
+ return b `<span class="lyrics-word"
3492
+ ><span
3493
+ class="lyrics-syllable-wrap${bgRomanizedText
3494
+ ? ' has-transliteration'
3495
+ : ''}"
3496
+ ><span
3497
+ class="lyrics-syllable no-chars${syllable.lineSynced
3498
+ ? ' line-synced'
3437
3499
  : ''}"
3438
3500
  data-start-time="${startTimeMs}"
3439
3501
  data-end-time="${endTimeMs}"
3440
3502
  data-duration="${durationMs}"
3441
3503
  data-syllable-index="${syllableIndex}"
3504
+ data-wipe-ratio="1"
3442
3505
  >${syllable.text}</span
3443
- >
3444
- ${bgRomanizedText}
3445
- </span>
3446
- </span>`;
3506
+ >${bgRomanizedText}</span
3507
+ ></span
3508
+ >`;
3447
3509
  })}
3448
3510
  </p>`
3449
3511
  : '';
@@ -3461,8 +3523,11 @@ class AmLyrics extends i {
3461
3523
  const vwFullText = lineData?.vwFullText ?? [];
3462
3524
  const vwFullDuration = lineData?.vwFullDuration ?? [];
3463
3525
  const vwCharOffset = lineData?.vwCharOffset ?? [];
3526
+ const lineIsRTL = lineData?.lineIsRTL ?? false;
3464
3527
  // Create main vocals using YouLyPlus syllable structure
3465
- const mainVocalElement = b `<p class="main-vocal-container">
3528
+ const mainVocalElement = b `<p
3529
+ class="main-vocal-container ${lineIsRTL ? 'rtl-text' : ''}"
3530
+ >
3466
3531
  ${wordGroups.map((group, groupIdx) => {
3467
3532
  const isGrowable = groupGrowable[groupIdx];
3468
3533
  const isGlowing = groupGlowing[groupIdx];
@@ -3472,12 +3537,21 @@ class AmLyrics extends i {
3472
3537
  const wordNumChars = wordText.length;
3473
3538
  const groupCharOffset = isGrowable ? vwCharOffset[groupIdx] : 0;
3474
3539
  let sylCharAccumulator = 0;
3540
+ const groupText = group.map(s => s.text).join('');
3541
+ const shouldAllowBreak = groupText.trim().length >= 16 ||
3542
+ /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(groupText);
3543
+ // Calculate dynamic rise duration based on the audio duration of the word
3544
+ const wordStartTimeMs = group[0].timestamp;
3545
+ const wordEndTimeMs = group[group.length - 1].endtime;
3546
+ const actualDurationMs = wordEndTimeMs - wordStartTimeMs;
3547
+ // Base float is 0.8s, plus a portion of the audio duration, capped between 1.0s and 2.5s
3548
+ const riseDuration = Math.max(1.2, Math.min(2.5, 1.2 + (actualDurationMs / 1000) * 0.6));
3475
3549
  return b `<span
3476
- class="lyrics-word ${isGrowable ? 'growable' : ''} ${isGlowing
3477
- ? 'glowing'
3478
- : ''} ${group.length > 1 ? 'allow-break' : ''}"
3479
- >
3480
- ${group.map((syllable, sylIdx) => {
3550
+ class="lyrics-word${isGrowable ? ' growable' : ''}${isGlowing
3551
+ ? ' glowing'
3552
+ : ''}${shouldAllowBreak ? ' allow-break' : ''}"
3553
+ style="--rise-duration: ${riseDuration}s"
3554
+ >${group.map((syllable, sylIdx) => {
3481
3555
  const startTimeMs = syllable.timestamp;
3482
3556
  const endTimeMs = syllable.endtime;
3483
3557
  const durationMs = endTimeMs - startTimeMs;
@@ -3563,17 +3637,22 @@ class AmLyrics extends i {
3563
3637
  data-wipe-duration="${(1 / numCharsInSyllable).toFixed(4)}"
3564
3638
  data-horizontal-offset="${horizontalOffset.toFixed(2)}"
3565
3639
  data-max-scale="${charMaxScale.toFixed(3)}"
3640
+ data-matrix-scale="${(charMaxScale * 0.98).toFixed(3)}"
3641
+ data-char-offset-x="${(horizontalOffset * 0.98).toFixed(2)}"
3566
3642
  data-shadow-intensity="${charShadowIntensity.toFixed(3)}"
3567
3643
  data-translate-y-peak="${charTranslateYPeak.toFixed(3)}"
3568
3644
  >${char}</span
3569
3645
  >`;
3570
3646
  })}`;
3571
3647
  }
3572
- return b `<span class="lyrics-syllable-wrap">
3573
- <span
3574
- class="lyrics-syllable ${groupLineSynced
3575
- ? 'line-synced'
3648
+ return b `<span
3649
+ class="lyrics-syllable-wrap${romanizedText
3650
+ ? ' has-transliteration'
3576
3651
  : ''}"
3652
+ ><span
3653
+ class="lyrics-syllable${groupLineSynced
3654
+ ? ' line-synced'
3655
+ : ''}${isGrowable ? ' has-chars' : ' no-chars'}"
3577
3656
  data-start-time="${startTimeMs}"
3578
3657
  data-end-time="${endTimeMs}"
3579
3658
  data-duration="${durationMs}"
@@ -3581,11 +3660,10 @@ class AmLyrics extends i {
3581
3660
  data-syllable-index="${sylIdx}"
3582
3661
  data-wipe-ratio="1"
3583
3662
  >${syllableContent}</span
3584
- >
3585
- ${romanizedText}
3586
- </span>`;
3587
- })}
3588
- </span>`;
3663
+ >${romanizedText}</span
3664
+ >`;
3665
+ })}</span
3666
+ >`;
3589
3667
  })}
3590
3668
  </p>`;
3591
3669
  // Translation container (if enabled)
@@ -3607,7 +3685,11 @@ class AmLyrics extends i {
3607
3685
  line.romanizedText &&
3608
3686
  !line.text.some(s => s.romanizedText) &&
3609
3687
  line.romanizedText.trim() !== fullLineText
3610
- ? b `<div class="lyrics-romanization-container">
3688
+ ? b `<div
3689
+ class="lyrics-romanization-container ${lineIsRTL
3690
+ ? 'rtl-text'
3691
+ : ''}"
3692
+ >
3611
3693
  ${line.romanizedText}
3612
3694
  </div>`
3613
3695
  : '';
@@ -3627,42 +3709,37 @@ class AmLyrics extends i {
3627
3709
  data-end-time="${gapForLine.gapEnd}"
3628
3710
  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
3711
  >
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>
3712
+ <p class="main-vocal-container">
3713
+ <span class="lyrics-word"
3714
+ ><span class="lyrics-syllable-wrap"
3715
+ ><span
3716
+ class="lyrics-syllable"
3717
+ data-start-time="${gapForLine.gapStart}"
3718
+ data-end-time="${gapForLine.gapStart + dotDuration}"
3719
+ data-duration="${dotDuration}"
3720
+ data-wipe-ratio="1"
3721
+ data-syllable-index="0"
3722
+ ></span></span
3723
+ ><span class="lyrics-syllable-wrap"
3724
+ ><span
3725
+ class="lyrics-syllable"
3726
+ data-start-time="${gapForLine.gapStart + dotDuration}"
3727
+ data-end-time="${gapForLine.gapStart + dotDuration * 2}"
3728
+ data-duration="${dotDuration}"
3729
+ data-wipe-ratio="1"
3730
+ data-syllable-index="1"
3731
+ ></span></span
3732
+ ><span class="lyrics-syllable-wrap"
3733
+ ><span
3734
+ class="lyrics-syllable"
3735
+ data-start-time="${gapForLine.gapStart + dotDuration * 2}"
3736
+ data-end-time="${gapForLine.gapEnd}"
3737
+ data-duration="${dotDuration}"
3738
+ data-wipe-ratio="1"
3739
+ data-syllable-index="2"
3740
+ ></span></span
3741
+ ></span>
3742
+ </p>
3666
3743
  </div>`;
3667
3744
  }
3668
3745
  return b `
@@ -3671,7 +3748,7 @@ class AmLyrics extends i {
3671
3748
  id="${lineId}"
3672
3749
  class="lyrics-line ${line.alignment === 'end'
3673
3750
  ? 'singer-right'
3674
- : 'singer-left'}"
3751
+ : 'singer-left'} ${lineIsRTL ? 'rtl-text' : ''}"
3675
3752
  data-start-time="${lineStartTime}"
3676
3753
  data-end-time="${lineEndTime}"
3677
3754
  @click=${() => this.handleLineClick(line)}
@@ -3682,11 +3759,11 @@ class AmLyrics extends i {
3682
3759
  }
3683
3760
  }}
3684
3761
  >
3685
- <div class="lyrics-line-container">
3762
+ <div class="lyrics-line-container ${lineIsRTL ? 'rtl-text' : ''}">
3686
3763
  ${bgPlacement === 'before' ? backgroundVocalElement : ''}
3687
3764
  ${mainVocalElement}
3688
3765
  ${bgPlacement === 'after' ? backgroundVocalElement : ''}
3689
- ${translationElement} ${lineRomanizationElement}
3766
+ ${lineRomanizationElement} ${translationElement}
3690
3767
  </div>
3691
3768
  </div>
3692
3769
  `;
@@ -3799,13 +3876,13 @@ class AmLyrics extends i {
3799
3876
  ${renderContent()}
3800
3877
  ${!this.isLoading
3801
3878
  ? b `
3802
- <footer class="lyrics-footer">
3879
+ <footer class="lyrics-footer lyrics-line">
3803
3880
  <div class="footer-content">
3804
3881
  <span
3805
3882
  class="source-info"
3806
3883
  style="display: flex; align-items: center; gap: 8px;"
3807
3884
  >
3808
- Source: ${sourceLabel}
3885
+ <b style="font-weight: 750;">Source</b> ${sourceLabel}
3809
3886
  ${(this.availableSources &&
3810
3887
  this.availableSources.length > 1) ||
3811
3888
  !this.hasFetchedAllProviders
@@ -3848,15 +3925,25 @@ class AmLyrics extends i {
3848
3925
  `
3849
3926
  : ''}
3850
3927
  </span>
3851
- <span class="version-info">
3852
- v${VERSION}
3928
+ ${this.songwriters
3929
+ ? b `<span
3930
+ class="songwriters-info"
3931
+ style="margin-top: 4px; font-weight: normal; font-size: 0.9em;"
3932
+ >
3933
+ <b style="font-weight: 750;">Songwriters</b> ${this
3934
+ .songwriters}
3935
+ </span>`
3936
+ : ''}
3937
+ <span class="version-info" style="margin-top: 8px;">
3938
+ <b style="font-weight: 750;">am-lyrics</b> v${VERSION} •
3853
3939
 
3854
3940
  <a
3855
3941
  href="https://github.com/uimaxbai/apple-music-web-components"
3856
3942
  target="_blank"
3857
3943
  rel="noopener noreferrer"
3858
- >Star me on GitHub</a
3859
- >
3944
+ style="display: inline-flex; align-items: center; gap: 4px;"
3945
+ >Star me on GitHub
3946
+ </a>
3860
3947
  </span>
3861
3948
  </div>
3862
3949
  </footer>
@@ -3893,6 +3980,7 @@ AmLyrics.styles = i$3 `
3893
3980
  --lyplus-font-size-base: 32px;
3894
3981
  --lyplus-font-size-base-grow: 24.5;
3895
3982
  --lyplus-font-size-subtext: 0.6em;
3983
+ --char-rise-y: calc(-0.035 * var(--lyplus-font-size-base));
3896
3984
 
3897
3985
  --lyplus-blur-amount: 0.07em;
3898
3986
  --lyplus-blur-amount-near: 0.035em;
@@ -3926,7 +4014,6 @@ AmLyrics.styles = i$3 `
3926
4014
  -webkit-overflow-scrolling: touch;
3927
4015
  box-sizing: border-box;
3928
4016
  scrollbar-width: none;
3929
- transform: translateZ(0);
3930
4017
  }
3931
4018
 
3932
4019
  .lyrics-container::-webkit-scrollbar {
@@ -3937,11 +4024,13 @@ AmLyrics.styles = i$3 `
3937
4024
  .lyrics-container.touch-scrolling .lyrics-line,
3938
4025
  .lyrics-container.touch-scrolling .lyrics-plus-metadata {
3939
4026
  transition: none !important;
4027
+ filter: none !important;
3940
4028
  }
3941
4029
 
3942
4030
  /* Apply smooth gliding transition for mouse-wheel scrolling */
3943
4031
  .lyrics-container.wheel-scrolling .lyrics-line {
3944
4032
  transition: transform 0.3s ease-out !important;
4033
+ filter: none !important;
3945
4034
  }
3946
4035
 
3947
4036
  .lyrics-line.scroll-animate {
@@ -3968,18 +4057,13 @@ AmLyrics.styles = i$3 `
3968
4057
  font-size: var(--lyplus-font-size-base);
3969
4058
  cursor: pointer;
3970
4059
  transform-origin: left;
3971
- transform: translateZ(1px);
3972
4060
  transition:
3973
4061
  opacity 0.3s ease,
3974
4062
  transform 0.4s cubic-bezier(0.41, 0, 0.12, 0.99)
3975
4063
  var(--lyrics-line-delay, 0ms),
3976
4064
  filter 0.3s ease;
3977
- will-change: transform, filter, opacity;
3978
4065
  content-visibility: auto;
3979
4066
  text-rendering: optimizeLegibility;
3980
- overflow-wrap: break-word;
3981
- mix-blend-mode: lighten;
3982
- border-radius: var(--lyplus-border-radius-base);
3983
4067
  }
3984
4068
 
3985
4069
  .lyrics-line:not(.scroll-animate) {
@@ -3999,8 +4083,7 @@ AmLyrics.styles = i$3 `
3999
4083
 
4000
4084
  .lyrics-line.active .lyrics-line-container,
4001
4085
  .lyrics-line.pre-active .lyrics-line-container {
4002
- transform: scale3d(1.001, 1.001, 1);
4003
- will-change: transform;
4086
+ transform: scale3d(1.001, 1.001, 1) translateZ(0);
4004
4087
  transition:
4005
4088
  transform 0.5s ease,
4006
4089
  background-color 0.18s,
@@ -4045,12 +4128,10 @@ AmLyrics.styles = i$3 `
4045
4128
  .lyrics-line.active {
4046
4129
  opacity: 1;
4047
4130
  color: var(--lyplus-text-primary);
4048
- will-change: transform, opacity;
4049
4131
  }
4050
4132
 
4051
4133
  .lyrics-line.pre-active {
4052
4134
  opacity: 1;
4053
- will-change: transform, opacity;
4054
4135
  }
4055
4136
 
4056
4137
  .lyrics-line.singer-right {
@@ -4064,6 +4145,18 @@ AmLyrics.styles = i$3 `
4064
4145
 
4065
4146
  .lyrics-line.rtl-text {
4066
4147
  direction: rtl;
4148
+ text-align: right !important;
4149
+ transform-origin: right;
4150
+ }
4151
+
4152
+ .lyrics-line.rtl-text .lyrics-line-container,
4153
+ .lyrics-line.rtl-text .main-vocal-container {
4154
+ transform-origin: right;
4155
+ }
4156
+
4157
+ .lyrics-line.rtl-text .lyrics-romanization-container,
4158
+ .lyrics-line.rtl-text .lyrics-translation-container {
4159
+ text-align: right;
4067
4160
  }
4068
4161
 
4069
4162
  /* --- Unsynced (Plain Text) Lyrics Overrides --- */
@@ -4095,7 +4188,8 @@ AmLyrics.styles = i$3 `
4095
4188
 
4096
4189
  @media (hover: hover) and (pointer: fine) {
4097
4190
  .lyrics-line:hover {
4098
- background: var(--hover-background-color, rgba(255, 255, 255, 0.13));
4191
+ filter: none !important;
4192
+ opacity: 1 !important;
4099
4193
  }
4100
4194
  .lyrics-container.is-unsynced .lyrics-line:hover {
4101
4195
  background: transparent !important;
@@ -4125,6 +4219,7 @@ AmLyrics.styles = i$3 `
4125
4219
 
4126
4220
  /* Unblur all lines when user is scrolling */
4127
4221
  .lyrics-container.user-scrolling .lyrics-line {
4222
+ transition: none !important;
4128
4223
  filter: none !important;
4129
4224
  opacity: 0.8 !important;
4130
4225
  }
@@ -4141,6 +4236,7 @@ AmLyrics.styles = i$3 `
4141
4236
  .lyrics-word:not(.allow-break) {
4142
4237
  display: inline-block;
4143
4238
  vertical-align: baseline;
4239
+ white-space: nowrap;
4144
4240
  }
4145
4241
 
4146
4242
  .lyrics-word.allow-break {
@@ -4151,7 +4247,7 @@ AmLyrics.styles = i$3 `
4151
4247
  display: inline;
4152
4248
  }
4153
4249
 
4154
- .lyrics-syllable-wrap:has(.lyrics-syllable.transliteration) {
4250
+ .lyrics-syllable-wrap.has-transliteration {
4155
4251
  display: inline-flex;
4156
4252
  flex-direction: column;
4157
4253
  align-items: start;
@@ -4179,7 +4275,7 @@ AmLyrics.styles = i$3 `
4179
4275
  transition: transform 1s ease !important;
4180
4276
  }
4181
4277
 
4182
- .lyrics-syllable.finished:has(.char) {
4278
+ .lyrics-syllable.finished.has-chars {
4183
4279
  background-color: transparent;
4184
4280
  }
4185
4281
 
@@ -4188,19 +4284,16 @@ AmLyrics.styles = i$3 `
4188
4284
  }
4189
4285
 
4190
4286
  .lyrics-line.active:not(.lyrics-gap) .lyrics-syllable {
4191
- transform: translateY(0.001%) translateZ(1px);
4192
4287
  transition:
4193
4288
  transform 1s ease,
4194
4289
  background-color 0.5s,
4195
4290
  color 0.5s;
4196
- will-change: transform, background;
4197
4291
  }
4198
4292
 
4199
4293
  /* --- Wipe Highlight Effect --- */
4294
+ .lyrics-line.active:not(.lyrics-gap) .lyrics-syllable.highlight.no-chars,
4200
4295
  .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)) {
4296
+ .lyrics-syllable.pre-highlight.no-chars {
4204
4297
  background-repeat: no-repeat;
4205
4298
  background-image:
4206
4299
  linear-gradient(
@@ -4242,11 +4335,19 @@ AmLyrics.styles = i$3 `
4242
4335
  right;
4243
4336
  }
4244
4337
 
4338
+ /* Non-growable words float up with a gentle curve */
4245
4339
  .lyrics-line.active:not(.lyrics-gap)
4246
4340
  .lyrics-word:not(.growable)
4247
- .lyrics-syllable.highlight,
4341
+ .lyrics-syllable.highlight {
4342
+ transform: translateY(-3.5%);
4343
+ transition:
4344
+ transform var(--rise-duration, 1.5s) cubic-bezier(0.22, 1, 0.36, 1),
4345
+ background-color 0.5s,
4346
+ color 0.5s;
4347
+ }
4348
+
4248
4349
  .lyrics-word.growable .lyrics-syllable.cleanup .char {
4249
- transform: translateY(-3.5%) translateZ(1px);
4350
+ transform: translateY(-3.5%);
4250
4351
  }
4251
4352
 
4252
4353
  .lyrics-line.active:not(.lyrics-gap) .lyrics-syllable.highlight.finished {
@@ -4273,7 +4374,7 @@ AmLyrics.styles = i$3 `
4273
4374
  }
4274
4375
 
4275
4376
  /* Syllable with chars: make syllable transparent, chars handle color */
4276
- .lyrics-line .lyrics-syllable:has(span.char):not(.finished) {
4377
+ .lyrics-line .lyrics-syllable.has-chars:not(.finished) {
4277
4378
  background-color: transparent;
4278
4379
  color: transparent;
4279
4380
  }
@@ -4286,6 +4387,7 @@ AmLyrics.styles = i$3 `
4286
4387
  font-feature-settings: 'liga' 0;
4287
4388
  background-clip: text;
4288
4389
  -webkit-background-clip: text;
4390
+ backface-visibility: hidden;
4289
4391
  transition:
4290
4392
  color 0.7s,
4291
4393
  background-color 0.7s,
@@ -4321,11 +4423,9 @@ AmLyrics.styles = i$3 `
4321
4423
  -0.5em 0%,
4322
4424
  -0.25em 0%;
4323
4425
  transform-origin: 50% 80%;
4324
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
4325
4426
  transition:
4326
4427
  transform 0.7s ease,
4327
4428
  color 0.18s;
4328
- will-change: background, transform;
4329
4429
  }
4330
4430
 
4331
4431
  .lyrics-line.active .lyrics-syllable span.char.highlight {
@@ -4377,6 +4477,8 @@ AmLyrics.styles = i$3 `
4377
4477
  box-sizing: content-box;
4378
4478
  background-clip: unset;
4379
4479
  transform-origin: top;
4480
+ content-visibility: visible !important;
4481
+ contain: none !important;
4380
4482
  transition:
4381
4483
  opacity 160ms ease-out,
4382
4484
  transform var(--scroll-duration, 280ms) var(--lyrics-line-delay, 0ms);
@@ -4387,41 +4489,35 @@ AmLyrics.styles = i$3 `
4387
4489
  transition:
4388
4490
  opacity 160ms ease-out,
4389
4491
  transform var(--scroll-duration, 280ms);
4390
- will-change: opacity;
4391
4492
  }
4392
4493
 
4393
4494
  /* Exiting state: quickly collapse width and height so dots don't distort page, or remove max-height transition */
4394
4495
  .lyrics-gap.gap-exiting {
4395
4496
  opacity: 1;
4396
- transition: transform var(--scroll-duration, 280ms);
4397
4497
  }
4398
4498
 
4399
4499
  .lyrics-gap .main-vocal-container {
4400
- transform: translateY(-25%) scale(1) translateZ(0);
4500
+ transform: translateY(-25%) scale(1);
4401
4501
  transition: transform 400ms cubic-bezier(0.22, 1, 0.36, 1);
4402
4502
  }
4403
4503
 
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
4504
  .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;
4505
+ transform: translateY(-25%) scale(0);
4418
4506
  }
4419
4507
 
4420
- .lyrics-gap.active .main-vocal-container .lyrics-word {
4508
+ /* Pulse — must come BEFORE .gap-exiting so exiting wins via specificity+order */
4509
+ .lyrics-gap.active .main-vocal-container {
4421
4510
  animation: gap-loop var(--gap-pulse-duration, 4000ms) ease-in-out infinite
4422
4511
  alternate;
4423
4512
  animation-delay: var(--gap-loop-delay, 0ms);
4424
- will-change: transform;
4513
+ }
4514
+
4515
+ /* Jump animation plays during exit — disable transition so animation wins.
4516
+ Placed AFTER .active so it wins when both classes are present briefly. */
4517
+ .lyrics-gap.gap-exiting .main-vocal-container {
4518
+ animation: gap-ended var(--gap-exit-duration, 360ms)
4519
+ cubic-bezier(0.33, 1, 0.68, 1) forwards;
4520
+ transition: none !important;
4425
4521
  }
4426
4522
 
4427
4523
  .lyrics-gap .lyrics-syllable {
@@ -4472,20 +4568,17 @@ AmLyrics.styles = i$3 `
4472
4568
  background-clip: unset;
4473
4569
  }
4474
4570
 
4475
- .lyrics-gap.active .lyrics-syllable.highlight,
4476
4571
  .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 {
4572
+ .lyrics-gap.gap-exiting .lyrics-syllable.finished,
4573
+ .lyrics-gap:not(.active):not(.gap-exiting).post-active-line
4574
+ .lyrics-syllable,
4575
+ .lyrics-gap:not(.active):not(.gap-exiting).lyrics-activest
4576
+ .lyrics-syllable {
4480
4577
  background-color: var(--lyplus-text-primary);
4481
4578
  animation: none !important;
4482
4579
  opacity: 1;
4483
4580
  }
4484
4581
 
4485
- .lyrics-gap.active .lyrics-syllable.finished {
4486
- animation: none !important;
4487
- }
4488
-
4489
4582
  /* ==========================================================================
4490
4583
  METADATA & FOOTER STYLES
4491
4584
  ========================================================================== */
@@ -4514,12 +4607,49 @@ AmLyrics.styles = i$3 `
4514
4607
  align-items: center;
4515
4608
  flex-wrap: wrap;
4516
4609
  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);
4610
+ font-size: 1.2em;
4611
+ color: rgba(255, 255, 255, 0.6);
4612
+ padding: 20px 0 50vh 0;
4521
4613
  margin-top: 10px;
4522
- font-weight: normal;
4614
+ font-weight: 400;
4615
+ opacity: 0.8;
4616
+ transition:
4617
+ opacity 0.3s ease,
4618
+ transform 0.5s cubic-bezier(0.41, 0, 0.12, 0.99),
4619
+ filter 0.3s ease;
4620
+ transform-origin: left;
4621
+ }
4622
+
4623
+ .lyrics-footer.lyrics-line {
4624
+ font-size: 1.2em;
4625
+ padding: 20px var(--lyplus-padding-line) 50vh var(--lyplus-padding-line);
4626
+ cursor: default;
4627
+ }
4628
+
4629
+ .lyrics-footer.active {
4630
+ opacity: 1;
4631
+ color: rgba(255, 255, 255, 0.5); /* Grey instead of primary */
4632
+ }
4633
+
4634
+ .lyrics-footer.scroll-animate {
4635
+ transition: none !important;
4636
+ animation-name: lyrics-scroll;
4637
+ animation-duration: var(--scroll-duration, 280ms);
4638
+ animation-timing-function: cubic-bezier(0.41, 0, 0.12, 0.99);
4639
+ animation-fill-mode: both;
4640
+ animation-delay: var(--lyrics-line-delay, 0ms);
4641
+ }
4642
+
4643
+ .lyrics-container.blur-inactive-enabled:not(.not-focused)
4644
+ .lyrics-footer:not(.active) {
4645
+ filter: blur(var(--lyplus-blur-amount));
4646
+ opacity: 0.5;
4647
+ }
4648
+
4649
+ .lyrics-container.user-scrolling .lyrics-footer {
4650
+ transition: none !important;
4651
+ filter: none !important;
4652
+ opacity: 0.8 !important;
4523
4653
  }
4524
4654
 
4525
4655
  .lyrics-footer p {
@@ -4527,12 +4657,14 @@ AmLyrics.styles = i$3 `
4527
4657
  }
4528
4658
 
4529
4659
  .lyrics-footer a {
4530
- color: rgba(255, 255, 255, 0.7);
4531
- text-decoration: none;
4660
+ color: var(--lyplus-text-primary); /* Stand out using primary color */
4661
+ text-underline-offset: 2px;
4662
+ opacity: 0.8;
4663
+ transition: opacity 0.2s;
4532
4664
  }
4533
4665
 
4534
4666
  .lyrics-footer a:hover {
4535
- text-decoration: underline;
4667
+ opacity: 1;
4536
4668
  }
4537
4669
 
4538
4670
  .footer-content {
@@ -4656,6 +4788,7 @@ AmLyrics.styles = i$3 `
4656
4788
 
4657
4789
  .lyrics-romanization-container.rtl-text {
4658
4790
  direction: rtl !important;
4791
+ text-align: right;
4659
4792
  }
4660
4793
 
4661
4794
  .lyrics-romanization-container .lyrics-syllable {
@@ -4869,23 +5002,22 @@ AmLyrics.styles = i$3 `
4869
5002
  /* Gap dot animations */
4870
5003
  @keyframes gap-loop {
4871
5004
  from {
4872
- transform: scale(1.12);
5005
+ transform: translateY(-25%) scale(1.12);
4873
5006
  }
4874
5007
  to {
4875
- transform: scale(var(--gap-exit-scale, 0.85));
5008
+ transform: translateY(-25%) scale(var(--gap-exit-scale, 0.85));
4876
5009
  }
4877
5010
  }
4878
5011
 
4879
5012
  @keyframes gap-ended {
4880
5013
  0% {
4881
- transform: translateY(-25%) scale(var(--gap-exit-scale, 0.85))
4882
- translateZ(0);
5014
+ transform: translateY(-25%) scale(var(--gap-exit-scale, 0.85));
4883
5015
  }
4884
5016
  35% {
4885
- transform: translateY(-5%) scale(1.08) translateZ(0);
5017
+ transform: translateY(-25%) scale(1.2);
4886
5018
  }
4887
5019
  100% {
4888
- transform: translateY(-25%) scale(0) translateZ(0);
5020
+ transform: translateY(-25%) scale(0);
4889
5021
  }
4890
5022
  }
4891
5023
 
@@ -4902,17 +5034,18 @@ AmLyrics.styles = i$3 `
4902
5034
  reflow in between) to reliably restart the animation each time */
4903
5035
  @keyframes lyrics-scroll {
4904
5036
  from {
4905
- transform: translateY(var(--scroll-delta)) translateZ(1px);
5037
+ transform: translate3d(0, var(--scroll-delta), 0);
4906
5038
  }
4907
5039
  to {
4908
- transform: translateY(0) translateZ(1px);
5040
+ transform: translate3d(0, 0, 0);
4909
5041
  }
4910
5042
  }
4911
5043
 
4912
- /* Character grow animation - exact copy from YouLyPlus */
5044
+ /* Character grow animation translate3d+scale3d for smooth transform,
5045
+ drop-shadow for glow (text-shadow doesn't work with background-clip:text) */
4913
5046
  @keyframes grow-dynamic {
4914
5047
  0% {
4915
- transform: matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
5048
+ transform: translate3d(0, 0, 0) scale3d(1, 1, 1);
4916
5049
  filter: drop-shadow(
4917
5050
  0 0 0
4918
5051
  color-mix(in srgb, var(--lyplus-lyrics-palette), transparent 100%)
@@ -4920,27 +5053,12 @@ AmLyrics.styles = i$3 `
4920
5053
  }
4921
5054
  25%,
4922
5055
  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
- );
5056
+ transform: translate3d(
5057
+ var(--char-offset-x, 0px),
5058
+ var(--translate-y-peak, -2px),
5059
+ 0
5060
+ )
5061
+ scale3d(var(--matrix-scale, 1.1), var(--matrix-scale, 1.1), 1);
4944
5062
  filter: drop-shadow(
4945
5063
  0 0 0.1em
4946
5064
  color-mix(
@@ -4950,8 +5068,10 @@ AmLyrics.styles = i$3 `
4950
5068
  )
4951
5069
  );
4952
5070
  }
5071
+ 75%,
4953
5072
  100% {
4954
- transform: translateY(-3.5%) translateZ(1px);
5073
+ transform: translate3d(0, var(--char-rise-y, -1.12px), 0)
5074
+ scale3d(1, 1, 1);
4955
5075
  filter: drop-shadow(
4956
5076
  0 0 0
4957
5077
  color-mix(in srgb, var(--lyplus-lyrics-palette), transparent 100%)
@@ -5083,15 +5203,15 @@ __decorate([
5083
5203
  __decorate([
5084
5204
  n({ type: String, attribute: 'song-album' })
5085
5205
  ], AmLyrics.prototype, "songAlbum", void 0);
5206
+ __decorate([
5207
+ n({ type: String, attribute: 'songwriters' })
5208
+ ], AmLyrics.prototype, "songwriters", void 0);
5086
5209
  __decorate([
5087
5210
  n({ type: Number, attribute: 'song-duration' })
5088
5211
  ], AmLyrics.prototype, "songDurationMs", void 0);
5089
5212
  __decorate([
5090
5213
  n({ type: String, attribute: 'highlight-color' })
5091
5214
  ], AmLyrics.prototype, "highlightColor", void 0);
5092
- __decorate([
5093
- n({ type: String, attribute: 'hover-background-color' })
5094
- ], AmLyrics.prototype, "hoverBackgroundColor", void 0);
5095
5215
  __decorate([
5096
5216
  n({ type: String, attribute: 'font-family' })
5097
5217
  ], AmLyrics.prototype, "fontFamily", void 0);