fix(ui): full-width spectrum + log-mapped bars; deeper sepia + soft art fade
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.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 + '%';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user