Compare commits

..

5 commits

Author SHA1 Message Date
ltadeu6
4c4b1bbb96
feat: verse-based lyric display with smooth active-line transitions
Replace circular ring-buffer scroll with verse-aware display: backend
now groups lyrics into verses on empty lines or timestamp gaps >3.5s
and exposes verse_lines + verse_active; frontend rebuilds the DOM once
per verse (fade-out/in) and animates only opacity/font/glow when the
active line advances within the same verse.  CSS mask-image gradient
replaces hard overflow clipping at top and bottom edges.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 17:33:12 -03:00
ltadeu6
2b05d5844d
fix: explicit width on lyric-display + simplify scroll logic
position:absolute children with width:100% require explicit parent width.
max-width alone leaves the container at 0px, hiding all text via overflow:hidden.
Also removed jump-detection branch to avoid opacity:0 dead-ends.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 17:16:30 -03:00
ltadeu6
b75cec0107
fix: correct stale currentLyricKey ref → currentActive in updateMusic
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 17:07:41 -03:00
ltadeu6
fdc68594d5
feat: smooth karaoke scroll — no blink on lyric advance
Replace blink-all transition with a circular ring buffer: each lyric
line is absolutely positioned (30px slot). On advance the ring scrolls
up, the outgoing element is recycled to the bottom with new content
(transition disabled for the snap). All style properties (top, opacity,
font-size, color, text-shadow) animate together via CSS transition so
the new active line gains focus as it scrolls into center position.
Jumps (seek / track change) still use a crossfade.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 17:05:40 -03:00
ltadeu6
10b3bbbf2e
feat: multi-player support + 5-line karaoke lyrics display
- get-playing.sh: iterates playerctl players, picks the one actually
  Playing; QML now calls this instead of bare playerctl metadata so
  a paused Spotify no longer blocks a playing YouTube from showing

- dancer.html: replace single lyric line with 5-slot karaoke display —
  active line (center) is full-brightness with theme-color glow; 2 lines
  before/after are progressively dimmed; slots fade out/in (0.18s/0.45s)
  when the active line advances

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 16:53:17 -03:00
3 changed files with 130 additions and 30 deletions

View file

@ -237,29 +237,38 @@ html, body {
}
/* ============================================================
LYRICS DISPLAY
LYRICS DISPLAY — verse karaoke
============================================================ */
.lyric-display {
position: absolute;
left: 2%;
top: 4%;
max-width: 36%;
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-line {
color: #ffffff;
.lyric-ctx {
position: absolute;
left: 0;
width: 100%;
height: 30px;
line-height: 30px;
font-family: 'Courier New', monospace;
font-size: clamp(16px, 1.3vw, 18px);
line-height: 1.5;
letter-spacing: 0.03em;
text-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),
-1px -1px 0 rgba(0,0,0,0.7),
1px 1px 0 rgba(0,0,0,0.7);
letter-spacing: 0.02em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition:
top 0.5s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.45s ease,
font-size 0.35s ease,
color 0.35s ease,
text-shadow 0.35s ease;
}
/* ============================================================
@ -887,10 +896,8 @@ html, body {
</div>
</div>
<!-- Lyrics display -->
<div class="lyric-display" id="lyricDisplay">
<div class="lyric-line" id="lyricLine"></div>
</div>
<!-- Lyrics display — verse karaoke -->
<div class="lyric-display" id="lyricDisplay"></div>
<!-- Scanlines overlay -->
<div class="scanlines"></div>
@ -1166,10 +1173,95 @@ function applyTheme(rgb) {
const albumArtEl = document.getElementById('albumArt');
const fogWisps = document.querySelectorAll('.fog-wisp');
const lyricDisplay = document.getElementById('lyricDisplay');
const lyricLineEl = document.getElementById('lyricLine');
let lastArtUrl = '';
let lastLyricLine = '';
/* ── 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 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 verseEls = [];
let currentVerseKey = '';
let currentVerseActive = -1;
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 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(() => {
verseEls.forEach(el => { el.style.transition = ''; });
}));
}
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 verseKey = verse_lines.join('\x00');
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 {
lyricDisplay.style.opacity = '1';
}
}
function fetchCurrent() {
fetch('http://127.0.0.1:8765/current')
@ -1181,12 +1273,7 @@ function fetchCurrent() {
document.getElementById('albumArtImg').src =
'http://127.0.0.1:8765/art?' + Date.now();
}
const line = d.lyric_line || '';
if (line !== lastLyricLine) {
lastLyricLine = line;
lyricLineEl.textContent = line;
}
lyricDisplay.style.opacity = (isPlaying && line) ? '1' : '0';
updateLyricVerse(d.verse_lines || [], d.verse_active ?? -1);
})
.catch(() => {});
}
@ -1246,7 +1333,12 @@ function updateMusic(artist, title) {
document.querySelector('.robot-wrap').style.opacity = playing ? '1' : '0';
document.querySelector('.music-ticker').style.opacity = playing ? '1' : '0';
albumArtEl.style.opacity = playing ? '1' : '0';
if (!playing) vuBars.forEach(b => { b.style.height = '3px'; b.style.opacity = '0.15'; });
if (!playing) {
vuBars.forEach(b => { b.style.height = '3px'; b.style.opacity = '0.15'; });
lyricDisplay.style.opacity = '0';
currentVerseKey = '';
currentVerseActive = -1;
}
if (playing) fetchCurrent();
const key = artist + '|' + title;
if (el.dataset.key === key) return;

View file

@ -0,0 +1,8 @@
#!/bin/bash
for player in $(playerctl -l 2>/dev/null); do
if [ "$(playerctl --player "$player" status 2>/dev/null)" = "Playing" ]; then
playerctl --player "$player" metadata --format "{{status}}||{{artist}}||{{title}}" 2>/dev/null
exit 0
fi
done
printf "||"

View file

@ -38,7 +38,7 @@ WallpaperItem {
Plasma5Support.DataSource {
id: musicSource
engine: "executable"
connectedSources: ["playerctl metadata --format '{{status}}||{{artist}}||{{title}}' 2>/dev/null || echo '||'"]
connectedSources: ["bash " + Qt.resolvedUrl("get-playing.sh").toString().replace("file://", "")]
interval: 2000
onNewData: function(sourceName, data) {