feat(ui): live VU + audio-driven spectrum, editorial banner, subtler dynamic bg

VU needle (animated)
- Synthetic wobble bounded by current volume runs only while
  state==='playing'. Two combined sines + jitter make it look
  like a real analog needle reacting to peaks.
- Settles back to the static volume-mapped position when paused.

Spectrum (real audio)
- Now driven by the same frequencyData the visualizer canvas
  uses. Each visual bar averages a chunk of frequency bins.
- Spans are now JS-injected (60 bars) instead of hardcoded so
  the bar count is no longer baked in.
- Spectrum spans full width of the masthead column, height
  bumped to 56px for presence.
- CSS animation pauses (sets via `body.audio-spectrum-live`)
  when JS is driving heights so the keyframes don't fight.
- Synthetic CSS animation remains as the fallback when audio
  capture isn't available.

Visualizer auto-enable
- On first install with loopback support, visualizer is
  enabled so the spectrum is alive out of the box.

Dynamic background
- Lower max opacity (1 → 0.45 dark, 0.35 light)
- sepia + saturate filter + hue-rotate keep it palette-aligned
  with the copper editorial tones instead of fighting them
- mix-blend-mode screen (dark) / multiply (light) blends into
  the page background instead of overlaying

Update + connection banners
- Fully restyled: glassy card with copper hairline accent,
  mono uppercase text, copper hairline-border CTA buttons,
  minimal close button. Matches the rest of the editorial
  palette instead of the old solid-green-bar look.
This commit is contained in:
2026-04-25 02:03:15 +03:00
parent d157388a94
commit d937c1590c
4 changed files with 230 additions and 72 deletions
+26 -2
View File
@@ -166,9 +166,33 @@ window.addEventListener('DOMContentLoaded', async () => {
// Vinyl is now structural / always-on via CSS — no init call needed.
// applyVinylMode();
// Initialize audio visualizer
// Build the editorial spectrum bars (60 spans). JS-managed so we can
// drive heights from real audio data when available.
const spectrumRoot = document.getElementById('player-spectrum');
if (spectrumRoot && !spectrumRoot.children.length) {
const SPECTRUM_BARS = 60;
const frag = document.createDocumentFragment();
for (let i = 0; i < SPECTRUM_BARS; i++) {
const s = document.createElement('span');
// Pseudo-random heights for the synthetic CSS animation phase
s.style.setProperty('--bar-h', (25 + Math.abs(Math.sin(i * 0.7)) * 70).toFixed(0) + '%');
s.style.setProperty('--bar-delay', (-Math.random() * 1.1).toFixed(2) + 's');
frag.appendChild(s);
}
spectrumRoot.appendChild(frag);
}
// Initialize audio visualizer — auto-enable when supported so the
// spectrum shows real audio out of the box.
checkVisualizerAvailability().then(() => {
if (visualizerEnabled && visualizerAvailable) {
if (visualizerAvailable && !visualizerEnabled) {
// Auto-enable on first install if loopback capture works.
if (localStorage.getItem('visualizerEnabled') === null) {
localStorage.setItem('visualizerEnabled', 'true');
}
}
if ((visualizerEnabled || localStorage.getItem('visualizerEnabled') === 'true')
&& visualizerAvailable) {
applyVisualizerMode();
}
});