@uimaxbai/am-lyrics 1.2.6 → 1.2.8
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/AmLyrics.d.ts +13 -0
- package/dist/src/AmLyrics.d.ts.map +1 -1
- package/dist/src/am-lyrics.js +323 -241
- package/dist/src/am-lyrics.js.map +1 -1
- package/dist/src/react.js +323 -241
- package/dist/src/react.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/fix.cjs +7 -0
- package/package.json +1 -1
- package/patch.diff +210 -0
- package/patch2.diff +26 -0
- package/src/AmLyrics.ts +393 -298
package/src/AmLyrics.ts
CHANGED
|
@@ -2,17 +2,17 @@ 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.2.
|
|
5
|
+
const VERSION = '1.2.8';
|
|
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;
|
|
9
9
|
const PRE_SCROLL_LEAD_MS = 500;
|
|
10
|
-
const PRE_SCROLL_LEAD_SHORT_MS =
|
|
10
|
+
const PRE_SCROLL_LEAD_SHORT_MS = 350;
|
|
11
11
|
const SCROLL_ANIMATION_DURATION_MS = 280;
|
|
12
12
|
const SCROLL_DELAY_INCREMENT_MS = 24;
|
|
13
13
|
const GAP_PULSE_DURATION_MS = 4000;
|
|
14
14
|
const GAP_PULSE_CYCLE_MS = GAP_PULSE_DURATION_MS * 2;
|
|
15
|
-
const GAP_EXIT_LEAD_MS =
|
|
15
|
+
const GAP_EXIT_LEAD_MS = 600;
|
|
16
16
|
const GAP_MIN_SCALE = 0.85;
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -1010,8 +1010,8 @@ export class AmLyrics extends LitElement {
|
|
|
1010
1010
|
0.75em 100%,
|
|
1011
1011
|
0% 100%;
|
|
1012
1012
|
background-position:
|
|
1013
|
-
-0.
|
|
1014
|
-
|
|
1013
|
+
-0.75em 0%,
|
|
1014
|
+
-0.375em 0%;
|
|
1015
1015
|
}
|
|
1016
1016
|
100% {
|
|
1017
1017
|
background-size:
|
|
@@ -1105,7 +1105,7 @@ export class AmLyrics extends LitElement {
|
|
|
1105
1105
|
0.75em 100%,
|
|
1106
1106
|
0% 100%;
|
|
1107
1107
|
background-position:
|
|
1108
|
-
-0.
|
|
1108
|
+
-0.75em 0%,
|
|
1109
1109
|
left;
|
|
1110
1110
|
}
|
|
1111
1111
|
to {
|
|
@@ -1113,7 +1113,7 @@ export class AmLyrics extends LitElement {
|
|
|
1113
1113
|
0.75em 100%,
|
|
1114
1114
|
0% 100%;
|
|
1115
1115
|
background-position:
|
|
1116
|
-
-0.
|
|
1116
|
+
-0.375em 0%,
|
|
1117
1117
|
left;
|
|
1118
1118
|
}
|
|
1119
1119
|
}
|
|
@@ -1475,12 +1475,32 @@ export class AmLyrics extends LitElement {
|
|
|
1475
1475
|
@state()
|
|
1476
1476
|
private currentSourceIndex = 0;
|
|
1477
1477
|
|
|
1478
|
-
@state()
|
|
1479
1478
|
private isFetchingAlternatives = false;
|
|
1480
1479
|
|
|
1481
|
-
@state()
|
|
1482
1480
|
private hasFetchedAllProviders = false;
|
|
1483
1481
|
|
|
1482
|
+
private _updateFooter() {
|
|
1483
|
+
const footer = this.shadowRoot?.querySelector('.lyrics-footer');
|
|
1484
|
+
if (!footer) return;
|
|
1485
|
+
const switchBtn = footer.querySelector('.source-switch-btn');
|
|
1486
|
+
const svgEl = footer.querySelector('.source-switch-svg');
|
|
1487
|
+
const labelEl = footer.querySelector('.source-switch-label');
|
|
1488
|
+
if (switchBtn) {
|
|
1489
|
+
(switchBtn as HTMLButtonElement).disabled = this.isFetchingAlternatives;
|
|
1490
|
+
}
|
|
1491
|
+
if (svgEl) {
|
|
1492
|
+
svgEl.setAttribute(
|
|
1493
|
+
'style',
|
|
1494
|
+
`margin-right: 4px; ${this.isFetchingAlternatives ? 'animation: spin 1s linear infinite;' : ''}`,
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
if (labelEl) {
|
|
1498
|
+
labelEl.textContent = this.isFetchingAlternatives
|
|
1499
|
+
? 'Switching...'
|
|
1500
|
+
: 'Switch';
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1484
1504
|
private animationFrameId?: number;
|
|
1485
1505
|
|
|
1486
1506
|
private mainWordAnimations: Map<
|
|
@@ -1500,7 +1520,6 @@ export class AmLyrics extends LitElement {
|
|
|
1500
1520
|
|
|
1501
1521
|
private userScrollTimeoutId?: number;
|
|
1502
1522
|
|
|
1503
|
-
@state()
|
|
1504
1523
|
private isUserScrolling = false;
|
|
1505
1524
|
|
|
1506
1525
|
private isProgrammaticScroll = false;
|
|
@@ -1512,6 +1531,33 @@ export class AmLyrics extends LitElement {
|
|
|
1512
1531
|
// Cached DOM elements for animation updates
|
|
1513
1532
|
private cachedLyricsLines: HTMLElement[] = [];
|
|
1514
1533
|
|
|
1534
|
+
// Cached line and gap element maps for fast lookup
|
|
1535
|
+
private lineElementCache = new Map<number, HTMLElement>();
|
|
1536
|
+
|
|
1537
|
+
private gapElementCache = new Map<number, HTMLElement>();
|
|
1538
|
+
|
|
1539
|
+
// Cached gap computation results
|
|
1540
|
+
private cachedAllGaps: Array<{
|
|
1541
|
+
insertBeforeIndex: number;
|
|
1542
|
+
gapStart: number;
|
|
1543
|
+
gapEnd: number;
|
|
1544
|
+
}> = [];
|
|
1545
|
+
|
|
1546
|
+
// Cached isUnsynced flag
|
|
1547
|
+
private cachedIsUnsynced = false;
|
|
1548
|
+
|
|
1549
|
+
// Cached pre-computed line data for render
|
|
1550
|
+
private cachedLineData: Array<{
|
|
1551
|
+
wordGroups: Syllable[][];
|
|
1552
|
+
groupGrowable: boolean[];
|
|
1553
|
+
groupGlowing: boolean[];
|
|
1554
|
+
vwFullText: string[];
|
|
1555
|
+
vwFullDuration: number[];
|
|
1556
|
+
vwCharOffset: number[];
|
|
1557
|
+
vwStartMs: number[];
|
|
1558
|
+
vwEndMs: number[];
|
|
1559
|
+
}> | null = null;
|
|
1560
|
+
|
|
1515
1561
|
// Active line tracking
|
|
1516
1562
|
private activeLineIds: Set<string> = new Set();
|
|
1517
1563
|
|
|
@@ -1602,6 +1648,7 @@ export class AmLyrics extends LitElement {
|
|
|
1602
1648
|
this.currentSourceIndex = 0;
|
|
1603
1649
|
this.isFetchingAlternatives = false;
|
|
1604
1650
|
this.hasFetchedAllProviders = false;
|
|
1651
|
+
this._updateFooter();
|
|
1605
1652
|
try {
|
|
1606
1653
|
const resolvedMetadata = await this.resolveSongMetadata();
|
|
1607
1654
|
// If a newer fetch was triggered while we awaited, bail out
|
|
@@ -1678,6 +1725,7 @@ export class AmLyrics extends LitElement {
|
|
|
1678
1725
|
s.source === 'Tidal' ||
|
|
1679
1726
|
s.source === 'Genius',
|
|
1680
1727
|
);
|
|
1728
|
+
this._updateFooter();
|
|
1681
1729
|
|
|
1682
1730
|
if (collectedSources.length > 0) {
|
|
1683
1731
|
this.availableSources = AmLyrics.mergeAndSortSources(collectedSources);
|
|
@@ -1803,6 +1851,7 @@ export class AmLyrics extends LitElement {
|
|
|
1803
1851
|
|
|
1804
1852
|
if (!this.hasFetchedAllProviders) {
|
|
1805
1853
|
this.isFetchingAlternatives = true;
|
|
1854
|
+
this._updateFooter();
|
|
1806
1855
|
try {
|
|
1807
1856
|
const resolvedMetadata = await this.resolveSongMetadata();
|
|
1808
1857
|
if (resolvedMetadata?.metadata) {
|
|
@@ -1865,6 +1914,7 @@ export class AmLyrics extends LitElement {
|
|
|
1865
1914
|
} finally {
|
|
1866
1915
|
this.hasFetchedAllProviders = true;
|
|
1867
1916
|
this.isFetchingAlternatives = false;
|
|
1917
|
+
this._updateFooter();
|
|
1868
1918
|
}
|
|
1869
1919
|
}
|
|
1870
1920
|
|
|
@@ -3135,15 +3185,11 @@ export class AmLyrics extends LitElement {
|
|
|
3135
3185
|
const linesChanged = !AmLyrics.arraysEqual(newActiveLines, oldActiveLines);
|
|
3136
3186
|
|
|
3137
3187
|
if (linesChanged || isSeek) {
|
|
3138
|
-
// Imperatively manage 'active' class so that scroll-animate and other
|
|
3139
|
-
// imperative classes are never clobbered.
|
|
3140
3188
|
if (this.lyricsContainer) {
|
|
3141
3189
|
// Remove 'active' from lines that are no longer active
|
|
3142
3190
|
for (const lineIndex of oldActiveLines) {
|
|
3143
3191
|
if (!newActiveLines.includes(lineIndex)) {
|
|
3144
|
-
const lineElement = this.
|
|
3145
|
-
`#lyrics-line-${lineIndex}`,
|
|
3146
|
-
) as HTMLElement;
|
|
3192
|
+
const lineElement = this._getLineElement(lineIndex);
|
|
3147
3193
|
if (lineElement) {
|
|
3148
3194
|
lineElement.classList.remove('active');
|
|
3149
3195
|
AmLyrics.resetSyllables(lineElement);
|
|
@@ -3153,12 +3199,10 @@ export class AmLyrics extends LitElement {
|
|
|
3153
3199
|
// Add 'active' to newly active lines
|
|
3154
3200
|
for (const lineIndex of newActiveLines) {
|
|
3155
3201
|
if (!oldActiveLines.includes(lineIndex)) {
|
|
3156
|
-
const lineElement = this.
|
|
3157
|
-
`#lyrics-line-${lineIndex}`,
|
|
3158
|
-
) as HTMLElement;
|
|
3202
|
+
const lineElement = this._getLineElement(lineIndex);
|
|
3159
3203
|
if (lineElement) {
|
|
3160
3204
|
lineElement.classList.add('active');
|
|
3161
|
-
lineElement.classList.remove('pre-active');
|
|
3205
|
+
lineElement.classList.remove('pre-active');
|
|
3162
3206
|
}
|
|
3163
3207
|
}
|
|
3164
3208
|
}
|
|
@@ -3170,17 +3214,13 @@ export class AmLyrics extends LitElement {
|
|
|
3170
3214
|
|
|
3171
3215
|
this.startAnimationFromTime(newTime);
|
|
3172
3216
|
|
|
3173
|
-
// Trigger scroll imperatively (was previously in updated() via @state)
|
|
3174
3217
|
this._handleActiveLineScroll(oldActiveLines, isSeek);
|
|
3175
3218
|
}
|
|
3176
3219
|
|
|
3177
|
-
// YouLyPlus-style syllable animation updates
|
|
3178
3220
|
if (this.lyricsContainer) {
|
|
3179
|
-
// Update syllables in active lines
|
|
3221
|
+
// Update syllables in active lines using cached elements
|
|
3180
3222
|
for (const lineIndex of this.activeLineIndices) {
|
|
3181
|
-
const lineElement = this.
|
|
3182
|
-
`#lyrics-line-${lineIndex}`,
|
|
3183
|
-
) as HTMLElement;
|
|
3223
|
+
const lineElement = this._getLineElement(lineIndex);
|
|
3184
3224
|
if (lineElement) {
|
|
3185
3225
|
AmLyrics.updateSyllablesForLine(lineElement, newTime);
|
|
3186
3226
|
}
|
|
@@ -3193,62 +3233,91 @@ export class AmLyrics extends LitElement {
|
|
|
3193
3233
|
AmLyrics.updateSyllablesForLine(gapLine as HTMLElement, newTime);
|
|
3194
3234
|
});
|
|
3195
3235
|
|
|
3196
|
-
// Imperatively manage gap active state
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3236
|
+
// Imperatively manage gap active state
|
|
3237
|
+
if (this.gapElementCache.size > 0) {
|
|
3238
|
+
for (const [, gap] of this.gapElementCache) {
|
|
3239
|
+
const gapStartTime = parseFloat(
|
|
3240
|
+
gap.getAttribute('data-start-time') || '0',
|
|
3241
|
+
);
|
|
3242
|
+
const gapEndTime = parseFloat(
|
|
3243
|
+
gap.getAttribute('data-end-time') || '0',
|
|
3244
|
+
);
|
|
3245
|
+
const shouldBeActive =
|
|
3246
|
+
newTime >= gapStartTime && newTime < gapEndTime;
|
|
3247
|
+
const isActive = gap.classList.contains('active');
|
|
3248
|
+
const isExiting = gap.classList.contains('gap-exiting');
|
|
3249
|
+
const exitLeadMs = GAP_EXIT_LEAD_MS;
|
|
3250
|
+
const shouldStartExiting =
|
|
3251
|
+
isActive && !isExiting && newTime >= gapEndTime - exitLeadMs;
|
|
3252
|
+
|
|
3253
|
+
if (shouldBeActive && !isActive && !isExiting) {
|
|
3254
|
+
gap.classList.remove('gap-exiting');
|
|
3255
|
+
gap.classList.add('active');
|
|
3256
|
+
const dotSyllables = gap.querySelectorAll('.lyrics-syllable');
|
|
3257
|
+
dotSyllables.forEach(dot => {
|
|
3258
|
+
const dotStart = parseFloat(
|
|
3259
|
+
dot.getAttribute('data-start-time') || '0',
|
|
3260
|
+
);
|
|
3261
|
+
const dotEnd = parseFloat(
|
|
3262
|
+
dot.getAttribute('data-end-time') || '0',
|
|
3263
|
+
);
|
|
3264
|
+
if (newTime > dotEnd) {
|
|
3265
|
+
dot.classList.add('finished');
|
|
3266
|
+
if (!dot.classList.contains('highlight')) {
|
|
3267
|
+
AmLyrics.updateSyllableAnimation(dot as HTMLElement);
|
|
3268
|
+
}
|
|
3269
|
+
} else if (newTime >= dotStart && newTime <= dotEnd) {
|
|
3228
3270
|
AmLyrics.updateSyllableAnimation(dot as HTMLElement);
|
|
3229
3271
|
}
|
|
3230
|
-
}
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3272
|
+
});
|
|
3273
|
+
} else if (shouldStartExiting) {
|
|
3274
|
+
gap.classList.add('gap-exiting');
|
|
3275
|
+
gap.classList.remove('active');
|
|
3276
|
+
setTimeout(() => {
|
|
3277
|
+
gap.classList.remove('gap-exiting');
|
|
3278
|
+
}, GAP_EXIT_LEAD_MS);
|
|
3279
|
+
} else if (isActive && !shouldBeActive) {
|
|
3280
|
+
gap.classList.remove('active');
|
|
3281
|
+
gap.classList.remove('gap-exiting');
|
|
3282
|
+
} else if (isExiting && newTime < gapEndTime - exitLeadMs) {
|
|
3241
3283
|
gap.classList.remove('gap-exiting');
|
|
3242
|
-
}
|
|
3243
|
-
} else if (isActive && !shouldBeActive) {
|
|
3244
|
-
// NEW: Immediate cleanup if we seeked out of valid range
|
|
3245
|
-
gap.classList.remove('active');
|
|
3246
|
-
gap.classList.remove('gap-exiting');
|
|
3247
|
-
} else if (isExiting && newTime < gapEndTime - exitLeadMs) {
|
|
3248
|
-
// NEW: Cleanup exiting state if we seeked backwards before exit window
|
|
3249
|
-
gap.classList.remove('gap-exiting');
|
|
3284
|
+
}
|
|
3250
3285
|
}
|
|
3251
|
-
})
|
|
3286
|
+
} else if (this.lyricsContainer) {
|
|
3287
|
+
// Fallback: no cache yet, use querySelectorAll
|
|
3288
|
+
const allGaps = this.lyricsContainer.querySelectorAll('.lyrics-gap');
|
|
3289
|
+
allGaps.forEach(gap => {
|
|
3290
|
+
const gapStartTime = parseFloat(
|
|
3291
|
+
gap.getAttribute('data-start-time') || '0',
|
|
3292
|
+
);
|
|
3293
|
+
const gapEndTime = parseFloat(
|
|
3294
|
+
gap.getAttribute('data-end-time') || '0',
|
|
3295
|
+
);
|
|
3296
|
+
const shouldBeActive =
|
|
3297
|
+
newTime >= gapStartTime && newTime < gapEndTime;
|
|
3298
|
+
const isActive = gap.classList.contains('active');
|
|
3299
|
+
const isExiting = gap.classList.contains('gap-exiting');
|
|
3300
|
+
const exitLeadMs = GAP_EXIT_LEAD_MS;
|
|
3301
|
+
const shouldStartExiting =
|
|
3302
|
+
isActive && !isExiting && newTime >= gapEndTime - exitLeadMs;
|
|
3303
|
+
|
|
3304
|
+
if (shouldBeActive && !isActive && !isExiting) {
|
|
3305
|
+
gap.classList.remove('gap-exiting');
|
|
3306
|
+
gap.classList.add('active');
|
|
3307
|
+
} else if (shouldStartExiting) {
|
|
3308
|
+
gap.classList.add('gap-exiting');
|
|
3309
|
+
gap.classList.remove('active');
|
|
3310
|
+
setTimeout(() => {
|
|
3311
|
+
gap.classList.remove('gap-exiting');
|
|
3312
|
+
}, GAP_EXIT_LEAD_MS);
|
|
3313
|
+
} else if (isActive && !shouldBeActive) {
|
|
3314
|
+
gap.classList.remove('active');
|
|
3315
|
+
gap.classList.remove('gap-exiting');
|
|
3316
|
+
} else if (isExiting && newTime < gapEndTime - exitLeadMs) {
|
|
3317
|
+
gap.classList.remove('gap-exiting');
|
|
3318
|
+
}
|
|
3319
|
+
});
|
|
3320
|
+
}
|
|
3252
3321
|
|
|
3253
3322
|
// Track instrumental gap state
|
|
3254
3323
|
const currentGap = this.findInstrumentalGapAt(newTime);
|
|
@@ -3271,9 +3340,7 @@ export class AmLyrics extends LitElement {
|
|
|
3271
3340
|
const line = this.lyrics[i];
|
|
3272
3341
|
const timeUntilStart = line.timestamp - newTime;
|
|
3273
3342
|
|
|
3274
|
-
const nextLineEl = this.
|
|
3275
|
-
`#lyrics-line-${i}`,
|
|
3276
|
-
) as HTMLElement;
|
|
3343
|
+
const nextLineEl = this._getLineElement(i);
|
|
3277
3344
|
|
|
3278
3345
|
const isBackToBack = this.activeLineIndices.length > 0;
|
|
3279
3346
|
const leadTime = isBackToBack
|
|
@@ -3281,16 +3348,14 @@ export class AmLyrics extends LitElement {
|
|
|
3281
3348
|
: PRE_SCROLL_LEAD_MS;
|
|
3282
3349
|
|
|
3283
3350
|
if (timeUntilStart > leadTime) {
|
|
3284
|
-
break;
|
|
3351
|
+
break;
|
|
3285
3352
|
}
|
|
3286
3353
|
|
|
3287
3354
|
if (timeUntilStart > 0 && timeUntilStart <= leadTime) {
|
|
3288
|
-
// Time to pre-scroll and pre-activate!
|
|
3289
3355
|
if (nextLineEl) {
|
|
3290
3356
|
preActiveLineIndex = i;
|
|
3291
3357
|
|
|
3292
3358
|
if (!isBackToBack) {
|
|
3293
|
-
// Apply unblur & zoom effect ahead of lyric start only if no line is currently active
|
|
3294
3359
|
nextLineEl.classList.add('pre-active');
|
|
3295
3360
|
}
|
|
3296
3361
|
this.clearPreActiveClasses(i);
|
|
@@ -3302,8 +3367,7 @@ export class AmLyrics extends LitElement {
|
|
|
3302
3367
|
this.focusLine(
|
|
3303
3368
|
nextLineEl,
|
|
3304
3369
|
false,
|
|
3305
|
-
slowScrollDuration,
|
|
3306
|
-
!!currentGap,
|
|
3370
|
+
isBackToBack ? 500 : slowScrollDuration,
|
|
3307
3371
|
);
|
|
3308
3372
|
}
|
|
3309
3373
|
break;
|
|
@@ -3317,6 +3381,9 @@ export class AmLyrics extends LitElement {
|
|
|
3317
3381
|
|
|
3318
3382
|
updated(changedProperties: Map<string | number | symbol, unknown>) {
|
|
3319
3383
|
if (changedProperties.has('lyrics')) {
|
|
3384
|
+
this._invalidateCaches();
|
|
3385
|
+
this._ensureLineDataCache();
|
|
3386
|
+
this._updateCachedIsUnsynced();
|
|
3320
3387
|
// Recalculate timing data for accurate animations whenever lyrics change
|
|
3321
3388
|
this._updateCharTimingData();
|
|
3322
3389
|
|
|
@@ -3326,9 +3393,7 @@ export class AmLyrics extends LitElement {
|
|
|
3326
3393
|
if (this.lyricsContainer && this.lyrics) {
|
|
3327
3394
|
const activeLines = this.findActiveLineIndices(this.currentTime);
|
|
3328
3395
|
for (const lineIndex of activeLines) {
|
|
3329
|
-
const lineEl = this.
|
|
3330
|
-
`#lyrics-line-${lineIndex}`,
|
|
3331
|
-
) as HTMLElement;
|
|
3396
|
+
const lineEl = this._getLineElement(lineIndex);
|
|
3332
3397
|
if (lineEl) lineEl.classList.add('active');
|
|
3333
3398
|
}
|
|
3334
3399
|
}
|
|
@@ -3344,7 +3409,7 @@ export class AmLyrics extends LitElement {
|
|
|
3344
3409
|
this.backgroundWordProgress.clear();
|
|
3345
3410
|
this.mainWordAnimations.clear();
|
|
3346
3411
|
this.backgroundWordAnimations.clear();
|
|
3347
|
-
this.
|
|
3412
|
+
this.setUserScrolling(false);
|
|
3348
3413
|
|
|
3349
3414
|
// Cancel any running animations
|
|
3350
3415
|
if (this.animationFrameId) {
|
|
@@ -3403,9 +3468,7 @@ export class AmLyrics extends LitElement {
|
|
|
3403
3468
|
);
|
|
3404
3469
|
if (targetLineIndex === null) return;
|
|
3405
3470
|
|
|
3406
|
-
const targetLine = this.
|
|
3407
|
-
`#lyrics-line-${targetLineIndex}`,
|
|
3408
|
-
) as HTMLElement;
|
|
3471
|
+
const targetLine = this._getLineElement(targetLineIndex);
|
|
3409
3472
|
|
|
3410
3473
|
if (targetLine) {
|
|
3411
3474
|
this.focusLine(targetLine, forceScroll);
|
|
@@ -3430,9 +3493,167 @@ export class AmLyrics extends LitElement {
|
|
|
3430
3493
|
return 0;
|
|
3431
3494
|
}
|
|
3432
3495
|
|
|
3496
|
+
private _rebuildDomCache() {
|
|
3497
|
+
if (!this.lyricsContainer) return;
|
|
3498
|
+
|
|
3499
|
+
this.lineElementCache.clear();
|
|
3500
|
+
this.gapElementCache.clear();
|
|
3501
|
+
|
|
3502
|
+
if (!this.lyrics) return;
|
|
3503
|
+
|
|
3504
|
+
for (let i = 0; i < this.lyrics.length; i += 1) {
|
|
3505
|
+
const lineEl = this.lyricsContainer.querySelector(
|
|
3506
|
+
`#lyrics-line-${i}`,
|
|
3507
|
+
) as HTMLElement | null;
|
|
3508
|
+
if (lineEl) this.lineElementCache.set(i, lineEl);
|
|
3509
|
+
|
|
3510
|
+
const gapEl = this.lyricsContainer.querySelector(
|
|
3511
|
+
`#gap-${i}`,
|
|
3512
|
+
) as HTMLElement | null;
|
|
3513
|
+
if (gapEl) this.gapElementCache.set(i, gapEl);
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
private _getLineElement(index: number): HTMLElement | null {
|
|
3518
|
+
const cached = this.lineElementCache.get(index);
|
|
3519
|
+
if (cached) return cached;
|
|
3520
|
+
if (!this.lyricsContainer) return null;
|
|
3521
|
+
const el = this.lyricsContainer.querySelector(
|
|
3522
|
+
`#lyrics-line-${index}`,
|
|
3523
|
+
) as HTMLElement | null;
|
|
3524
|
+
if (el) this.lineElementCache.set(index, el);
|
|
3525
|
+
return el;
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3528
|
+
private _getGapElement(index: number): HTMLElement | null {
|
|
3529
|
+
const cached = this.gapElementCache.get(index);
|
|
3530
|
+
if (cached) return cached;
|
|
3531
|
+
if (!this.lyricsContainer) return null;
|
|
3532
|
+
const el = this.lyricsContainer.querySelector(
|
|
3533
|
+
`#gap-${index}`,
|
|
3534
|
+
) as HTMLElement | null;
|
|
3535
|
+
if (el) this.gapElementCache.set(index, el);
|
|
3536
|
+
return el;
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
private _invalidateCaches() {
|
|
3540
|
+
this.cachedAllGaps = [];
|
|
3541
|
+
this.cachedIsUnsynced = false;
|
|
3542
|
+
this.cachedLineData = null;
|
|
3543
|
+
this.lineElementCache.clear();
|
|
3544
|
+
this.gapElementCache.clear();
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
private _updateCachedIsUnsynced() {
|
|
3548
|
+
this.cachedIsUnsynced =
|
|
3549
|
+
this.lyrics && this.lyrics.length > 0
|
|
3550
|
+
? this.lyrics.every(l => l.timestamp === 0 && l.endtime === 0)
|
|
3551
|
+
: false;
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
private _ensureLineDataCache() {
|
|
3555
|
+
if (this.cachedLineData || !this.lyrics) return;
|
|
3556
|
+
|
|
3557
|
+
this.cachedLineData = this.lyrics.map(line => {
|
|
3558
|
+
const wordGroups: Syllable[][] = [];
|
|
3559
|
+
for (const syllable of line.text) {
|
|
3560
|
+
if (syllable.part && wordGroups.length > 0) {
|
|
3561
|
+
wordGroups[wordGroups.length - 1].push(syllable);
|
|
3562
|
+
} else {
|
|
3563
|
+
wordGroups.push([syllable]);
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
|
|
3567
|
+
const groupGrowable: boolean[] = new Array(wordGroups.length).fill(false);
|
|
3568
|
+
const groupGlowing: boolean[] = new Array(wordGroups.length).fill(false);
|
|
3569
|
+
const vwFullText: string[] = new Array(wordGroups.length).fill('');
|
|
3570
|
+
const vwFullDuration: number[] = new Array(wordGroups.length).fill(0);
|
|
3571
|
+
const vwCharOffset: number[] = new Array(wordGroups.length).fill(0);
|
|
3572
|
+
const vwStartMs: number[] = new Array(wordGroups.length).fill(0);
|
|
3573
|
+
const vwEndMs: number[] = new Array(wordGroups.length).fill(0);
|
|
3574
|
+
|
|
3575
|
+
let vwStart = 0;
|
|
3576
|
+
while (vwStart < wordGroups.length) {
|
|
3577
|
+
let vwEnd = vwStart;
|
|
3578
|
+
while (vwEnd < wordGroups.length - 1) {
|
|
3579
|
+
const grp = wordGroups[vwEnd];
|
|
3580
|
+
const lastText = grp[grp.length - 1].text;
|
|
3581
|
+
if (/\s$/.test(lastText)) break;
|
|
3582
|
+
vwEnd += 1;
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3585
|
+
const combinedText = wordGroups
|
|
3586
|
+
.slice(vwStart, vwEnd + 1)
|
|
3587
|
+
.flatMap(g => g.map(s => s.text))
|
|
3588
|
+
.join('')
|
|
3589
|
+
.trim();
|
|
3590
|
+
const combinedStart = wordGroups[vwStart][0].timestamp;
|
|
3591
|
+
const lastGrp = wordGroups[vwEnd];
|
|
3592
|
+
const combinedEnd = lastGrp[lastGrp.length - 1].endtime;
|
|
3593
|
+
const combinedDuration = combinedEnd - combinedStart;
|
|
3594
|
+
|
|
3595
|
+
const isCJK =
|
|
3596
|
+
/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(
|
|
3597
|
+
combinedText,
|
|
3598
|
+
);
|
|
3599
|
+
const isRTL =
|
|
3600
|
+
/[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u0590-\u05FF]/.test(
|
|
3601
|
+
combinedText,
|
|
3602
|
+
);
|
|
3603
|
+
const hasHyphen = combinedText.includes('-');
|
|
3604
|
+
|
|
3605
|
+
const wordLen = combinedText.length;
|
|
3606
|
+
let isGrowableVW =
|
|
3607
|
+
!isCJK && !isRTL && !hasHyphen && wordLen > 0 && wordLen <= 12;
|
|
3608
|
+
if (isGrowableVW) {
|
|
3609
|
+
if (wordLen < 3) {
|
|
3610
|
+
isGrowableVW =
|
|
3611
|
+
combinedDuration >= 1110 && combinedDuration >= wordLen * 550;
|
|
3612
|
+
} else {
|
|
3613
|
+
isGrowableVW =
|
|
3614
|
+
combinedDuration >= 850 && combinedDuration >= wordLen * 200;
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
const isGlowingVW =
|
|
3619
|
+
isGrowableVW &&
|
|
3620
|
+
combinedDuration >= 1000 &&
|
|
3621
|
+
combinedDuration >= combinedText.length * 250;
|
|
3622
|
+
|
|
3623
|
+
let charOff = 0;
|
|
3624
|
+
for (let gi = vwStart; gi <= vwEnd; gi += 1) {
|
|
3625
|
+
groupGrowable[gi] = isGrowableVW;
|
|
3626
|
+
groupGlowing[gi] = isGlowingVW;
|
|
3627
|
+
vwFullText[gi] = combinedText;
|
|
3628
|
+
vwFullDuration[gi] = combinedDuration;
|
|
3629
|
+
vwCharOffset[gi] = charOff;
|
|
3630
|
+
vwStartMs[gi] = combinedStart;
|
|
3631
|
+
vwEndMs[gi] = combinedEnd;
|
|
3632
|
+
const grpText = wordGroups[gi].map(s => s.text).join('');
|
|
3633
|
+
charOff += grpText.replace(/\s/g, '').length;
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
vwStart = vwEnd + 1;
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
return {
|
|
3640
|
+
wordGroups,
|
|
3641
|
+
groupGrowable,
|
|
3642
|
+
groupGlowing,
|
|
3643
|
+
vwFullText,
|
|
3644
|
+
vwFullDuration,
|
|
3645
|
+
vwCharOffset,
|
|
3646
|
+
vwStartMs,
|
|
3647
|
+
vwEndMs,
|
|
3648
|
+
};
|
|
3649
|
+
});
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3433
3652
|
private _updateCharTimingData() {
|
|
3434
3653
|
if (!this.shadowRoot) return;
|
|
3435
3654
|
|
|
3655
|
+
this._rebuildDomCache();
|
|
3656
|
+
|
|
3436
3657
|
// Get the computed font from the first syllable to ensure accuracy
|
|
3437
3658
|
const referenceSyllable = this.shadowRoot.querySelector('.lyrics-syllable');
|
|
3438
3659
|
if (!referenceSyllable) return;
|
|
@@ -3587,6 +3808,15 @@ export class AmLyrics extends LitElement {
|
|
|
3587
3808
|
}
|
|
3588
3809
|
}
|
|
3589
3810
|
|
|
3811
|
+
private setUserScrolling(value: boolean) {
|
|
3812
|
+
this.isUserScrolling = value;
|
|
3813
|
+
if (value) {
|
|
3814
|
+
this.lyricsContainer?.classList.add('user-scrolling');
|
|
3815
|
+
} else {
|
|
3816
|
+
this.lyricsContainer?.classList.remove('user-scrolling');
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3590
3820
|
private handleUserScroll() {
|
|
3591
3821
|
// Ignore programmatic scrolls and click-seek scrolls
|
|
3592
3822
|
if (this.isProgrammaticScroll || this.isClickSeeking) {
|
|
@@ -3594,8 +3824,7 @@ export class AmLyrics extends LitElement {
|
|
|
3594
3824
|
}
|
|
3595
3825
|
|
|
3596
3826
|
// Mark that user is currently scrolling
|
|
3597
|
-
this.
|
|
3598
|
-
this.lyricsContainer?.classList.add('user-scrolling');
|
|
3827
|
+
this.setUserScrolling(true);
|
|
3599
3828
|
|
|
3600
3829
|
// Clear any existing timeout
|
|
3601
3830
|
if (this.userScrollTimeoutId) {
|
|
@@ -3604,7 +3833,7 @@ export class AmLyrics extends LitElement {
|
|
|
3604
3833
|
|
|
3605
3834
|
// Set timeout to re-enable auto-scroll after 2 seconds of no scrolling
|
|
3606
3835
|
this.userScrollTimeoutId = window.setTimeout(() => {
|
|
3607
|
-
this.
|
|
3836
|
+
this.setUserScrolling(false);
|
|
3608
3837
|
this.userScrollTimeoutId = undefined;
|
|
3609
3838
|
|
|
3610
3839
|
// Optionally scroll back to current active line when re-enabling auto-scroll
|
|
@@ -3615,20 +3844,16 @@ export class AmLyrics extends LitElement {
|
|
|
3615
3844
|
}
|
|
3616
3845
|
|
|
3617
3846
|
private findActiveLineIndices(time: number): number[] {
|
|
3618
|
-
if (!this.lyrics) return [];
|
|
3847
|
+
if (!this.lyrics || this.lyrics.length === 0) return [];
|
|
3619
3848
|
const activeLines: number[] = [];
|
|
3849
|
+
|
|
3620
3850
|
for (let i = 0; i < this.lyrics.length; i += 1) {
|
|
3621
3851
|
const line = this.lyrics[i];
|
|
3622
3852
|
let effectiveEndTime = line.endtime;
|
|
3623
3853
|
|
|
3624
|
-
// Extend the "active" highlight window to abut the next line,
|
|
3625
|
-
// leaving a 500ms gap for breathing/scrolling
|
|
3626
3854
|
if (i < this.lyrics.length - 1) {
|
|
3627
3855
|
const nextLineStart = this.lyrics[i + 1].timestamp;
|
|
3628
3856
|
const gapDuration = nextLineStart - line.endtime;
|
|
3629
|
-
|
|
3630
|
-
// If the gap is large enough to trigger the breathing dots,
|
|
3631
|
-
// DO NOT extend the highlight. The text should dim when the dots appear.
|
|
3632
3857
|
if (gapDuration < INSTRUMENTAL_THRESHOLD_MS) {
|
|
3633
3858
|
if (effectiveEndTime < nextLineStart) {
|
|
3634
3859
|
effectiveEndTime = Math.max(effectiveEndTime, nextLineStart - 500);
|
|
@@ -3636,6 +3861,7 @@ export class AmLyrics extends LitElement {
|
|
|
3636
3861
|
}
|
|
3637
3862
|
}
|
|
3638
3863
|
|
|
3864
|
+
if (line.timestamp > time) break;
|
|
3639
3865
|
if (time >= line.timestamp && time <= effectiveEndTime) {
|
|
3640
3866
|
activeLines.push(i);
|
|
3641
3867
|
}
|
|
@@ -3685,6 +3911,7 @@ export class AmLyrics extends LitElement {
|
|
|
3685
3911
|
gapStart: number;
|
|
3686
3912
|
gapEnd: number;
|
|
3687
3913
|
}> {
|
|
3914
|
+
if (this.cachedAllGaps.length > 0) return this.cachedAllGaps;
|
|
3688
3915
|
if (!this.lyrics || this.lyrics.length === 0) return [];
|
|
3689
3916
|
const gaps: Array<{
|
|
3690
3917
|
insertBeforeIndex: number;
|
|
@@ -3709,6 +3936,7 @@ export class AmLyrics extends LitElement {
|
|
|
3709
3936
|
}
|
|
3710
3937
|
}
|
|
3711
3938
|
|
|
3939
|
+
this.cachedAllGaps = gaps;
|
|
3712
3940
|
return gaps;
|
|
3713
3941
|
}
|
|
3714
3942
|
|
|
@@ -3898,7 +4126,7 @@ export class AmLyrics extends LitElement {
|
|
|
3898
4126
|
clearTimeout(this.userScrollTimeoutId);
|
|
3899
4127
|
this.userScrollTimeoutId = undefined;
|
|
3900
4128
|
}
|
|
3901
|
-
this.
|
|
4129
|
+
this.setUserScrolling(false);
|
|
3902
4130
|
|
|
3903
4131
|
// Reset active line tracking to prevent scroll fighting
|
|
3904
4132
|
this.currentPrimaryActiveLine = null;
|
|
@@ -4289,7 +4517,7 @@ export class AmLyrics extends LitElement {
|
|
|
4289
4517
|
|
|
4290
4518
|
this.lyricsContainer.classList.remove('not-focused', 'user-scrolling');
|
|
4291
4519
|
this.isProgrammaticScroll = true;
|
|
4292
|
-
this.
|
|
4520
|
+
this.setUserScrolling(false);
|
|
4293
4521
|
|
|
4294
4522
|
if (this.userScrollTimeoutId) {
|
|
4295
4523
|
clearTimeout(this.userScrollTimeoutId);
|
|
@@ -4344,71 +4572,29 @@ export class AmLyrics extends LitElement {
|
|
|
4344
4572
|
value: string;
|
|
4345
4573
|
}> = [];
|
|
4346
4574
|
|
|
4347
|
-
// Step 1
|
|
4575
|
+
// Step 1: Grow Pass
|
|
4348
4576
|
if (isGrowable && isFirstSyllable && allWordCharSpans.length > 0) {
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
const firstSyllableStartTime = parseFloat(
|
|
4353
|
-
syllable.getAttribute('data-start-time') || '0',
|
|
4354
|
-
);
|
|
4577
|
+
const finalDuration = wordDurationMs;
|
|
4578
|
+
const baseDelayPerChar = finalDuration * 0.09;
|
|
4579
|
+
const growDurationMs = finalDuration * 1.5;
|
|
4355
4580
|
|
|
4356
|
-
allWordCharSpans.forEach(
|
|
4581
|
+
allWordCharSpans.forEach(span => {
|
|
4357
4582
|
const horizontalOffset = parseFloat(
|
|
4358
4583
|
span.dataset.horizontalOffset || '0',
|
|
4359
4584
|
);
|
|
4360
|
-
|
|
4361
4585
|
const maxScale = span.dataset.maxScale || '1.1';
|
|
4362
4586
|
const shadowIntensity = span.dataset.shadowIntensity || '0.6';
|
|
4363
4587
|
const translateYPeak = span.dataset.translateYPeak || '-2';
|
|
4364
4588
|
|
|
4365
|
-
const
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
const parentDuration = parseFloat(
|
|
4370
|
-
parentSyllable.getAttribute('data-duration') || '0',
|
|
4371
|
-
);
|
|
4372
|
-
const parentStartTime = parseFloat(
|
|
4373
|
-
parentSyllable.getAttribute('data-start-time') || '0',
|
|
4374
|
-
);
|
|
4375
|
-
|
|
4376
|
-
const startPct = parseFloat(span.dataset.wipeStart || '0');
|
|
4377
|
-
const durationPct = parseFloat(span.dataset.wipeDuration || '0');
|
|
4378
|
-
|
|
4379
|
-
const relativeStartOffset = Math.max(
|
|
4380
|
-
0,
|
|
4381
|
-
parentStartTime - firstSyllableStartTime,
|
|
4382
|
-
);
|
|
4383
|
-
const wipeDelay = relativeStartOffset + parentDuration * startPct;
|
|
4384
|
-
const wipeDuration = parentDuration * durationPct;
|
|
4385
|
-
|
|
4386
|
-
const useStartAnimation = isFirstInContainer && charIndexInWord === 0;
|
|
4387
|
-
let charWipeAnimation = 'wipe';
|
|
4388
|
-
if (useStartAnimation)
|
|
4389
|
-
charWipeAnimation = isRTL ? 'start-wipe-rtl' : 'start-wipe';
|
|
4390
|
-
else charWipeAnimation = isRTL ? 'wipe-rtl' : 'wipe';
|
|
4391
|
-
|
|
4392
|
-
// Blend word and syllable durations to let the gradient flow smoothly
|
|
4393
|
-
// while still responding to syllable pacing (no strict exactness, just natural flow)
|
|
4394
|
-
const growDelay = wipeDelay;
|
|
4395
|
-
const growDurationMs = Math.max(
|
|
4396
|
-
600,
|
|
4397
|
-
wordDurationMs * 0.8 + parentDuration * 1.5,
|
|
4398
|
-
);
|
|
4399
|
-
|
|
4400
|
-
animationParts.push(
|
|
4401
|
-
`grow-dynamic ${growDurationMs}ms ease-in-out ${growDelay}ms forwards`,
|
|
4402
|
-
);
|
|
4403
|
-
|
|
4404
|
-
if (wipeDuration > 0) {
|
|
4405
|
-
animationParts.push(
|
|
4406
|
-
`${charWipeAnimation} ${wipeDuration}ms linear ${wipeDelay}ms forwards`,
|
|
4407
|
-
);
|
|
4408
|
-
}
|
|
4409
|
-
}
|
|
4589
|
+
const syllableCharIndex = parseFloat(
|
|
4590
|
+
span.dataset.syllableCharIndex || '0',
|
|
4591
|
+
);
|
|
4592
|
+
const growDelay = baseDelayPerChar * syllableCharIndex;
|
|
4410
4593
|
|
|
4411
|
-
charAnimationsMap.set(
|
|
4594
|
+
charAnimationsMap.set(
|
|
4595
|
+
span,
|
|
4596
|
+
`grow-dynamic ${growDurationMs}ms ease-in-out ${growDelay}ms forwards`,
|
|
4597
|
+
);
|
|
4412
4598
|
|
|
4413
4599
|
styleUpdates.push({
|
|
4414
4600
|
element: span,
|
|
@@ -4431,31 +4617,10 @@ export class AmLyrics extends LitElement {
|
|
|
4431
4617
|
value: `${translateYPeak}`,
|
|
4432
4618
|
});
|
|
4433
4619
|
});
|
|
4434
|
-
}
|
|
4435
|
-
// For subsequent syllables of a growable word:
|
|
4436
|
-
// If they already have `grow-dynamic`, it means the first syllable correctly took care of BOTH grow and wipe!
|
|
4437
|
-
// Otherwise, they scrubbed directly into this syllable, so let's at least do the wipe.
|
|
4438
|
-
charSpans.forEach(span => {
|
|
4439
|
-
const existingAnimation =
|
|
4440
|
-
charAnimationsMap.get(span) || span.style.animation || '';
|
|
4441
|
-
if (existingAnimation.includes('grow-dynamic')) return;
|
|
4442
|
-
|
|
4443
|
-
const startPct = parseFloat(span.dataset.wipeStart || '0');
|
|
4444
|
-
const durationPct = parseFloat(span.dataset.wipeDuration || '0');
|
|
4445
|
-
const wipeDelay = syllableDurationMs * startPct;
|
|
4446
|
-
const wipeDuration = syllableDurationMs * durationPct;
|
|
4447
|
-
|
|
4448
|
-
const charWipeAnimation = isRTL ? 'wipe-rtl' : 'wipe';
|
|
4620
|
+
}
|
|
4449
4621
|
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
span,
|
|
4453
|
-
`${charWipeAnimation} ${wipeDuration}ms linear ${wipeDelay}ms forwards`,
|
|
4454
|
-
);
|
|
4455
|
-
}
|
|
4456
|
-
});
|
|
4457
|
-
} else if (charSpans.length > 0) {
|
|
4458
|
-
// Per-character wipe for non-growable words (matching YouLyPlus)
|
|
4622
|
+
// Step 2: Wipe Pass
|
|
4623
|
+
if (charSpans.length > 0) {
|
|
4459
4624
|
charSpans.forEach((span, charIndex) => {
|
|
4460
4625
|
const startPct = parseFloat(span.dataset.wipeStart || '0');
|
|
4461
4626
|
const durationPct = parseFloat(span.dataset.wipeDuration || '0');
|
|
@@ -4471,12 +4636,39 @@ export class AmLyrics extends LitElement {
|
|
|
4471
4636
|
charWipeAnimation = isRTL ? 'wipe-rtl' : 'wipe';
|
|
4472
4637
|
}
|
|
4473
4638
|
|
|
4639
|
+
const existingAnimation =
|
|
4640
|
+
charAnimationsMap.get(span) || span.style.animation || '';
|
|
4641
|
+
|
|
4642
|
+
const animationParts = [];
|
|
4643
|
+
|
|
4644
|
+
if (existingAnimation && existingAnimation.includes('grow-dynamic')) {
|
|
4645
|
+
animationParts.push(existingAnimation.split(',')[0].trim());
|
|
4646
|
+
}
|
|
4647
|
+
if (charIndex > 0) {
|
|
4648
|
+
const arrivalTime = span.dataset.preWipeArrival
|
|
4649
|
+
? parseFloat(span.dataset.preWipeArrival)
|
|
4650
|
+
: wipeDelay;
|
|
4651
|
+
const constantDuration = parseFloat(
|
|
4652
|
+
span.dataset.preWipeDuration || '100',
|
|
4653
|
+
);
|
|
4654
|
+
const animDelay = arrivalTime - constantDuration;
|
|
4655
|
+
|
|
4656
|
+
if (constantDuration > 0) {
|
|
4657
|
+
animationParts.push(
|
|
4658
|
+
`pre-wipe-char ${constantDuration}ms linear ${animDelay}ms forwards`,
|
|
4659
|
+
);
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4662
|
+
|
|
4474
4663
|
if (wipeDuration > 0) {
|
|
4475
|
-
|
|
4476
|
-
span,
|
|
4664
|
+
animationParts.push(
|
|
4477
4665
|
`${charWipeAnimation} ${wipeDuration}ms linear ${wipeDelay}ms forwards`,
|
|
4478
4666
|
);
|
|
4479
4667
|
}
|
|
4668
|
+
|
|
4669
|
+
if (animationParts.length > 0) {
|
|
4670
|
+
charAnimationsMap.set(span, animationParts.join(', '));
|
|
4671
|
+
}
|
|
4480
4672
|
});
|
|
4481
4673
|
} else {
|
|
4482
4674
|
// Syllable-level wipe for regular (non-growable) words without chars
|
|
@@ -4925,10 +5117,7 @@ export class AmLyrics extends LitElement {
|
|
|
4925
5117
|
|
|
4926
5118
|
const sourceLabel = this.lyricsSource ?? 'Unavailable';
|
|
4927
5119
|
|
|
4928
|
-
const isUnsynced =
|
|
4929
|
-
this.lyrics && this.lyrics.length > 0
|
|
4930
|
-
? this.lyrics.every(l => l.timestamp === 0 && l.endtime === 0)
|
|
4931
|
-
: false;
|
|
5120
|
+
const isUnsynced = this.cachedIsUnsynced;
|
|
4932
5121
|
|
|
4933
5122
|
const renderContent = () => {
|
|
4934
5123
|
if (this.isLoading) {
|
|
@@ -5014,104 +5203,13 @@ export class AmLyrics extends LitElement {
|
|
|
5014
5203
|
// translation/romanization block for background — it would just duplicate
|
|
5015
5204
|
// the main line's text.
|
|
5016
5205
|
|
|
5017
|
-
|
|
5018
|
-
const wordGroups
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
// New word
|
|
5025
|
-
wordGroups.push([syllable]);
|
|
5026
|
-
}
|
|
5027
|
-
}
|
|
5028
|
-
|
|
5029
|
-
// Pre-compute isGrowable per "visual word": adjacent groups whose text
|
|
5030
|
-
// doesn't end with whitespace form one visual word (e.g. "a"+"live" = "alive").
|
|
5031
|
-
// We evaluate growable on the combined text/duration, then propagate
|
|
5032
|
-
// the result to each individual group so it renders through the
|
|
5033
|
-
// single-syllable path (which supports char-level glow).
|
|
5034
|
-
const groupGrowable: boolean[] = new Array(wordGroups.length).fill(
|
|
5035
|
-
false,
|
|
5036
|
-
);
|
|
5037
|
-
const groupGlowing: boolean[] = new Array(wordGroups.length).fill(
|
|
5038
|
-
false,
|
|
5039
|
-
);
|
|
5040
|
-
// Visual word info for growable char-level glow:
|
|
5041
|
-
// Each group stores the combined visual word's text, duration, and
|
|
5042
|
-
// the char offset of this group within the visual word.
|
|
5043
|
-
const vwFullText: string[] = new Array(wordGroups.length).fill('');
|
|
5044
|
-
const vwFullDuration: number[] = new Array(wordGroups.length).fill(0);
|
|
5045
|
-
const vwCharOffset: number[] = new Array(wordGroups.length).fill(0);
|
|
5046
|
-
const vwStartMs: number[] = new Array(wordGroups.length).fill(0);
|
|
5047
|
-
const vwEndMs: number[] = new Array(wordGroups.length).fill(0);
|
|
5048
|
-
{
|
|
5049
|
-
let vwStart = 0;
|
|
5050
|
-
while (vwStart < wordGroups.length) {
|
|
5051
|
-
let vwEnd = vwStart;
|
|
5052
|
-
while (vwEnd < wordGroups.length - 1) {
|
|
5053
|
-
const grp = wordGroups[vwEnd];
|
|
5054
|
-
const lastText = grp[grp.length - 1].text;
|
|
5055
|
-
if (/\s$/.test(lastText)) break;
|
|
5056
|
-
vwEnd += 1;
|
|
5057
|
-
}
|
|
5058
|
-
|
|
5059
|
-
// Compute combined properties for this visual word
|
|
5060
|
-
const combinedText = wordGroups
|
|
5061
|
-
.slice(vwStart, vwEnd + 1)
|
|
5062
|
-
.flatMap(g => g.map(s => s.text))
|
|
5063
|
-
.join('')
|
|
5064
|
-
.trim();
|
|
5065
|
-
const combinedStart = wordGroups[vwStart][0].timestamp;
|
|
5066
|
-
const lastGrp = wordGroups[vwEnd];
|
|
5067
|
-
const combinedEnd = lastGrp[lastGrp.length - 1].endtime;
|
|
5068
|
-
const combinedDuration = combinedEnd - combinedStart;
|
|
5069
|
-
|
|
5070
|
-
const isCJK =
|
|
5071
|
-
/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(
|
|
5072
|
-
combinedText,
|
|
5073
|
-
);
|
|
5074
|
-
const isRTL =
|
|
5075
|
-
/[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u0590-\u05FF]/.test(
|
|
5076
|
-
combinedText,
|
|
5077
|
-
);
|
|
5078
|
-
const hasHyphen = combinedText.includes('-');
|
|
5079
|
-
|
|
5080
|
-
const wordLen = combinedText.length;
|
|
5081
|
-
let isGrowableVW =
|
|
5082
|
-
!isCJK && !isRTL && !hasHyphen && wordLen > 0 && wordLen <= 12;
|
|
5083
|
-
if (isGrowableVW) {
|
|
5084
|
-
if (wordLen < 3) {
|
|
5085
|
-
isGrowableVW =
|
|
5086
|
-
combinedDuration >= 1110 && combinedDuration >= wordLen * 550;
|
|
5087
|
-
} else {
|
|
5088
|
-
isGrowableVW =
|
|
5089
|
-
combinedDuration >= 850 && combinedDuration >= wordLen * 200;
|
|
5090
|
-
}
|
|
5091
|
-
}
|
|
5092
|
-
|
|
5093
|
-
// Glow requirement (more strict)
|
|
5094
|
-
const isGlowingVW =
|
|
5095
|
-
isGrowableVW &&
|
|
5096
|
-
combinedDuration >= 1000 &&
|
|
5097
|
-
combinedDuration >= combinedText.length * 250;
|
|
5098
|
-
|
|
5099
|
-
let charOff = 0;
|
|
5100
|
-
for (let gi = vwStart; gi <= vwEnd; gi += 1) {
|
|
5101
|
-
groupGrowable[gi] = isGrowableVW;
|
|
5102
|
-
groupGlowing[gi] = isGlowingVW;
|
|
5103
|
-
vwFullText[gi] = combinedText;
|
|
5104
|
-
vwFullDuration[gi] = combinedDuration;
|
|
5105
|
-
vwCharOffset[gi] = charOff;
|
|
5106
|
-
vwStartMs[gi] = combinedStart;
|
|
5107
|
-
vwEndMs[gi] = combinedEnd;
|
|
5108
|
-
const grpText = wordGroups[gi].map(s => s.text).join('');
|
|
5109
|
-
charOff += grpText.replace(/\s/g, '').length;
|
|
5110
|
-
}
|
|
5111
|
-
|
|
5112
|
-
vwStart = vwEnd + 1;
|
|
5113
|
-
}
|
|
5114
|
-
}
|
|
5206
|
+
const lineData = this.cachedLineData?.[lineIndex];
|
|
5207
|
+
const wordGroups = lineData?.wordGroups ?? [];
|
|
5208
|
+
const groupGrowable = lineData?.groupGrowable ?? [];
|
|
5209
|
+
const groupGlowing = lineData?.groupGlowing ?? [];
|
|
5210
|
+
const vwFullText = lineData?.vwFullText ?? [];
|
|
5211
|
+
const vwFullDuration = lineData?.vwFullDuration ?? [];
|
|
5212
|
+
const vwCharOffset = lineData?.vwCharOffset ?? [];
|
|
5115
5213
|
|
|
5116
5214
|
// Create main vocals using YouLyPlus syllable structure
|
|
5117
5215
|
const mainVocalElement = html`<p class="main-vocal-container">
|
|
@@ -5387,9 +5485,7 @@ export class AmLyrics extends LitElement {
|
|
|
5387
5485
|
<div
|
|
5388
5486
|
class="lyrics-container ${isUnsynced
|
|
5389
5487
|
? 'is-unsynced'
|
|
5390
|
-
: 'blur-inactive-enabled'}
|
|
5391
|
-
? 'user-scrolling'
|
|
5392
|
-
: ''}"
|
|
5488
|
+
: 'blur-inactive-enabled'}"
|
|
5393
5489
|
>
|
|
5394
5490
|
${!this.isLoading && this.lyrics && this.lyrics.length > 0
|
|
5395
5491
|
? html`
|
|
@@ -5504,17 +5600,15 @@ export class AmLyrics extends LitElement {
|
|
|
5504
5600
|
!this.hasFetchedAllProviders
|
|
5505
5601
|
? html`
|
|
5506
5602
|
<button
|
|
5507
|
-
class="download-button"
|
|
5603
|
+
class="download-button source-switch-btn"
|
|
5508
5604
|
title="Switch Lyrics Source"
|
|
5509
5605
|
style="font-family: inherit; font-size: 11px; padding: 2px 6px; border-radius: 4px; border: 1px solid rgba(255, 255, 255, 0.2); background: transparent; cursor: pointer; color: #aaa; display: inline-flex; align-items: center;"
|
|
5510
5606
|
@click=${this.switchSource}
|
|
5511
5607
|
?disabled=${this.isFetchingAlternatives}
|
|
5512
5608
|
>
|
|
5513
5609
|
<svg
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
? 'animation: spin 1s linear infinite;'
|
|
5517
|
-
: ''}"
|
|
5610
|
+
class="source-switch-svg lucide lucide-arrow-down-up-icon lucide-arrow-down-up"
|
|
5611
|
+
style="margin-right: 4px;"
|
|
5518
5612
|
xmlns="http://www.w3.org/2000/svg"
|
|
5519
5613
|
width="12"
|
|
5520
5614
|
height="12"
|
|
@@ -5524,7 +5618,6 @@ export class AmLyrics extends LitElement {
|
|
|
5524
5618
|
stroke-width="2"
|
|
5525
5619
|
stroke-linecap="round"
|
|
5526
5620
|
stroke-linejoin="round"
|
|
5527
|
-
class="lucide lucide-arrow-down-up-icon lucide-arrow-down-up"
|
|
5528
5621
|
>
|
|
5529
5622
|
${this.isFetchingAlternatives
|
|
5530
5623
|
? svg`<path
|
|
@@ -5535,9 +5628,11 @@ export class AmLyrics extends LitElement {
|
|
|
5535
5628
|
><path d="m21 8-4-4-4 4"></path
|
|
5536
5629
|
><path d="M17 4v16"></path>`}
|
|
5537
5630
|
</svg>
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5631
|
+
<span class="source-switch-label"
|
|
5632
|
+
>${this.isFetchingAlternatives
|
|
5633
|
+
? 'Switching...'
|
|
5634
|
+
: 'Switch'}</span
|
|
5635
|
+
>
|
|
5541
5636
|
</button>
|
|
5542
5637
|
`
|
|
5543
5638
|
: ''}
|