ui(vu): narrower 44deg swing, peak-based level, faster response; mini progress bar fix

- VU needle swings -22..+22deg instead of -45..+45 for a more realistic VU look
- Switch from RMS to peak frequency reading so the needle catches musical hits
- Faster attack (0.7) and release (0.25) so it swings rather than pinning
- Replace explicit grid lines with subtle repeating-conic-gradient ticks
- Scope mini progress bar styles to .mini-player; taller (3px), clickable
This commit is contained in:
2026-04-25 11:41:32 +03:00
parent 588a303c44
commit f2c82164e8
2 changed files with 26 additions and 51 deletions
+13 -32
View File
@@ -5377,12 +5377,17 @@ body.visualizer-active .vinyl-stage .spectrogram-canvas {
} }
#mini-current-time { color: var(--copper); font-weight: 500; } #mini-current-time { color: var(--copper); font-weight: 500; }
.mini-progress-bar { .mini-player .mini-progress-bar {
height: 2px; flex: 0 0 auto;
height: 3px;
background: var(--rule-strong); background: var(--rule-strong);
border-radius: 0; border-radius: 0;
cursor: pointer;
position: relative;
min-width: 0;
} }
.mini-progress-fill { .mini-player .mini-progress-fill {
height: 100%;
background: var(--copper); background: var(--copper);
box-shadow: 0 0 8px var(--copper-glow); box-shadow: 0 0 8px var(--copper-glow);
border-radius: 0; border-radius: 0;
@@ -7062,33 +7067,10 @@ body.audio-spectrum-live .now-playing .spectrum span {
content: ""; content: "";
position: absolute; position: absolute;
inset: 0; inset: 0;
/* 11 grid lines (every 9°) drawn ONLY in the needle's -45°..+45° background: repeating-conic-gradient(from 195deg at 50% 100%,
swing range. No mask — the conic-gradient is explicitly transparent transparent 0deg 4deg,
outside the 90° active wedge so the leftmost line aligns with the rgba(242, 235, 220, 0.08) 4deg 5deg,
needle's rest position (proper zero). */ transparent 5deg 9deg);
background: conic-gradient(from 315deg at 50% 100%,
rgba(242, 235, 220, 0.18) 0deg 0.5deg,
transparent 0.5deg 9deg,
rgba(242, 235, 220, 0.18) 9deg 9.5deg,
transparent 9.5deg 18deg,
rgba(242, 235, 220, 0.18) 18deg 18.5deg,
transparent 18.5deg 27deg,
rgba(242, 235, 220, 0.18) 27deg 27.5deg,
transparent 27.5deg 36deg,
rgba(242, 235, 220, 0.18) 36deg 36.5deg,
transparent 36.5deg 45deg,
rgba(242, 235, 220, 0.25) 45deg 45.5deg, /* slightly brighter centre line at 0 */
transparent 45.5deg 54deg,
rgba(242, 235, 220, 0.18) 54deg 54.5deg,
transparent 54.5deg 63deg,
rgba(242, 235, 220, 0.18) 63deg 63.5deg,
transparent 63.5deg 72deg,
rgba(242, 235, 220, 0.18) 72deg 72.5deg,
transparent 72.5deg 81deg,
rgba(242, 235, 220, 0.18) 81deg 81.5deg,
transparent 81.5deg 90deg,
rgba(242, 235, 220, 0.18) 90deg 90.5deg,
transparent 90.5deg 360deg);
} }
.now-playing .vu-meter::after { .now-playing .vu-meter::after {
content: "VU"; content: "VU";
@@ -7109,8 +7091,7 @@ body.audio-spectrum-live .now-playing .spectrum span {
height: 88%; height: 88%;
background: linear-gradient(to top, var(--copper) 0%, var(--copper-hi) 70%, var(--ink) 100%); background: linear-gradient(to top, var(--copper) 0%, var(--copper-hi) 70%, var(--ink) 100%);
transform-origin: bottom center; transform-origin: bottom center;
/* Rest at full-left like a real VU meter at silence (-∞ dB) */ transform: rotate(-22deg);
transform: rotate(-45deg);
transition: transform 350ms var(--ease); transition: transform 350ms var(--ease);
box-shadow: 0 0 8px var(--copper-glow); box-shadow: 0 0 8px var(--copper-glow);
} }
+13 -19
View File
@@ -763,10 +763,10 @@ export function updateUI(status) {
dom.volumeDisplay.textContent = `${status.volume}%`; dom.volumeDisplay.textContent = `${status.volume}%`;
dom.miniVolumeSlider.value = status.volume; dom.miniVolumeSlider.value = status.volume;
dom.miniVolumeDisplay.textContent = `${status.volume}%`; dom.miniVolumeDisplay.textContent = `${status.volume}%`;
// VU needle: map 0-100 volume to -45deg..+45deg rotation. // VU needle: map 0-100 volume to -22deg..+22deg rotation.
const needle = document.getElementById('vuNeedle'); const needle = document.getElementById('vuNeedle');
if (needle) { if (needle) {
const deg = -45 + (status.volume / 100) * 90; const deg = -22 + (status.volume / 100) * 44;
needle.style.transform = `rotate(${deg}deg)`; needle.style.transform = `rotate(${deg}deg)`;
} }
// Editorial VU readout: VOL XX% / OUT (SYS or MUTED) // Editorial VU readout: VOL XX% / OUT (SYS or MUTED)
@@ -802,22 +802,19 @@ export function updateUI(status) {
// slider position so the needle still looks alive. // slider position so the needle still looks alive.
let vuWobbleHandle = null; let vuWobbleHandle = null;
let vuWobbleStart = 0; let vuWobbleStart = 0;
let vuLevelSmoothed = 0; // Smoothed RMS of recent frequency frames let vuLevelSmoothed = 0;
const VU_LEVEL_ATTACK = 0.55; // How fast needle climbs to a peak const VU_LEVEL_ATTACK = 0.7; // Fast climb so the needle catches musical hits
const VU_LEVEL_RELEASE = 0.12; // How fast it falls back const VU_LEVEL_RELEASE = 0.25; // Faster fall so it swings between hits, not pins
function readAudioLevel() { function readAudioLevel() {
// frequencyData is the WS-driven FFT payload from player.js scope.
if (!frequencyData || !frequencyData.frequencies) return null; if (!frequencyData || !frequencyData.frequencies) return null;
const bins = frequencyData.frequencies; const bins = frequencyData.frequencies;
if (!bins.length) return null; if (!bins.length) return null;
let sumSq = 0; let peak = 0;
// Skip the very lowest bin (DC + sub-rumble) for cleaner level. for (let i = 1; i < bins.length; i++) {
for (let i = 1; i < bins.length; i++) sumSq += bins[i] * bins[i]; if (bins[i] > peak) peak = bins[i];
const rms = Math.sqrt(sumSq / (bins.length - 1)); }
// The values are in 0..1 from the backend; gain a touch so quieter return Math.min(1, peak * 1.4);
// tracks still swing the needle.
return Math.min(1, rms * 1.6);
} }
function startVuWobble() { function startVuWobble() {
@@ -839,11 +836,9 @@ function startVuWobble() {
const wanted = audioLevel * (vol / 100); const wanted = audioLevel * (vol / 100);
const k = wanted > vuLevelSmoothed ? VU_LEVEL_ATTACK : VU_LEVEL_RELEASE; const k = wanted > vuLevelSmoothed ? VU_LEVEL_ATTACK : VU_LEVEL_RELEASE;
vuLevelSmoothed = vuLevelSmoothed * (1 - k) + wanted * k; vuLevelSmoothed = vuLevelSmoothed * (1 - k) + wanted * k;
// Map 0..1 to -45deg..+45deg. target = -22 + vuLevelSmoothed * 44;
target = -45 + vuLevelSmoothed * 90;
} else { } else {
// Synthetic fallback: volume-mapped + sine wobble. const base = -22 + (vol / 100) * 44;
const base = -45 + (vol / 100) * 90;
const mag = Math.max(2, Math.min(14, vol * 0.16)); const mag = Math.max(2, Math.min(14, vol * 0.16));
const t = (performance.now() - vuWobbleStart) / 1000; const t = (performance.now() - vuWobbleStart) / 1000;
target = base target = base
@@ -864,9 +859,8 @@ function stopVuWobble() {
vuWobbleHandle = null; vuWobbleHandle = null;
} }
vuLevelSmoothed = 0; vuLevelSmoothed = 0;
// Settle needle back to the bottom of the swing.
const needle = document.getElementById('vuNeedle'); const needle = document.getElementById('vuNeedle');
if (needle) needle.style.transform = 'rotate(-45deg)'; if (needle) needle.style.transform = 'rotate(-22deg)';
} }
export function updatePlaybackState(state) { export function updatePlaybackState(state) {