@uimaxbai/am-lyrics 1.5.5 → 1.5.6

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/src/AmLyrics.ts CHANGED
@@ -2,7 +2,7 @@ import { css, html, LitElement, svg } from 'lit';
2
2
  import { property, query, state } from 'lit/decorators.js';
3
3
  import { GoogleService } from './GoogleService.js';
4
4
 
5
- const VERSION = '1.5.5';
5
+ const VERSION = '1.5.6';
6
6
  const INSTRUMENTAL_THRESHOLD_MS = 7000; // Show dots for gaps >= 7s
7
7
  const FETCH_TIMEOUT_MS = 8000; // Timeout for all lyrics fetch requests
8
8
  const SEEK_THRESHOLD_MS = 500;
@@ -19,7 +19,7 @@ const LONG_WORD_WIPE_EXTRA_EM = 0.45;
19
19
  const LONG_WORD_WIPE_EXTRA_RATIO = 0.35;
20
20
  const SHORT_WORD_DRAG_MIN_DURATION_MS = 760;
21
21
  const SHORT_WORD_GLOW_MIN_DURATION_MS = 1320;
22
- const WORD_PRE_WIPE_HANDOFF_LEAD_MS = 120;
22
+ const WORD_PRE_WIPE_HANDOFF_LEAD_MS = 100;
23
23
 
