@uimaxbai/am-lyrics 1.0.7 → 1.0.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 +15 -0
- package/dist/src/AmLyrics.d.ts.map +1 -1
- package/dist/src/am-lyrics.js +375 -25
- package/dist/src/am-lyrics.js.map +1 -1
- package/dist/src/react.js +375 -25
- package/dist/src/react.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/AmLyrics.ts +452 -27
package/dist/src/AmLyrics.d.ts
CHANGED
|
@@ -62,6 +62,21 @@ export declare class AmLyrics extends LitElement {
|
|
|
62
62
|
private static parseQueryMetadata;
|
|
63
63
|
private static searchLyricsPlusCatalog;
|
|
64
64
|
private static fetchLyricsFromYouLyPlus;
|
|
65
|
+
/**
|
|
66
|
+
* Parse LRC subtitle format into LyricsLine[].
|
|
67
|
+
* Handles "[mm:ss.xx] text" lines.
|
|
68
|
+
*/
|
|
69
|
+
private static parseLrcSubtitles;
|
|
70
|
+
/**
|
|
71
|
+
* Fetch lyrics from Tidal API.
|
|
72
|
+
* Picks 2 random servers, tries search + lyrics on each.
|
|
73
|
+
*/
|
|
74
|
+
private static fetchLyricsFromTidal;
|
|
75
|
+
/**
|
|
76
|
+
* Fetch lyrics from LRCLIB.
|
|
77
|
+
* Uses search endpoint, prefers synced lyrics.
|
|
78
|
+
*/
|
|
79
|
+
private static fetchLyricsFromLrclib;
|
|
65
80
|
private static convertKPoeLyrics;
|
|
66
81
|
private static toMilliseconds;
|
|
67
82
|
firstUpdated(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AmLyrics.d.ts","sourceRoot":"","sources":["../../src/AmLyrics.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,UAAU,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"AmLyrics.d.ts","sourceRoot":"","sources":["../../src/AmLyrics.ts"],"names":[],"mappings":"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"}
|
package/dist/src/am-lyrics.js
CHANGED
|
@@ -310,7 +310,7 @@ class GoogleService {
|
|
|
310
310
|
}
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
const VERSION = '1.0.
|
|
313
|
+
const VERSION = '1.0.8';
|
|
314
314
|
const INSTRUMENTAL_THRESHOLD_MS = 7000; // Show dots for gaps >= 7s
|
|
315
315
|
const KPOE_SERVERS = [
|
|
316
316
|
'https://lyricsplus.binimum.org',
|
|
@@ -320,6 +320,19 @@ const KPOE_SERVERS = [
|
|
|
320
320
|
'https://lyricsplus.prjktla.online',
|
|
321
321
|
];
|
|
322
322
|
const DEFAULT_KPOE_SOURCE_ORDER = 'apple,lyricsplus,musixmatch,spotify,musixmatch-word';
|
|
323
|
+
const TIDAL_SERVERS = [
|
|
324
|
+
'https://arran.monochrome.tf',
|
|
325
|
+
'https://api.monochrome.tf/',
|
|
326
|
+
'https://triton.squid.wtf',
|
|
327
|
+
'https://wolf.qqdl.site',
|
|
328
|
+
'https://maus.qqdl.site',
|
|
329
|
+
'https://vogel.qqdl.site',
|
|
330
|
+
'https://katze.qqdl.site',
|
|
331
|
+
'https://hund.qqdl.site',
|
|
332
|
+
'https://tidal.kinoplus.online',
|
|
333
|
+
'https://hifi-one.spotisaver.net',
|
|
334
|
+
'https://hifi-two.spotisaver.net',
|
|
335
|
+
];
|
|
323
336
|
class AmLyrics extends i {
|
|
324
337
|
constructor() {
|
|
325
338
|
super(...arguments);
|
|
@@ -466,6 +479,26 @@ class AmLyrics extends i {
|
|
|
466
479
|
return;
|
|
467
480
|
}
|
|
468
481
|
}
|
|
482
|
+
// Fallback: Tidal
|
|
483
|
+
if (resolvedMetadata?.metadata) {
|
|
484
|
+
const tidalResult = await AmLyrics.fetchLyricsFromTidal(resolvedMetadata.metadata, resolvedMetadata.catalogIsrc);
|
|
485
|
+
if (tidalResult && tidalResult.lines.length > 0) {
|
|
486
|
+
this.lyrics = tidalResult.lines;
|
|
487
|
+
this.lyricsSource = 'Tidal';
|
|
488
|
+
await this.onLyricsLoaded();
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
// Fallback: LRCLIB
|
|
493
|
+
if (resolvedMetadata?.metadata) {
|
|
494
|
+
const lrclibResult = await AmLyrics.fetchLyricsFromLrclib(resolvedMetadata.metadata);
|
|
495
|
+
if (lrclibResult && lrclibResult.lines.length > 0) {
|
|
496
|
+
this.lyrics = lrclibResult.lines;
|
|
497
|
+
this.lyricsSource = 'LRCLIB';
|
|
498
|
+
await this.onLyricsLoaded();
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
469
502
|
this.lyrics = undefined;
|
|
470
503
|
this.lyricsSource = null;
|
|
471
504
|
}
|
|
@@ -689,6 +722,196 @@ class AmLyrics extends i {
|
|
|
689
722
|
}
|
|
690
723
|
return fallbackResult;
|
|
691
724
|
}
|
|
725
|
+
/**
|
|
726
|
+
* Parse LRC subtitle format into LyricsLine[].
|
|
727
|
+
* Handles "[mm:ss.xx] text" lines.
|
|
728
|
+
*/
|
|
729
|
+
static parseLrcSubtitles(lrc) {
|
|
730
|
+
if (!lrc || typeof lrc !== 'string')
|
|
731
|
+
return [];
|
|
732
|
+
const lines = [];
|
|
733
|
+
const rawLines = lrc.split('\n');
|
|
734
|
+
const parsed = [];
|
|
735
|
+
for (const raw of rawLines) {
|
|
736
|
+
const match = raw.match(/^\[(\d{1,3}):(\d{2})\.(\d{2,3})\]\s?(.*)$/);
|
|
737
|
+
if (!match) {
|
|
738
|
+
// Skip non-timestamped lines (headers like [ti:], [ar:], etc.)
|
|
739
|
+
// eslint-disable-next-line no-continue
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const minutes = parseInt(match[1], 10);
|
|
743
|
+
const seconds = parseInt(match[2], 10);
|
|
744
|
+
let centiseconds = parseInt(match[3], 10);
|
|
745
|
+
// Handle both mm:ss.xx (centiseconds) and mm:ss.xxx (milliseconds)
|
|
746
|
+
if (match[3].length === 3) {
|
|
747
|
+
centiseconds = Math.round(centiseconds / 10);
|
|
748
|
+
}
|
|
749
|
+
const timestamp = (minutes * 60 + seconds) * 1000 + centiseconds * 10;
|
|
750
|
+
const text = match[4] || '';
|
|
751
|
+
parsed.push({ timestamp, text });
|
|
752
|
+
}
|
|
753
|
+
for (let i = 0; i < parsed.length; i += 1) {
|
|
754
|
+
const { timestamp, text } = parsed[i];
|
|
755
|
+
// Endtime is the start of the next line, or timestamp + 5s for the last line
|
|
756
|
+
const endtime = i + 1 < parsed.length ? parsed[i + 1].timestamp : timestamp + 5000;
|
|
757
|
+
// Skip empty lines (instrumental gaps)
|
|
758
|
+
if (!text.trim()) {
|
|
759
|
+
// eslint-disable-next-line no-continue
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
const syllable = {
|
|
763
|
+
text,
|
|
764
|
+
part: false,
|
|
765
|
+
timestamp,
|
|
766
|
+
endtime,
|
|
767
|
+
lineSynced: true,
|
|
768
|
+
};
|
|
769
|
+
lines.push({
|
|
770
|
+
text: [syllable],
|
|
771
|
+
background: false,
|
|
772
|
+
backgroundText: [],
|
|
773
|
+
oppositeTurn: false,
|
|
774
|
+
timestamp,
|
|
775
|
+
endtime,
|
|
776
|
+
isWordSynced: false,
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
return lines;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Fetch lyrics from Tidal API.
|
|
783
|
+
* Picks 2 random servers, tries search + lyrics on each.
|
|
784
|
+
*/
|
|
785
|
+
static async fetchLyricsFromTidal(metadata, isrc) {
|
|
786
|
+
const title = metadata.title?.trim();
|
|
787
|
+
const artist = metadata.artist?.trim();
|
|
788
|
+
if (!title || !artist)
|
|
789
|
+
return null;
|
|
790
|
+
// Pick 2 random unique servers
|
|
791
|
+
const shuffled = [...TIDAL_SERVERS].sort(() => Math.random() - 0.5);
|
|
792
|
+
const serversToTry = shuffled.slice(0, 2);
|
|
793
|
+
for (const base of serversToTry) {
|
|
794
|
+
try {
|
|
795
|
+
const normalizedBase = base.endsWith('/') ? base.slice(0, -1) : base;
|
|
796
|
+
// Step 1: Search for the track
|
|
797
|
+
const searchQuery = `${title} ${artist}`;
|
|
798
|
+
const searchParams = new URLSearchParams({ s: searchQuery });
|
|
799
|
+
// eslint-disable-next-line no-await-in-loop
|
|
800
|
+
const searchResponse = await fetch(`${normalizedBase}/search/?${searchParams.toString()}`);
|
|
801
|
+
if (!searchResponse.ok) {
|
|
802
|
+
// eslint-disable-next-line no-continue
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
// eslint-disable-next-line no-await-in-loop
|
|
806
|
+
const searchData = await searchResponse.json();
|
|
807
|
+
const items = searchData?.data?.items;
|
|
808
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
809
|
+
// eslint-disable-next-line no-continue
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
// Find best match: prefer ISRC match, then first result
|
|
813
|
+
let bestTrack = items[0];
|
|
814
|
+
if (isrc) {
|
|
815
|
+
const isrcMatch = items.find((item) => item.isrc && item.isrc.toLowerCase() === isrc.toLowerCase());
|
|
816
|
+
if (isrcMatch) {
|
|
817
|
+
bestTrack = isrcMatch;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const trackId = bestTrack?.id;
|
|
821
|
+
if (!trackId) {
|
|
822
|
+
// eslint-disable-next-line no-continue
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
// Step 2: Fetch lyrics
|
|
826
|
+
// eslint-disable-next-line no-await-in-loop
|
|
827
|
+
const lyricsResponse = await fetch(`${normalizedBase}/lyrics/?id=${trackId}`);
|
|
828
|
+
if (!lyricsResponse.ok) {
|
|
829
|
+
// eslint-disable-next-line no-continue
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
// eslint-disable-next-line no-await-in-loop
|
|
833
|
+
const lyricsData = await lyricsResponse.json();
|
|
834
|
+
const subtitles = lyricsData?.lyrics?.subtitles;
|
|
835
|
+
if (subtitles && typeof subtitles === 'string') {
|
|
836
|
+
const lines = AmLyrics.parseLrcSubtitles(subtitles);
|
|
837
|
+
if (lines.length > 0) {
|
|
838
|
+
const provider = lyricsData?.lyrics?.lyricsProvider || 'Tidal';
|
|
839
|
+
return {
|
|
840
|
+
lines,
|
|
841
|
+
source: `Tidal (${provider})`,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
// Try next server
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Fetch lyrics from LRCLIB.
|
|
854
|
+
* Uses search endpoint, prefers synced lyrics.
|
|
855
|
+
*/
|
|
856
|
+
static async fetchLyricsFromLrclib(metadata) {
|
|
857
|
+
const title = metadata.title?.trim();
|
|
858
|
+
const artist = metadata.artist?.trim();
|
|
859
|
+
if (!title || !artist)
|
|
860
|
+
return null;
|
|
861
|
+
try {
|
|
862
|
+
const searchQuery = `${title} ${artist}`;
|
|
863
|
+
const params = new URLSearchParams({ q: searchQuery });
|
|
864
|
+
const response = await fetch(`https://lrclib.net/api/search?${params.toString()}`, {
|
|
865
|
+
headers: {
|
|
866
|
+
'User-Agent': `apple-music-web-components/${VERSION}`,
|
|
867
|
+
},
|
|
868
|
+
});
|
|
869
|
+
if (!response.ok)
|
|
870
|
+
return null;
|
|
871
|
+
const results = await response.json();
|
|
872
|
+
if (!Array.isArray(results) || results.length === 0)
|
|
873
|
+
return null;
|
|
874
|
+
// Prefer results with synced lyrics
|
|
875
|
+
const withSynced = results.find((r) => r.syncedLyrics && typeof r.syncedLyrics === 'string');
|
|
876
|
+
const bestMatch = withSynced || results[0];
|
|
877
|
+
// Try synced lyrics first
|
|
878
|
+
if (bestMatch.syncedLyrics) {
|
|
879
|
+
const lines = AmLyrics.parseLrcSubtitles(bestMatch.syncedLyrics);
|
|
880
|
+
if (lines.length > 0) {
|
|
881
|
+
return { lines, source: 'LRCLIB' };
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
// Fall back to plain lyrics (unsynced)
|
|
885
|
+
if (bestMatch.plainLyrics && typeof bestMatch.plainLyrics === 'string') {
|
|
886
|
+
const plainLines = bestMatch.plainLyrics
|
|
887
|
+
.split('\n')
|
|
888
|
+
.filter((l) => l.trim());
|
|
889
|
+
if (plainLines.length > 0) {
|
|
890
|
+
const lines = plainLines.map((text) => ({
|
|
891
|
+
text: [
|
|
892
|
+
{
|
|
893
|
+
text,
|
|
894
|
+
part: false,
|
|
895
|
+
timestamp: 0,
|
|
896
|
+
endtime: 0,
|
|
897
|
+
},
|
|
898
|
+
],
|
|
899
|
+
background: false,
|
|
900
|
+
backgroundText: [],
|
|
901
|
+
oppositeTurn: false,
|
|
902
|
+
timestamp: 0,
|
|
903
|
+
endtime: 0,
|
|
904
|
+
isWordSynced: false,
|
|
905
|
+
}));
|
|
906
|
+
return { lines, source: 'LRCLIB (unsynced)' };
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
catch {
|
|
911
|
+
// LRCLIB fetch failed
|
|
912
|
+
}
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
692
915
|
static convertKPoeLyrics(payload) {
|
|
693
916
|
if (!payload) {
|
|
694
917
|
return null;
|
|
@@ -2265,28 +2488,160 @@ class AmLyrics extends i {
|
|
|
2265
2488
|
wordGroups.push([syllable]);
|
|
2266
2489
|
}
|
|
2267
2490
|
}
|
|
2491
|
+
// Pre-compute isGrowable per "visual word": adjacent groups whose text
|
|
2492
|
+
// doesn't end with whitespace form one visual word (e.g. "a"+"live" = "alive").
|
|
2493
|
+
// We evaluate growable on the combined text/duration, then propagate
|
|
2494
|
+
// the result to each individual group so it renders through the
|
|
2495
|
+
// single-syllable path (which supports char-level glow).
|
|
2496
|
+
const groupGrowable = new Array(wordGroups.length).fill(false);
|
|
2497
|
+
// Visual word info for growable char-level glow:
|
|
2498
|
+
// Each group stores the combined visual word's text, duration, and
|
|
2499
|
+
// the char offset of this group within the visual word.
|
|
2500
|
+
const vwFullText = new Array(wordGroups.length).fill('');
|
|
2501
|
+
const vwFullDuration = new Array(wordGroups.length).fill(0);
|
|
2502
|
+
const vwCharOffset = new Array(wordGroups.length).fill(0);
|
|
2503
|
+
const vwStartMs = new Array(wordGroups.length).fill(0);
|
|
2504
|
+
const vwEndMs = new Array(wordGroups.length).fill(0);
|
|
2505
|
+
{
|
|
2506
|
+
let vwStart = 0;
|
|
2507
|
+
while (vwStart < wordGroups.length) {
|
|
2508
|
+
let vwEnd = vwStart;
|
|
2509
|
+
while (vwEnd < wordGroups.length - 1) {
|
|
2510
|
+
const grp = wordGroups[vwEnd];
|
|
2511
|
+
const lastText = grp[grp.length - 1].text;
|
|
2512
|
+
if (/\s$/.test(lastText))
|
|
2513
|
+
break;
|
|
2514
|
+
vwEnd += 1;
|
|
2515
|
+
}
|
|
2516
|
+
// Compute combined properties for this visual word
|
|
2517
|
+
const combinedText = wordGroups
|
|
2518
|
+
.slice(vwStart, vwEnd + 1)
|
|
2519
|
+
.flatMap(g => g.map(s => s.text))
|
|
2520
|
+
.join('')
|
|
2521
|
+
.trim();
|
|
2522
|
+
const combinedStart = wordGroups[vwStart][0].timestamp;
|
|
2523
|
+
const lastGrp = wordGroups[vwEnd];
|
|
2524
|
+
const combinedEnd = lastGrp[lastGrp.length - 1].endtime;
|
|
2525
|
+
const combinedDuration = combinedEnd - combinedStart;
|
|
2526
|
+
const isCJK = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(combinedText);
|
|
2527
|
+
const isRTL = /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\u0590-\u05FF]/.test(combinedText);
|
|
2528
|
+
const hasHyphen = combinedText.includes('-');
|
|
2529
|
+
const isGrowableVW = !isCJK &&
|
|
2530
|
+
!isRTL &&
|
|
2531
|
+
!hasHyphen &&
|
|
2532
|
+
combinedText.length <= 7 &&
|
|
2533
|
+
combinedText.length > 0 &&
|
|
2534
|
+
combinedDuration >= 900 &&
|
|
2535
|
+
combinedDuration >= combinedText.length * 300 &&
|
|
2536
|
+
(combinedText.length >= 4 ||
|
|
2537
|
+
combinedDuration / combinedText.length >= 600);
|
|
2538
|
+
let charOff = 0;
|
|
2539
|
+
for (let gi = vwStart; gi <= vwEnd; gi += 1) {
|
|
2540
|
+
groupGrowable[gi] = isGrowableVW;
|
|
2541
|
+
vwFullText[gi] = combinedText;
|
|
2542
|
+
vwFullDuration[gi] = combinedDuration;
|
|
2543
|
+
vwCharOffset[gi] = charOff;
|
|
2544
|
+
vwStartMs[gi] = combinedStart;
|
|
2545
|
+
vwEndMs[gi] = combinedEnd;
|
|
2546
|
+
const grpText = wordGroups[gi].map(s => s.text).join('');
|
|
2547
|
+
charOff += grpText.replace(/\s/g, '').length;
|
|
2548
|
+
}
|
|
2549
|
+
vwStart = vwEnd + 1;
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2268
2552
|
// Create main vocals using YouLyPlus syllable structure
|
|
2269
2553
|
const mainVocalElement = b `<p class="main-vocal-container">
|
|
2270
|
-
${wordGroups.map(group => {
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2554
|
+
${wordGroups.map((group, groupIdx) => {
|
|
2555
|
+
const isGrowable = groupGrowable[groupIdx];
|
|
2556
|
+
// For growable visual words spanning multiple groups:
|
|
2557
|
+
// skip continuation groups (rendered by the first group)
|
|
2558
|
+
if (isGrowable && vwCharOffset[groupIdx] > 0) {
|
|
2559
|
+
return '';
|
|
2560
|
+
}
|
|
2277
2561
|
// Check if ANY syllable in group is line-synced
|
|
2278
2562
|
const groupLineSynced = group.some(s => s.lineSynced);
|
|
2279
|
-
//
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2563
|
+
// For growable multi-group visual words, combine all text
|
|
2564
|
+
// into one syllable so the wipe + glow animates as one unit
|
|
2565
|
+
if (isGrowable && vwFullText[groupIdx].length > 0) {
|
|
2566
|
+
const wordText = vwFullText[groupIdx];
|
|
2567
|
+
const wordDuration = vwFullDuration[groupIdx];
|
|
2568
|
+
const startTimeMs = vwStartMs[groupIdx];
|
|
2569
|
+
const endTimeMs = vwEndMs[groupIdx];
|
|
2570
|
+
const numChars = wordText.length;
|
|
2571
|
+
// Collect all text from groups in this visual word
|
|
2572
|
+
// by scanning forward while vwCharOffset is consecutive
|
|
2573
|
+
let combinedRawText = '';
|
|
2574
|
+
for (let gi = groupIdx; gi < wordGroups.length; gi += 1) {
|
|
2575
|
+
if (gi > groupIdx && vwCharOffset[gi] === 0)
|
|
2576
|
+
break;
|
|
2577
|
+
if (gi > groupIdx && !groupGrowable[gi])
|
|
2578
|
+
break;
|
|
2579
|
+
combinedRawText += wordGroups[gi].map(s => s.text).join('');
|
|
2580
|
+
}
|
|
2581
|
+
const syllableContent = b `${combinedRawText
|
|
2582
|
+
.split('')
|
|
2583
|
+
.map((char, charIndex) => {
|
|
2584
|
+
if (char === ' ') {
|
|
2585
|
+
return ' ';
|
|
2586
|
+
}
|
|
2587
|
+
const charStartPercent = charIndex / numChars;
|
|
2588
|
+
const minDuration = 1000;
|
|
2589
|
+
const maxDuration = 5000;
|
|
2590
|
+
const easingPower = 3;
|
|
2591
|
+
const progress = Math.min(1, Math.max(0, (wordDuration - minDuration) /
|
|
2592
|
+
(maxDuration - minDuration)));
|
|
2593
|
+
const easedProgress = progress ** easingPower;
|
|
2594
|
+
const isLongWord = numChars > 5;
|
|
2595
|
+
const isShortDuration = wordDuration < 1500;
|
|
2596
|
+
let maxDecayRate = 0;
|
|
2597
|
+
if (isLongWord || isShortDuration) {
|
|
2598
|
+
let decayStrength = 0;
|
|
2599
|
+
if (isLongWord)
|
|
2600
|
+
decayStrength += Math.min((numChars - 5) / 3, 1.0) * 0.4;
|
|
2601
|
+
if (isShortDuration)
|
|
2602
|
+
decayStrength +=
|
|
2603
|
+
Math.max(0, 1.0 - (wordDuration - 1000) / 500) * 0.4;
|
|
2604
|
+
maxDecayRate = Math.min(decayStrength, 0.85);
|
|
2605
|
+
}
|
|
2606
|
+
const positionInWord = numChars > 1 ? charIndex / (numChars - 1) : 0;
|
|
2607
|
+
const decayFactor = 1.0 - positionInWord * maxDecayRate;
|
|
2608
|
+
const charProgress = easedProgress * decayFactor;
|
|
2609
|
+
const baseGrowth = numChars <= 3 ? 0.07 : 0.05;
|
|
2610
|
+
const charMaxScale = 1.0 + baseGrowth + charProgress * 0.1;
|
|
2611
|
+
const charShadowIntensity = 0.4 + charProgress * 0.4;
|
|
2612
|
+
const normalizedGrowth = (charMaxScale - 1.0) / 0.13;
|
|
2613
|
+
const charTranslateYPeak = -normalizedGrowth * 6;
|
|
2614
|
+
const position = (charIndex + 0.5) / numChars;
|
|
2615
|
+
const horizontalOffset = (position - 0.5) * 2 * ((charMaxScale - 1.0) * 25);
|
|
2616
|
+
return b `<span
|
|
2617
|
+
class="char"
|
|
2618
|
+
data-char-index="${charIndex}"
|
|
2619
|
+
data-syllable-char-index="${charIndex}"
|
|
2620
|
+
data-wipe-start="${charStartPercent.toFixed(4)}"
|
|
2621
|
+
data-wipe-duration="${(1 / numChars).toFixed(4)}"
|
|
2622
|
+
data-horizontal-offset="${horizontalOffset.toFixed(2)}"
|
|
2623
|
+
data-max-scale="${charMaxScale.toFixed(3)}"
|
|
2624
|
+
data-shadow-intensity="${charShadowIntensity.toFixed(3)}"
|
|
2625
|
+
data-translate-y-peak="${charTranslateYPeak.toFixed(3)}"
|
|
2626
|
+
>${char}</span
|
|
2627
|
+
>`;
|
|
2628
|
+
})}`;
|
|
2629
|
+
return b `<span class="lyrics-word growable">
|
|
2630
|
+
<span class="lyrics-syllable-wrap">
|
|
2631
|
+
<span
|
|
2632
|
+
class="lyrics-syllable ${groupLineSynced
|
|
2633
|
+
? 'line-synced'
|
|
2634
|
+
: ''}"
|
|
2635
|
+
data-start-time="${startTimeMs}"
|
|
2636
|
+
data-end-time="${endTimeMs}"
|
|
2637
|
+
data-duration="${wordDuration}"
|
|
2638
|
+
data-syllable-index="0"
|
|
2639
|
+
data-wipe-ratio="1"
|
|
2640
|
+
>${syllableContent}</span
|
|
2641
|
+
>
|
|
2642
|
+
</span>
|
|
2643
|
+
</span>`;
|
|
2644
|
+
}
|
|
2290
2645
|
// For single-syllable groups, use original logic
|
|
2291
2646
|
if (group.length === 1) {
|
|
2292
2647
|
const syllable = group[0];
|
|
@@ -2311,7 +2666,7 @@ class AmLyrics extends i {
|
|
|
2311
2666
|
>${syllable.romanizedText}</span
|
|
2312
2667
|
>`
|
|
2313
2668
|
: '';
|
|
2314
|
-
// For growable words
|
|
2669
|
+
// For growable words (single-group visual word), use char glow
|
|
2315
2670
|
const syllableContent = isGrowable
|
|
2316
2671
|
? b `${text.split('').map((char, charIndex) => {
|
|
2317
2672
|
if (char === ' ') {
|
|
@@ -2319,14 +2674,12 @@ class AmLyrics extends i {
|
|
|
2319
2674
|
}
|
|
2320
2675
|
const numChars = trimmedText.length;
|
|
2321
2676
|
const charStartPercent = charIndex / text.length;
|
|
2322
|
-
// YouLyPlus emphasisMetrics calculation
|
|
2323
2677
|
const minDuration = 1000;
|
|
2324
2678
|
const maxDuration = 5000;
|
|
2325
2679
|
const easingPower = 3;
|
|
2326
2680
|
const progress = Math.min(1, Math.max(0, (durationMs - minDuration) /
|
|
2327
2681
|
(maxDuration - minDuration)));
|
|
2328
2682
|
const easedProgress = progress ** easingPower;
|
|
2329
|
-
// Decay calculation for long/short words
|
|
2330
2683
|
const isLongWord = numChars > 5;
|
|
2331
2684
|
const isShortDuration = durationMs < 1500;
|
|
2332
2685
|
let maxDecayRate = 0;
|
|
@@ -2340,7 +2693,6 @@ class AmLyrics extends i {
|
|
|
2340
2693
|
Math.max(0, 1.0 - (durationMs - 1000) / 500) * 0.4;
|
|
2341
2694
|
maxDecayRate = Math.min(decayStrength, 0.85);
|
|
2342
2695
|
}
|
|
2343
|
-
// Per-character calculations (exact YouLyPlus logic)
|
|
2344
2696
|
const positionInWord = numChars > 1 ? charIndex / (numChars - 1) : 0;
|
|
2345
2697
|
const decayFactor = 1.0 - positionInWord * maxDecayRate;
|
|
2346
2698
|
const charProgress = easedProgress * decayFactor;
|
|
@@ -2349,10 +2701,8 @@ class AmLyrics extends i {
|
|
|
2349
2701
|
const charShadowIntensity = 0.4 + charProgress * 0.4;
|
|
2350
2702
|
const normalizedGrowth = (charMaxScale - 1.0) / 0.13;
|
|
2351
2703
|
const charTranslateYPeak = -normalizedGrowth * 6;
|
|
2352
|
-
// Horizontal offset (simplified - YouLyPlus uses actual text width measurement)
|
|
2353
2704
|
const position = (charIndex + 0.5) / numChars;
|
|
2354
2705
|
const horizontalOffset = (position - 0.5) * 2 * ((charMaxScale - 1.0) * 25);
|
|
2355
|
-
// MOVED TO DATA ATTRIBUTES and removed style attribute to avoid Lit conflict
|
|
2356
2706
|
return b `<span
|
|
2357
2707
|
class="char"
|
|
2358
2708
|
data-char-index="${charIndex}"
|