ui(player): soften vinyl-stage halo, transparent-bg album placeholder, crossfade artwork swaps
Lint & Test / test (push) Successful in 1m50s

- vinyl-stage background fades to transparent (matches fullscreen Listening Room) — no more rectangular dark card around the sleeve+disc composition
- album-art placeholder SVG drops the opaque #282828 backdrop in favour of a translucent disc glyph so the sleeve cardstock shows through before the first artwork load
- new swapArtworkSrc helper retriggers the .is-swapping keyframes so artwork changes crossfade instead of popping
This commit is contained in:
2026-04-25 15:22:08 +03:00
parent 59840a1190
commit 4c93bfb8c1
3 changed files with 61 additions and 29 deletions
+45 -25
View File
@@ -4432,6 +4432,12 @@ header .brand-sub {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow: visible; overflow: visible;
/* Kill the legacy albumArt3D rotateY/rotateX wobble inherited from
.album-art-container — the vinyl stage is a flat composition
and should not tilt. */
animation: none;
transform: none;
transform-style: flat;
} }
.album-art-container.vinyl-stage:hover { transform: none; } .album-art-container.vinyl-stage:hover { transform: none; }
@@ -4612,14 +4618,27 @@ body.visualizer-active .vinyl-stage .spectrogram-canvas {
filter: blur(40px) saturate(1.8); filter: blur(40px) saturate(1.8);
} }
/* Stage gets a warm ambient gradient (matches mockup) */ /* Honour reduced-motion: kill breathing pulse */
@media (prefers-reduced-motion: reduce) {
.vinyl-stage .sleeve #album-art {
animation: none !important;
}
}
/* Stage halo: soft radial bloom that fades to transparent — same
treatment as the fullscreen player so the stage never reads as a
rectangular card pasted into the page. */
.album-art-container.vinyl-stage { .album-art-container.vinyl-stage {
background: background:
radial-gradient(ellipse at center, #1a1611 0%, var(--bg-deep) 80%); radial-gradient(ellipse at center,
rgba(26, 22, 17, 0.85) 0%,
transparent 70%);
} }
:root[data-theme="light"] .album-art-container.vinyl-stage { :root[data-theme="light"] .album-art-container.vinyl-stage {
background: background:
radial-gradient(ellipse at center, var(--bg-card-2) 0%, var(--bg-deep) 80%); radial-gradient(ellipse at center,
rgba(255, 255, 255, 0.55) 0%,
transparent 75%);
} }
/* Sleeve: cardstock card with album cover printed on its face. /* Sleeve: cardstock card with album cover printed on its face.
@@ -4632,17 +4651,15 @@ body.visualizer-active .vinyl-stage .spectrogram-canvas {
width: 63%; width: 63%;
aspect-ratio: 1; aspect-ratio: 1;
z-index: 3; z-index: 3;
background: var(--bg-card-2); background: transparent;
box-shadow: box-shadow:
inset 4px 4px 24px rgba(0, 0, 0, 0.35),
-2px 8px 24px rgba(0, 0, 0, 0.5), -2px 8px 24px rgba(0, 0, 0, 0.5),
-4px 18px 44px rgba(0, 0, 0, 0.35); -4px 18px 44px rgba(0, 0, 0, 0.35);
overflow: hidden; overflow: hidden;
} }
:root[data-theme="light"] .vinyl-stage .sleeve { :root[data-theme="light"] .vinyl-stage .sleeve {
background: var(--bg-card); background: transparent;
box-shadow: box-shadow:
inset 4px 4px 18px rgba(0, 0, 0, 0.10),
-2px 8px 22px rgba(0, 0, 0, 0.20), -2px 8px 22px rgba(0, 0, 0, 0.20),
-4px 18px 36px rgba(0, 0, 0, 0.12); -4px 18px 36px rgba(0, 0, 0, 0.12);
} }
@@ -4655,22 +4672,31 @@ body.visualizer-active .vinyl-stage .spectrogram-canvas {
size, blowing out the grid track. */ size, blowing out the grid track. */
.vinyl-stage .sleeve #album-art { .vinyl-stage .sleeve #album-art {
position: absolute; position: absolute;
top: 5%; top: 0;
left: 5%; left: 0;
width: 90%; width: 100%;
height: 90%; height: 100%;
object-fit: cover; object-fit: cover;
border-radius: 0; border-radius: 0;
z-index: 1; z-index: 1;
box-shadow: box-shadow: none;
inset 0 0 18px rgba(0, 0, 0, 0.35),
0 2px 6px rgba(0, 0, 0, 0.35);
margin: 0; margin: 0;
background: var(--bg-card); background: transparent;
filter: contrast(0.96) saturate(0.92); filter: contrast(0.96) saturate(0.92);
transition: filter 0.6s ease; transition: filter 0.6s ease;
} }
/* Crossfade on artwork swap. Class toggled by player.js right before
src assignment so the new cover fades in instead of popping. */
.vinyl-stage .sleeve #album-art.is-swapping {
animation: sr-art-swap 650ms cubic-bezier(0.2, 0.7, 0.2, 1);
}
@keyframes sr-art-swap {
0% { opacity: 0; }
100% { opacity: 1; }
}
/* Cardstock paper grain over the print — multiplies into the image */ /* Cardstock paper grain over the print — multiplies into the image */
.vinyl-stage .sleeve-grain { .vinyl-stage .sleeve-grain {
position: absolute; position: absolute;
@@ -4683,17 +4709,11 @@ body.visualizer-active .vinyl-stage .spectrogram-canvas {
} }
:root[data-theme="light"] .vinyl-stage .sleeve-grain { opacity: 0.32; } :root[data-theme="light"] .vinyl-stage .sleeve-grain { opacity: 0.32; }
/* Worn corner notch — tiny triangular wear on bottom-right */ /* Worn corner notch — hidden now that the cardstock sleeve is
transparent; the wedge would otherwise read as a stray dark
triangle clipped over the album art. */
.vinyl-stage .sleeve-corner { .vinyl-stage .sleeve-corner {
position: absolute; display: none;
width: 13%;
height: 13%;
bottom: -1px;
right: -1px;
background: var(--bg-deep);
clip-path: polygon(100% 0, 100% 100%, 0 100%);
opacity: 0.6;
z-index: 4;
} }
/* Disc wrap — sits behind the sleeve, peeks out the right edge */ /* Disc wrap — sits behind the sleeve, peeks out the right edge */
+1 -1
View File
@@ -187,7 +187,7 @@
<div class="vinyl-stage album-art-container"> <div class="vinyl-stage album-art-container">
<img id="album-art-glow" class="album-art-glow" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Crect fill='%23282828' width='300' height='300'/%3E%3C/svg%3E" alt="" aria-hidden="true"> <img id="album-art-glow" class="album-art-glow" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Crect fill='%23282828' width='300' height='300'/%3E%3C/svg%3E" alt="" aria-hidden="true">
<div class="sleeve"> <div class="sleeve">
<img id="album-art" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Crect fill='%23282828' width='300' height='300'/%3E%3Cpath fill='%236a6a6a' d='M150 80c-38.66 0-70 31.34-70 70s31.34 70 70 70 70-31.34 70-70-31.34-70-70-70zm0 20c27.614 0 50 22.386 50 50s-22.386 50-50 50-50-22.386-50-50 22.386-50 50-50zm0 30a20 20 0 100 40 20 20 0 000-40z'/%3E%3C/svg%3E" alt="Album Art"> <img id="album-art" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Cpath fill='%236a6a6a' opacity='0.35' d='M150 80c-38.66 0-70 31.34-70 70s31.34 70 70 70 70-31.34 70-70-31.34-70-70-70zm0 20c27.614 0 50 22.386 50 50s-22.386 50-50 50-50-22.386-50-50 22.386-50 50-50zm0 30a20 20 0 100 40 20 20 0 000-40z'/%3E%3C/svg%3E" alt="Album Art">
<div class="sleeve-grain" aria-hidden="true"></div> <div class="sleeve-grain" aria-hidden="true"></div>
<div class="sleeve-corner" aria-hidden="true"></div> <div class="sleeve-corner" aria-hidden="true"></div>
</div> </div>
+15 -3
View File
@@ -613,6 +613,18 @@ export function setupProgressDrag(bar, fill) {
}); });
} }
// Replace the album-art src and replay the .is-swapping CSS animation
// so the new artwork crossfades in instead of popping. Re-toggling the
// class across rAF restarts the keyframes even if it was already on.
function swapArtworkSrc(imgEl, newSrc) {
if (!imgEl) return;
if (imgEl.src === newSrc) return;
imgEl.classList.remove('is-swapping');
void imgEl.offsetWidth;
imgEl.src = newSrc;
imgEl.classList.add('is-swapping');
}
export function updateUI(status) { export function updateUI(status) {
setLastStatus(status); setLastStatus(status);
@@ -639,7 +651,7 @@ export function updateUI(status) {
if (artworkKey !== lastArtworkKey) { if (artworkKey !== lastArtworkKey) {
lastArtworkKey = artworkKey; lastArtworkKey = artworkKey;
const placeholderArt = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Crect fill='%23282828' width='300' height='300'/%3E%3Cpath fill='%236a6a6a' d='M150 80c-38.66 0-70 31.34-70 70s31.34 70 70 70 70-31.34 70-70-31.34-70-70-70zm0 20c27.614 0 50 22.386 50 50s-22.386 50-50 50-50-22.386-50-50 22.386-50 50-50zm0 30a20 20 0 100 40 20 20 0 000-40z'/%3E%3C/svg%3E"; const placeholderArt = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Cpath fill='%236a6a6a' opacity='0.35' d='M150 80c-38.66 0-70 31.34-70 70s31.34 70 70 70 70-31.34 70-70-31.34-70-70-70zm0 20c27.614 0 50 22.386 50 50s-22.386 50-50 50-50-22.386-50-50 22.386-50 50-50zm0 30a20 20 0 100 40 20 20 0 000-40z'/%3E%3C/svg%3E";
const placeholderGlow = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Crect fill='%23282828' width='300' height='300'/%3E%3C/svg%3E"; const placeholderGlow = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Crect fill='%23282828' width='300' height='300'/%3E%3C/svg%3E";
if (artworkSource) { if (artworkSource) {
fetch(`/api/media/artwork?_=${Date.now()}`, { fetch(`/api/media/artwork?_=${Date.now()}`, {
@@ -651,7 +663,7 @@ export function updateUI(status) {
const oldBlobUrl = currentArtworkBlobUrl; const oldBlobUrl = currentArtworkBlobUrl;
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
currentArtworkBlobUrl = url; currentArtworkBlobUrl = url;
dom.albumArt.src = url; swapArtworkSrc(dom.albumArt, url);
dom.miniAlbumArt.src = url; dom.miniAlbumArt.src = url;
if (dom.albumArtGlow) dom.albumArtGlow.src = url; if (dom.albumArtGlow) dom.albumArtGlow.src = url;
if (oldBlobUrl) setTimeout(() => URL.revokeObjectURL(oldBlobUrl), 1000); if (oldBlobUrl) setTimeout(() => URL.revokeObjectURL(oldBlobUrl), 1000);
@@ -662,7 +674,7 @@ export function updateUI(status) {
URL.revokeObjectURL(currentArtworkBlobUrl); URL.revokeObjectURL(currentArtworkBlobUrl);
currentArtworkBlobUrl = null; currentArtworkBlobUrl = null;
} }
dom.albumArt.src = placeholderArt; swapArtworkSrc(dom.albumArt, placeholderArt);
dom.miniAlbumArt.src = placeholderArt; dom.miniAlbumArt.src = placeholderArt;
if (dom.albumArtGlow) dom.albumArtGlow.src = placeholderGlow; if (dom.albumArtGlow) dom.albumArtGlow.src = placeholderGlow;
} }