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