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:
2026-04-25 02:07:20 +03:00
parent d937c1590c
commit 336d596b66
2 changed files with 78 additions and 29 deletions
+23 -8
View File
@@ -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 + '%';
}
}