diff --git a/plugin/contents/ui/dancer.html b/plugin/contents/ui/dancer.html
index 7345ffb..0602200 100644
--- a/plugin/contents/ui/dancer.html
+++ b/plugin/contents/ui/dancer.html
@@ -237,18 +237,20 @@ html, body {
}
/* ============================================================
- LYRICS DISPLAY — 5-line karaoke scroll
+ LYRICS DISPLAY — verse karaoke
============================================================ */
.lyric-display {
position: absolute;
left: 2%;
top: 4%;
- width: 38%; /* explicit width needed for abs-pos children */
- height: 150px; /* 5 slots × 30px */
+ width: 38%;
+ height: 150px; /* 5 visible slots × 30px */
overflow: hidden;
pointer-events: none;
opacity: 0;
transition: opacity 0.5s ease;
+ -webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 15%, black 85%, transparent 100%);
+ mask-image: linear-gradient(to bottom, transparent 0%, black 15%, black 85%, transparent 100%);
}
.lyric-ctx {
position: absolute;
@@ -894,14 +896,8 @@ html, body {
-
-
+
+
@@ -1180,91 +1176,91 @@ const lyricDisplay = document.getElementById('lyricDisplay');
let lastArtUrl = '';
-/* ── karaoke scroll ── */
-const SLOT_H = 30; // px per slot — must match CSS height: 150px / 5
-const SCROLL_MS = 500; // must match CSS transition: top 0.5s
+/* ── verse display ── */
+const SLOT_H = 30;
+const CENTER_PX = 60; // top of the active slot within the 150px container
const ACTIVE_SHADOW =
'0 0 18px var(--theme-color,rgba(0,200,255,0.85)),' +
'0 0 6px var(--theme-color,rgba(0,200,255,0.5)),' +
'0 2px 10px rgba(0,0,0,0.95)';
-const SLOT_PROPS = [
- { opacity:'0.2', fontSize:'clamp(11px,0.85vw,13px)', color:'#777', shadow:'' },
- { opacity:'0.48', fontSize:'clamp(13px,1.0vw,15px)', color:'#bbb', shadow:'' },
- { opacity:'1', fontSize:'clamp(16px,1.3vw,18px)', color:'#fff', shadow: ACTIVE_SHADOW },
- { opacity:'0.48', fontSize:'clamp(13px,1.0vw,15px)', color:'#bbb', shadow:'' },
- { opacity:'0.2', fontSize:'clamp(11px,0.85vw,13px)', color:'#777', shadow:'' },
+const VERSE_STYLES = [
+ { opacity:'0.2', fontSize:'clamp(11px,0.85vw,13px)', color:'#777', shadow:'' }, // dist -2
+ { opacity:'0.48', fontSize:'clamp(13px,1.0vw,15px)', color:'#bbb', shadow:'' }, // dist -1
+ { opacity:'1', fontSize:'clamp(16px,1.3vw,18px)', color:'#fff', shadow: ACTIVE_SHADOW }, // dist 0
+ { opacity:'0.48', fontSize:'clamp(13px,1.0vw,15px)', color:'#bbb', shadow:'' }, // dist +1
+ { opacity:'0.2', fontSize:'clamp(11px,0.85vw,13px)', color:'#777', shadow:'' }, // dist +2
];
-let ring = Array.from(document.querySelectorAll('.lyric-ctx'));
-let currentActive = '';
-let scrollBusy = false;
+let verseEls = [];
+let currentVerseKey = '';
+let currentVerseActive = -1;
-function applySlotVisuals(el, slot) {
- if (slot < 0 || slot > 4) { el.style.opacity = '0'; return; }
- const p = SLOT_PROPS[slot];
+function applyVerseStyle(el, dist) {
+ if (dist < -2 || dist > 2) {
+ el.style.opacity = '0'; el.style.fontSize = 'clamp(11px,0.85vw,13px)';
+ el.style.color = '#777'; el.style.textShadow = ''; return;
+ }
+ const p = VERSE_STYLES[dist + 2];
el.style.opacity = p.opacity;
el.style.fontSize = p.fontSize;
el.style.color = p.color;
el.style.textShadow = p.shadow;
}
-function initSlots(context) {
- ring.forEach((el, i) => {
- el.style.transition = 'none';
- el.textContent = context[i]?.text ?? '';
- el.style.top = (i * SLOT_H) + 'px';
- applySlotVisuals(el, i);
+function positionVerseEls(verse_active) {
+ verseEls.forEach((el, i) => {
+ const dist = i - verse_active;
+ el.style.top = (CENTER_PX + dist * SLOT_H) + 'px';
+ applyVerseStyle(el, dist);
});
+}
+
+function buildVerse(verse_lines, verse_active) {
+ lyricDisplay.innerHTML = '';
+ verseEls = verse_lines.map(text => {
+ const el = document.createElement('div');
+ el.className = 'lyric-ctx';
+ el.textContent = text;
+ el.style.transition = 'none';
+ lyricDisplay.appendChild(el);
+ return el;
+ });
+ positionVerseEls(verse_active);
requestAnimationFrame(() => requestAnimationFrame(() => {
- ring.forEach(el => { el.style.transition = ''; });
+ verseEls.forEach(el => { el.style.transition = ''; });
}));
}
-function scrollUp(context) {
- if (scrollBusy) return;
- scrollBusy = true;
- ring.forEach((el, i) => {
- el.style.top = ((i - 1) * SLOT_H) + 'px';
- applySlotVisuals(el, i - 1);
- });
- setTimeout(() => {
- const recycled = ring[0];
- recycled.style.transition = 'none';
- recycled.textContent = context[4]?.text ?? '';
- recycled.style.top = (4 * SLOT_H) + 'px';
- applySlotVisuals(recycled, 4);
- requestAnimationFrame(() => requestAnimationFrame(() => {
- recycled.style.transition = '';
- }));
- ring = [...ring.slice(1), ring[0]];
- scrollBusy = false;
- }, SCROLL_MS + 50);
-}
-
-function updateLyricContext(context) {
- if (!context || !context.length || !isPlaying) {
+function updateLyricVerse(verse_lines, verse_active) {
+ if (!verse_lines || !verse_lines.length || verse_active < 0 || !isPlaying) {
lyricDisplay.style.opacity = '0';
+ currentVerseKey = '';
+ currentVerseActive = -1;
return;
}
- const active = context.find(c => c.offset === 0);
- if (!active || !active.text) {
- lyricDisplay.style.opacity = '0';
- return;
- }
- if (active.text === currentActive) {
- lyricDisplay.style.opacity = '1';
- return;
- }
- currentActive = active.text;
+ const verseKey = verse_lines.join('\x00');
- if (lyricDisplay.style.opacity !== '1') {
- initSlots(context);
+ if (verseKey !== currentVerseKey) {
+ currentVerseKey = verseKey;
+ currentVerseActive = verse_active;
+ if (lyricDisplay.style.opacity === '1') {
+ lyricDisplay.style.opacity = '0';
+ setTimeout(() => {
+ buildVerse(verse_lines, verse_active);
+ lyricDisplay.style.opacity = '1';
+ }, 500);
+ } else {
+ buildVerse(verse_lines, verse_active);
+ lyricDisplay.style.opacity = '1';
+ }
+ } else if (verse_active !== currentVerseActive) {
+ currentVerseActive = verse_active;
+ positionVerseEls(verse_active);
} else {
- scrollUp(context);
+ lyricDisplay.style.opacity = '1';
}
- lyricDisplay.style.opacity = '1';
}
function fetchCurrent() {
@@ -1277,7 +1273,7 @@ function fetchCurrent() {
document.getElementById('albumArtImg').src =
'http://127.0.0.1:8765/art?' + Date.now();
}
- updateLyricContext(d.lyric_context || []);
+ updateLyricVerse(d.verse_lines || [], d.verse_active ?? -1);
})
.catch(() => {});
}
@@ -1340,7 +1336,8 @@ function updateMusic(artist, title) {
if (!playing) {
vuBars.forEach(b => { b.style.height = '3px'; b.style.opacity = '0.15'; });
lyricDisplay.style.opacity = '0';
- currentActive = '';
+ currentVerseKey = '';
+ currentVerseActive = -1;
}
if (playing) fetchCurrent();
const key = artist + '|' + title;