24
24
  /**
25
25
  * Fetch with an automatic timeout via AbortSignal.
@@ -320,6 +320,18 @@ export class AmLyrics extends LitElement {
320
320
  opacity: 1;
321
321
  }
322
322
 
323
+ /* Predictive scrolling begins before the next timestamp. Start dimming
324
+ the outgoing line at the same moment so it settles with the scroll. */
325
+ .lyrics-line.scroll-exiting {
326
+ opacity: 0.8;
327
+ color: var(--lyplus-text-secondary);
328
+ transition:
329
+ opacity var(--scroll-duration, 400ms) cubic-bezier(0.41, 0, 0.12, 0.99),
330
+ transform var(--scroll-duration, 400ms)
331
+ cubic-bezier(0.41, 0, 0.12, 0.99) var(--lyrics-line-delay, 0ms),
332
+ filter var(--scroll-duration, 400ms) ease;
333
+ }
334
+
323
335
  .lyrics-line.persist-highlight {
324
336
  filter: none !important;
325
337
  opacity: 1;
@@ -750,15 +762,24 @@ export class AmLyrics extends LitElement {
750
762
  background-clip: text;
751
763
  -webkit-background-clip: text;
752
764
  background-repeat: no-repeat;
753
- background-image: linear-gradient(
754
- 90deg,
755
- var(--lyplus-text-primary, #fff) 0%,
756
- var(--lyplus-text-primary, #fff)
757
- calc(100% - var(--wipe-gradient-width, 0.75em)),
758
- #0000 100%
759
- );
760
- background-size: 0% 100%;
761
- background-position: left;
765
+ background-image:
766
+ linear-gradient(
767
+ 90deg,
768
+ #ffffff00 0%,
769
+ var(--lyplus-text-primary, #fff) 50%,
770
+ #0000 100%
771
+ ),
772
+ linear-gradient(
773
+ 90deg,
774
+ var(--lyplus-text-primary, #fff) 100%,
775
+ #0000 100%
776
+ );
777
+ background-size:
778
+ var(--wipe-gradient-width, 0.75em) 100%,
779
+ 0% 100%;
780
+ background-position:
781
+ calc(-1 * var(--wipe-gradient-width, 0.75em)) 0%,
782
+ left;
762
783
  transition:
763
784
  transform 0.7s ease,
764
785
  color 0.18s;
@@ -777,20 +798,11 @@ export class AmLyrics extends LitElement {
777
798
  }
778
799
 
779
800
  .lyrics-line.active .lyrics-syllable span.char.pre-wipe-lead {
780
- animation-name: pre-wipe-word-char;
801
+ animation-name: char-pre-wipe;
781
802
  animation-duration: var(--pre-wipe-duration);
782
803
  animation-delay: var(--pre-wipe-delay);
783
804
  animation-timing-function: linear;
784
805
  animation-fill-mode: forwards;
785
- background-image: linear-gradient(
786
- 90deg,
787
- var(--lyplus-text-primary, #fff) 0%,
788
- var(--lyplus-text-primary, #fff)
789
- calc(100% - var(--wipe-gradient-width, 0.75em)),
790
- #0000 100%
791
- );
792
- background-size: 0% 100%;
793
- background-position: var(--char-wipe-position, left) 0%;
794
806
  }
795
807
 
796
808
  /* ==========================================================================
@@ -1327,30 +1339,63 @@ export class AmLyrics extends LitElement {
1327
1339
  }
1328
1340
  }
1329
1341
 
1330
- @keyframes pre-wipe-word-char {
1342
+ /* Character-rendered words use a separate moving gradient in front of
1343
+ their solid fill. This makes the individual glyph wipes read as one
1344
+ continuous word-level wipe. */
1345
+ @keyframes char-pre-wipe {
1331
1346
  from {
1332
- background-size: 0px 100%;
1333
- background-position: var(--char-wipe-position, left) 0%;
1347
+ background-size:
1348
+ var(--wipe-gradient-width, 0.75em) 100%,
1349
+ 0% 100%;
1350
+ background-position:
1351
+ calc(-1 * var(--wipe-gradient-width, 0.75em)) 0%,
1352
+ left;
1334
1353
  }
1335
1354
  to {
1336
- background-size: var(--pre-wipe-word-size, var(--wipe-gradient-width))
1337
- 100%;
1338
- background-position: var(--char-wipe-position, left) 0%;
1355
+ background-size:
1356
+ var(--wipe-gradient-width, 0.75em) 100%,
1357
+ 0% 100%;
1358
+ background-position:
1359
+ calc(-1 * var(--wipe-gradient-half, 0.375em)) 0%,
1360
+ left;
1339
1361
  }
1340
1362
  }
1341
1363
 
1342
- @keyframes wipe-word-from-pre {
1364
+ @keyframes char-start-wipe {
1343
1365
  from {
1344
- background-size: var(--pre-wipe-word-size, var(--wipe-gradient-width))
1345
- 100%;
1346
- background-position: var(--char-wipe-position, left) 0%;
1366
+ background-size:
1367
+ var(--wipe-gradient-width, 0.75em) 100%,
1368
+ 0% 100%;
1369
+ background-position:
1370
+ calc(-1 * var(--wipe-gradient-width, 0.75em)) 0%,
1371
+ left;
1347
1372
  }
1348
1373
  to {
1349
- background-size: calc(
1350
- var(--word-wipe-width, 100%) + var(--wipe-gradient-width, 0.75em)
1351
- )
1352
- 100%;
1353
- background-position: var(--char-wipe-position, left) 0%;
1374
+ background-size:
1375
+ var(--wipe-gradient-width, 0.75em) 100%,
1376
+ 100% 100%;
1377
+ background-position:
1378
+ calc(100% + var(--wipe-gradient-half, 0.375em)) 0%,
1379
+ left;
1380
+ }
1381
+ }
1382
+
1383
+ @keyframes char-wipe {
1384
+ from {
1385
+ background-size:
1386
+ var(--wipe-gradient-width, 0.75em) 100%,
1387
+ 0% 100%;
1388
+ background-position:
1389
+ calc(-1 * var(--wipe-gradient-half, 0.375em)) 0%,
1390
+ left;
1391
+ }
1392
+ to {
1393
+ background-size:
1394
+ var(--wipe-gradient-width, 0.75em) 100%,
1395
+ 100% 100%;
1396
+ background-position:
1397
+ calc(100% + var(--wipe-gradient-half, 0.375em)) 0%,
1398
+ left;
1354
1399
  }
1355
1400
  }
1356
1401
 
@@ -1703,10 +1748,15 @@ export class AmLyrics extends LitElement {
1703
1748
  // Stop all running animations and clear highlights immediately
1704
1749
  if (this.lyricsContainer) {
1705
1750
  const activeLines = this.lyricsContainer.querySelectorAll(
1706
- '.lyrics-line.active, .lyrics-line.pre-active, .lyrics-line.bg-expanded',
1751
+ '.lyrics-line.active, .lyrics-line.pre-active, .lyrics-line.bg-expanded, .lyrics-line.scroll-exiting',
1707
1752
  );
1708
1753
  activeLines.forEach(line => {
1709
- line.classList.remove('active', 'pre-active', 'bg-expanded');
1754
+ line.classList.remove(
1755
+ 'active',
1756
+ 'pre-active',
1757
+ 'bg-expanded',
1758
+ 'scroll-exiting',
1759
+ );
1710
1760
  AmLyrics.resetSyllables(line as HTMLElement);
1711
1761
  });
1712
1762
 
@@ -3647,7 +3697,11 @@ export class AmLyrics extends LitElement {
3647
3697
  AmLyrics.finishSyllablesUpToTime(lineElement, newTime);
3648
3698
  }
3649
3699
 
3650
- lineElement.classList.remove('active', 'bg-expanded');
3700
+ lineElement.classList.remove(
3701
+ 'active',
3702
+ 'bg-expanded',
3703
+ 'scroll-exiting',
3704
+ );
3651
3705
 
3652
3706
  if (lineElement.classList.contains('pre-active')) {
3653
3707
  lineElement.classList.remove('pre-active');
@@ -3665,7 +3719,7 @@ export class AmLyrics extends LitElement {
3665
3719
  const lineElement = this._getLineElement(lineIndex);
3666
3720
  if (lineElement) {
3667
3721
  lineElement.classList.add('active');
3668
- lineElement.classList.remove('pre-active');
3722
+ lineElement.classList.remove('pre-active', 'scroll-exiting');
3669
3723
  const preIdx = this.preActiveLineElements.indexOf(lineElement);
3670
3724
  if (preIdx !== -1) this.preActiveLineElements.splice(preIdx, 1);
3671
3725
  }
@@ -3914,7 +3968,13 @@ export class AmLyrics extends LitElement {
3914
3968
  }
3915
3969
 
3916
3970
  updated(changedProperties: Map<string | number | symbol, unknown>) {
3917
- if (changedProperties.has('lyrics')) {
3971
+ const lyricsDomBecameRenderable =
3972
+ changedProperties.has('lyrics') ||
3973
+ (changedProperties.has('isLoading') &&
3974
+ !this.isLoading &&
3975
+ Boolean(this.lyrics));
3976
+
3977
+ if (lyricsDomBecameRenderable) {
3918
3978
  this._invalidateCaches();
3919
3979
  this._ensureLineDataCache();
3920
3980
  this._updateCachedIsUnsynced();
@@ -4112,7 +4172,12 @@ export class AmLyrics extends LitElement {
4112
4172
  }
4113
4173
 
4114
4174
  const scrollDuration = scrollLookAheadMs;
4115
- targetElement.style.setProperty('--scroll-duration', `${scrollDuration}ms`);
4175
+ if (targetElement !== this.currentPrimaryActiveLine || forceScroll) {
4176
+ targetElement.style.setProperty(
4177
+ '--scroll-duration',
4178
+ `${scrollDuration}ms`,
4179
+ );
4180
+ }
4116
4181
  this.setBackgroundExpandedLine(targetElement);
4117
4182
 
4118
4183
  // Unblur the upcoming target line early while the separate bg-expanded
@@ -4383,6 +4448,7 @@ export class AmLyrics extends LitElement {
4383
4448
 
4384
4449
  const computedStyle = getComputedStyle(referenceSyllable);
4385
4450
  const { font } = computedStyle; // Full font string
4451
+ const fontSize = Number.parseFloat(computedStyle.fontSize) || 16;
4386
4452
 
4387
4453
  const charTimedWords = Array.from(
4388
4454
  this.shadowRoot.querySelectorAll(
@@ -4466,6 +4532,15 @@ export class AmLyrics extends LitElement {
4466
4532
  Number.isFinite(virtualWordStart) &&
4467
4533
  Number.isFinite(virtualWordEnd) &&
4468
4534
  virtualWordDuration > 0;
4535
+ const wordVelocityPxPerMs = hasTimedSyllables
4536
+ ? totalWordWidth / virtualWordDuration
4537
+ : 0;
4538
+ const gradientLeadWidthPx =
4539
+ (BASE_WIPE_GRADIENT_EM * Math.max(1, fontSize)) / 2;
4540
+ const measuredPreWipeDuration =
4541
+ wordVelocityPxPerMs > 0
4542
+ ? gradientLeadWidthPx / wordVelocityPxPerMs
4543
+ : 100;
4469
4544
 
4470
4545
  let cumulativeCharWidth = 0;
4471
4546
  syllableEntries.forEach(entry => {
@@ -4504,6 +4579,7 @@ export class AmLyrics extends LitElement {
4504
4579
 
4505
4580
  target.dataset.wipeStart = startPercent.toFixed(4);
4506
4581
  target.dataset.wipeDuration = durationPercent.toFixed(4);
4582
+ target.dataset.preWipeDuration = measuredPreWipeDuration.toFixed(2);
4507
4583
  target.style.setProperty('--word-wipe-width', `${totalWordWidth}px`);
4508
4584
  target.style.setProperty(
4509
4585
  '--char-wipe-position',
@@ -4727,7 +4803,15 @@ export class AmLyrics extends LitElement {
4727
4803
  // effectiveEndTimes). Lines stay active until their extended end,
4728
4804
  // so we no longer need to remove .active here.
4729
4805
  this.lastPrimaryActiveLine = this.currentPrimaryActiveLine;
4806
+ if (this.lastPrimaryActiveLine) {
4807
+ this.lastPrimaryActiveLine.style.setProperty(
4808
+ '--scroll-duration',
4809
+ `${scrollDuration ?? SCROLL_ANIMATION_DURATION_MS}ms`,
4810
+ );
4811
+ this.lastPrimaryActiveLine.classList.add('scroll-exiting');
4812
+ }
4730
4813
  this.currentPrimaryActiveLine = lineElement;
4814
+ this.currentPrimaryActiveLine.classList.remove('scroll-exiting');
4731
4815
  const lineIndex = AmLyrics.getLineIndexFromElement(lineElement);
4732
4816
  if (lineIndex !== null) {
4733
4817
  this.lastActiveIndex = lineIndex;
@@ -5095,7 +5179,7 @@ export class AmLyrics extends LitElement {
5095
5179
  allLines.forEach(lineEl => {
5096
5180
  AmLyrics.resetSyllables(lineEl as HTMLElement);
5097
5181
  // Remove scroll-animate class and properties to stop any scroll animations
5098
- lineEl.classList.remove('scroll-animate');
5182
+ lineEl.classList.remove('scroll-animate', 'scroll-exiting');
5099
5183
  (lineEl as HTMLElement).style.removeProperty('--scroll-delta');
5100
5184
  (lineEl as HTMLElement).style.removeProperty('--lyrics-line-delay');
5101
5185
  });
@@ -5379,6 +5463,7 @@ export class AmLyrics extends LitElement {
5379
5463
  scrollDuration ?? SCROLL_ANIMATION_DURATION_MS,
5380
5464
  );
5381
5465
  const delayIncrement = duration * 0.1;
5466
+ const maxStaggerSteps = 4;
5382
5467
  const lookAhead = 20;
5383
5468
  const len = lineArray.length;
5384
5469
 
@@ -5387,13 +5472,17 @@ export class AmLyrics extends LitElement {
5387
5472
 
5388
5473
  let maxAnimationDuration = 0;
5389
5474
  const newAnimatingLines: HTMLElement[] = [];
5475
+ const lineDelays = new Map<HTMLElement, number>();
5390
5476
  const scrollingDown = delta >= 0;
5391
5477
 
5392
5478
  if (scrollingDown) {
5393
5479
  let delayCounter = 0;
5394
5480
  for (let i = start; i < end; i += 1) {
5395
5481
  const line = lineArray[i];
5396
- const delay = i >= referenceIndex ? delayCounter * delayIncrement : 0;
5482
+ const delay =
5483
+ i >= referenceIndex
5484
+ ? Math.min(delayCounter, maxStaggerSteps) * delayIncrement
5485
+ : 0;
5397
5486
 
5398
5487
  if (i >= referenceIndex && !line.classList.contains('lyrics-gap')) {
5399
5488
  delayCounter += 1;
@@ -5401,11 +5490,11 @@ export class AmLyrics extends LitElement {
5401
5490
 
5402
5491
  line.style.setProperty('--scroll-delta', `${delta}px`);
5403
5492
  line.style.setProperty('--lyrics-line-delay', `${delay}ms`);
5404
- line.style.setProperty('--scroll-duration', `${duration + 100}ms`);
5493
+ lineDelays.set(line, delay);
5405
5494
 
5406
5495
  newAnimatingLines.push(line);
5407
5496
 
5408
- const lineDuration = duration + delay;
5497
+ const lineDuration = duration + 100 + delay;
5409
5498
  if (lineDuration > maxAnimationDuration) {
5410
5499
  maxAnimationDuration = lineDuration;
5411
5500
  }
@@ -5414,7 +5503,10 @@ export class AmLyrics extends LitElement {
5414
5503
  let delayCounter = 0;
5415
5504
  for (let i = end - 1; i >= start; i -= 1) {
5416
5505
  const line = lineArray[i];
5417
- const delay = i <= referenceIndex ? delayCounter * delayIncrement : 0;
5506
+ const delay =
5507
+ i <= referenceIndex
5508
+ ? Math.min(delayCounter, maxStaggerSteps) * delayIncrement
5509
+ : 0;
5418
5510
 
5419
5511
  if (i <= referenceIndex && !line.classList.contains('lyrics-gap')) {
5420
5512
  delayCounter += 1;
@@ -5422,17 +5514,27 @@ export class AmLyrics extends LitElement {
5422
5514
 
5423
5515
  line.style.setProperty('--scroll-delta', `${delta}px`);
5424
5516
  line.style.setProperty('--lyrics-line-delay', `${delay}ms`);
5425
- line.style.setProperty('--scroll-duration', `${duration + 100}ms`);
5517
+ lineDelays.set(line, delay);
5426
5518
 
5427
5519
  newAnimatingLines.push(line);
5428
5520
 
5429
- const lineDuration = duration + delay;
5521
+ const lineDuration = duration + 100 + delay;
5430
5522
  if (lineDuration > maxAnimationDuration) {
5431
5523
  maxAnimationDuration = lineDuration;
5432
5524
  }
5433
5525
  }
5434
5526
  }
5435
5527
 
5528
+ /* Preserve the staggered starts, but make every line settle together.
5529
+ This keeps the selected line from drifting after its neighbours. */
5530
+ for (const line of newAnimatingLines) {
5531
+ const delay = lineDelays.get(line) ?? 0;
5532
+ line.style.setProperty(
5533
+ '--scroll-duration',
5534
+ `${Math.max(100, maxAnimationDuration - delay)}ms`,
5535
+ );
5536
+ }
5537
+
5436
5538
  // --- Step 3: Force reflow so the browser sees the class removal ---
5437
5539
  // Use offsetHeight which is cheaper than getBoundingClientRect
5438
5540
  // eslint-disable-next-line no-void
@@ -5810,16 +5912,18 @@ export class AmLyrics extends LitElement {
5810
5912
  );
5811
5913
  preWipeSyllable.classList.add('pre-highlight');
5812
5914
 
5813
- let leadSpans = charSpans;
5814
- if (leadSpans.length === 0 && leadChar) {
5815
- leadSpans = [leadChar];
5915
+ // Character-animated words still need a single word-leading gradient.
5916
+ // Giving every glyph this state creates one gradient per character and
5917
+ // makes the whole word enter the pre-wipe continuation simultaneously.
5918
+ if (leadChar) {
5919
+ AmLyrics.applyWipeShape(leadChar, charCount);
5920
+ leadChar.style.setProperty(
5921
+ '--pre-wipe-duration',
5922
+ `${preWipeDurationMs}ms`,
5923
+ );
5924
+ leadChar.style.setProperty('--pre-wipe-delay', `${-elapsedPreWipe}ms`);
5925
+ leadChar.classList.add('pre-wipe-lead');
5816
5926
  }
5817
- leadSpans.forEach(span => {
5818
- AmLyrics.applyWipeShape(span, charCount);
5819
- span.style.setProperty('--pre-wipe-duration', `${preWipeDurationMs}ms`);
5820
- span.style.setProperty('--pre-wipe-delay', `${-elapsedPreWipe}ms`);
5821
- span.classList.add('pre-wipe-lead');
5822
- });
5823
5927
 
5824
5928
  wordElements.forEach(element => {
5825
5929
  const target = element as any;
@@ -6152,22 +6256,18 @@ export class AmLyrics extends LitElement {
6152
6256
  0,
6153
6257
  charWipeDurationMs - charStartMs,
6154
6258
  );
6155
- let wipeDelay = charStartMs - wordElapsedTimeMs;
6156
- let wipeDuration = Math.min(
6259
+ const wipeDelay = charStartMs - wordElapsedTimeMs;
6260
+ const wipeDuration = Math.min(
6157
6261
  charWipeDurationMs * durationPct * wipeScale,
6158
6262
  remainingWordWipeMs,
6159
6263
  );
6160
6264
  const useStartAnimation =
6161
6265
  isFirstInContainer && globalCharIndex === 0 && !hadCharPreWipe;
6162
- let charWipeAnimation = 'wipe';
6266
+ let charWipeAnimation = 'char-wipe';
6163
6267
  if (hadCharPreWipe) {
6164
- charWipeAnimation = 'wipe-word-from-pre';
6165
- wipeDelay = -wordElapsedTimeMs;
6166
- wipeDuration = charWipeDurationMs;
6268
+ charWipeAnimation = 'char-wipe';
6167
6269
  } else if (useStartAnimation) {
6168
- charWipeAnimation = isRTL ? 'start-wipe-rtl' : 'start-wipe';
6169
- } else {
6170
- charWipeAnimation = isRTL ? 'wipe-rtl' : 'wipe';
6270
+ charWipeAnimation = 'char-start-wipe';
6171
6271
  }
6172
6272
 
6173
6273
  const existingAnimation =
@@ -6183,6 +6283,28 @@ export class AmLyrics extends LitElement {
6183
6283
  ) {
6184
6284
  animationParts.push(existingAnimation.split(',')[0].trim());
6185
6285
  }
6286
+ if (
6287
+ globalCharIndex > 0 &&
6288
+ !hadCharPreWipe &&
6289
+ wipeDelay > 0 &&
6290
+ wipeDuration > 0
6291
+ ) {
6292
+ const measuredDuration = Number.parseFloat(
6293
+ span.dataset.preWipeDuration || '100',
6294
+ );
6295
+ const preWipeDuration = Math.min(
6296
+ measuredDuration,
6297
+ wipeDuration * 0.9,
6298
+ charWipeDurationMs * 0.08,
6299
+ wipeDelay,
6300
+ );
6301
+
6302
+ if (preWipeDuration >= 16) {
6303
+ animationParts.push(
6304
+ `char-pre-wipe ${preWipeDuration}ms linear ${wipeDelay - preWipeDuration}ms none`,
6305
+ );
6306
+ }
6307
+ }
6186
6308
  if (wipeDuration > 0) {
6187
6309
  const wipeFillMode = hadCharPreWipe ? 'both' : 'forwards';
6188
6310
  animationParts.push(