From 4c93bfb8c1c4eb32b7d3a29e4cb9c8449929ed69 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sat, 25 Apr 2026 15:22:08 +0300 Subject: [PATCH] ui(player): soften vinyl-stage halo, transparent-bg album placeholder, crossfade artwork swaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- media_server/static/css/styles.css | 70 +++++++++++++++++++----------- media_server/static/index.html | 2 +- media_server/static/js/player.js | 18 ++++++-- 3 files changed, 61 insertions(+), 29 deletions(-) diff --git a/media_server/static/css/styles.css b/media_server/static/css/styles.css index f010c1b..ccf91aa 100644 --- a/media_server/static/css/styles.css +++ b/media_server/static/css/styles.css @@ -4432,6 +4432,12 @@ header .brand-sub { align-items: center; justify-content: center; 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; } @@ -4612,14 +4618,27 @@ body.visualizer-active .vinyl-stage .spectrogram-canvas { 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 { 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 { 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. @@ -4632,17 +4651,15 @@ body.visualizer-active .vinyl-stage .spectrogram-canvas { width: 63%; aspect-ratio: 1; z-index: 3; - background: var(--bg-card-2); + background: transparent; box-shadow: - inset 4px 4px 24px rgba(0, 0, 0, 0.35), -2px 8px 24px rgba(0, 0, 0, 0.5), -4px 18px 44px rgba(0, 0, 0, 0.35); overflow: hidden; } :root[data-theme="light"] .vinyl-stage .sleeve { - background: var(--bg-card); + background: transparent; box-shadow: - inset 4px 4px 18px rgba(0, 0, 0, 0.10), -2px 8px 22px rgba(0, 0, 0, 0.20), -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. */ .vinyl-stage .sleeve #album-art { position: absolute; - top: 5%; - left: 5%; - width: 90%; - height: 90%; + top: 0; + left: 0; + width: 100%; + height: 100%; object-fit: cover; border-radius: 0; z-index: 1; - box-shadow: - inset 0 0 18px rgba(0, 0, 0, 0.35), - 0 2px 6px rgba(0, 0, 0, 0.35); + box-shadow: none; margin: 0; - background: var(--bg-card); + background: transparent; filter: contrast(0.96) saturate(0.92); 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 */ .vinyl-stage .sleeve-grain { position: absolute; @@ -4683,17 +4709,11 @@ body.visualizer-active .vinyl-stage .spectrogram-canvas { } :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 { - position: absolute; - 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; + display: none; } /* Disc wrap — sits behind the sleeve, peeks out the right edge */ diff --git a/media_server/static/index.html b/media_server/static/index.html index 05601c4..9a76aff 100644 --- a/media_server/static/index.html +++ b/media_server/static/index.html @@ -187,7 +187,7 @@
- Album Art + Album Art
diff --git a/media_server/static/js/player.js b/media_server/static/js/player.js index 101a6f3..b451057 100644 --- a/media_server/static/js/player.js +++ b/media_server/static/js/player.js @@ -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) { setLastStatus(status); @@ -639,7 +651,7 @@ export function updateUI(status) { if (artworkKey !== lastArtworkKey) { 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"; if (artworkSource) { fetch(`/api/media/artwork?_=${Date.now()}`, { @@ -651,7 +663,7 @@ export function updateUI(status) { const oldBlobUrl = currentArtworkBlobUrl; const url = URL.createObjectURL(blob); currentArtworkBlobUrl = url; - dom.albumArt.src = url; + swapArtworkSrc(dom.albumArt, url); dom.miniAlbumArt.src = url; if (dom.albumArtGlow) dom.albumArtGlow.src = url; if (oldBlobUrl) setTimeout(() => URL.revokeObjectURL(oldBlobUrl), 1000); @@ -662,7 +674,7 @@ export function updateUI(status) { URL.revokeObjectURL(currentArtworkBlobUrl); currentArtworkBlobUrl = null; } - dom.albumArt.src = placeholderArt; + swapArtworkSrc(dom.albumArt, placeholderArt); dom.miniAlbumArt.src = placeholderArt; if (dom.albumArtGlow) dom.albumArtGlow.src = placeholderGlow; }