From 336d596b66325e778bc1f635ca866815a35f51ae Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sat, 25 Apr 2026 02:07:20 +0300 Subject: [PATCH] fix(ui): full-width spectrum + log-mapped bars; deeper sepia + soft art fade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spectrum - Logarithmic frequency-to-bar mapping (squared time so bins stretch toward the highs). Per-bar high-end gain ramps from 1.0x at the lowest bar to 3.0x at the highest, so the right half of the spectrum no longer reads as dead air. - Floor bumped from 6% to 12% so silent bars stay visible. - Skip bin 0 (DC + sub-rumble) which was overwhelming the lows. - Use peak (not average) within each band — punchier visual. - Container height 56→64, gradient now copper-lo → copper → copper-hi for more visible top tips. min-width: 0 / box-sizing border-box ensures the row truly claims the full grid column. - Backend FFT path is unchanged: WS audio_data → setFrequencyData → renderVisualizerFrame → updateEditorialSpectrum. No client-side analyzer added. Album art (vinyl label) - Deeper sepia (0.35→0.6) and lower saturate (0.85→0.7) so vibrant covers blend into the copper grooves. - Soft radial mask: outer ~22% of the disc fades toward the vinyl black so the album art dissolves into the surface rather than terminating at a hard clip edge. - Hover state pulls the fade inward and eases sepia back so the user can still see the real cover at near-natural color. - Glow tint matches the new sepia depth. --- media_server/static/css/styles.css | 76 +++++++++++++++++++++--------- media_server/static/js/player.js | 31 ++++++++---- 2 files changed, 78 insertions(+), 29 deletions(-) diff --git a/media_server/static/css/styles.css b/media_server/static/css/styles.css index 3322eca..1fa59c0 100644 --- a/media_server/static/css/styles.css +++ b/media_server/static/css/styles.css @@ -6639,29 +6639,58 @@ footer .separator { color: var(--ink-ghost); margin: 0 8px; } display: block; border-radius: 50%; z-index: 2; - /* Vinyl-consistent tint: warm sepia + slight sharpen + subtle contrast. - Saturate pushed down so vibrant covers don't fight the copper palette. */ + /* Heavy vinyl-consistent tint: deeper sepia + lower saturation + so vibrant covers blend with the copper grooves. */ filter: - sepia(0.35) - saturate(0.85) - contrast(1.08) - brightness(0.95) - hue-rotate(-6deg); - transition: filter 320ms var(--ease); + sepia(0.6) + saturate(0.7) + contrast(1.12) + brightness(0.88) + hue-rotate(-8deg); + transition: filter 480ms var(--ease), -webkit-mask-image 480ms var(--ease); + /* Soft radial fade — the outer ~12% of the art fades to black so + the album image dissolves into the vinyl surface rather than + cutting hard at the circular clip edge. */ + -webkit-mask-image: radial-gradient(circle at 50% 50%, + black 0%, + black 78%, + rgba(0,0,0,0.85) 88%, + rgba(0,0,0,0.4) 96%, + transparent 100%); + mask-image: radial-gradient(circle at 50% 50%, + black 0%, + black 78%, + rgba(0,0,0,0.85) 88%, + rgba(0,0,0,0.4) 96%, + transparent 100%); } .now-playing:hover .vinyl-label #album-art { - /* On hover, ease back toward natural color so users can see the real art */ + /* On hover, ease back toward natural color and pull the fade + inward so more of the real cover is visible. */ filter: - sepia(0.18) - saturate(0.95) + sepia(0.25) + saturate(0.92) contrast(1.05) - brightness(1) - hue-rotate(-3deg); + brightness(0.98) + hue-rotate(-4deg); + -webkit-mask-image: radial-gradient(circle at 50% 50%, + black 0%, + black 88%, + rgba(0,0,0,0.9) 95%, + rgba(0,0,0,0.5) 99%, + transparent 100%); + mask-image: radial-gradient(circle at 50% 50%, + black 0%, + black 88%, + rgba(0,0,0,0.9) 95%, + rgba(0,0,0,0.5) 99%, + transparent 100%); } -/* Match the glow tint to the album art treatment */ +/* Match the glow tint and soft edge to the album art treatment */ .now-playing .vinyl-label #album-art-glow { - filter: blur(22px) saturate(1.2) sepia(0.35) hue-rotate(-6deg); + filter: blur(22px) saturate(1.1) sepia(0.5) hue-rotate(-8deg); + opacity: 0.4; } /* Tonearm */ @@ -6836,29 +6865,34 @@ body.visualizer-active .now-playing .spectrogram-canvas { fill: currentColor; } -/* ─── Spectrum bars (JS-injected; real audio when available) ─── */ +/* ─── Spectrum bars (JS-injected; real audio from backend FFT) ── */ .now-playing .spectrum { display: flex; align-items: flex-end; justify-content: stretch; gap: 2px; - height: 56px; + height: 64px; margin: 36px 0 24px; width: 100%; + /* Force the spectrum to claim the full grid column width even + if siblings (meta-grid cells) report intrinsic widths. */ + box-sizing: border-box; + min-width: 0; } .now-playing .spectrum span { display: block; flex: 1 1 0; - min-width: 2px; - background: linear-gradient(to top, var(--copper-lo), var(--copper-hi)); - opacity: 0.9; + min-width: 0; + background: linear-gradient(to top, var(--copper-lo) 0%, var(--copper) 60%, var(--copper-hi) 100%); + opacity: 0.92; transform-origin: bottom; border-radius: 99px 99px 0 0; height: var(--bar-h, 40%); animation: sr-snap-bar 1.1s ease-in-out infinite; animation-delay: var(--bar-delay, 0s); animation-play-state: paused; - transition: height 80ms linear; + transition: height 60ms linear; + will-change: height; } :root[data-playstate="playing"] .now-playing .spectrum span { animation-play-state: running; diff --git a/media_server/static/js/player.js b/media_server/static/js/player.js index 68f00df..f84b1a9 100644 --- a/media_server/static/js/player.js +++ b/media_server/static/js/player.js @@ -433,6 +433,10 @@ function renderVisualizerFrame() { } // ─── Editorial spectrum (.spectrum bars) driven by audio ────── +// The bin distribution from the FFT is heavy on lows (the bass + mids +// dominate); a linear mapping leaves the right half of the spectrum +// looking dead. Use a logarithmic frequency-to-bar mapping plus a +// per-bar high-end gain so all bars carry visible motion. function updateEditorialSpectrum(bins, numBins) { const root = document.querySelector('.now-playing .spectrum'); if (!root) return; @@ -440,15 +444,26 @@ function updateEditorialSpectrum(bins, numBins) { const barCount = bars.length; if (!barCount) return; document.body.classList.add('audio-spectrum-live'); + + // Skip the very lowest bin (DC + sub-rumble) which often dominates. + const lowBin = 1; + const highBin = numBins - 1; for (let i = 0; i < barCount; i++) { - // Map each visual bar to a chunk of frequency bins (averaged). - const startIdx = Math.floor((i / barCount) * numBins); - const endIdx = Math.max(startIdx + 1, Math.floor(((i + 1) / barCount) * numBins)); - let sum = 0; - for (let j = startIdx; j < endIdx && j < numBins; j++) sum += bins[j]; - const avg = sum / (endIdx - startIdx); - // Boost mids/highs and floor to 6% so quiet bars are still visible. - const pct = Math.max(6, Math.min(100, avg * 110)); + // Logarithmic mapping: equal-area slices of the audible spectrum + // map to equal numbers of bars. Each bar covers a wider bin range + // toward the highs so they get amplified naturally. + const t0 = i / barCount; + const t1 = (i + 1) / barCount; + const startIdx = Math.max(lowBin, Math.floor(lowBin + Math.pow(t0, 2.0) * (highBin - lowBin))); + const endIdx = Math.max(startIdx + 1, Math.floor(lowBin + Math.pow(t1, 2.0) * (highBin - lowBin))); + let peak = 0; + for (let j = startIdx; j < endIdx && j < numBins; j++) { + if (bins[j] > peak) peak = bins[j]; + } + // Per-bar high-end gain: 1.0 at the lowest bar, ~3.0 at the highest. + const gain = 1 + (i / barCount) * 2.0; + // Floor at 12% so silent bars are still visually present. + const pct = Math.max(12, Math.min(100, peak * 110 * gain)); bars[i].style.height = pct + '%'; } }