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-progress-bar {
height: 2px;
.mini-player .mini-progress-bar {
flex: 0 0 auto;
height: 3px;
background: var(--rule-strong);
border-radius: 0;
cursor: pointer;
position: relative;
min-width: 0;
}
.mini-progress-fill {
.mini-player .mini-progress-fill {
height: 100%;
background: var(--copper);
box-shadow: 0 0 8px var(--copper-glow);
border-radius: 0;
@@ -7062,33 +7067,10 @@ body.audio-spectrum-live .now-playing .spectrum span {
content: "";
position: absolute;
inset: 0;
/* 11 grid lines (every 9°) drawn ONLY in the needle's -45°..+45°
swing range. No mask — the conic-gradient is explicitly transparent
outside the 90° active wedge so the leftmost line aligns with the
needle's rest position (proper zero). */
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);
background: repeating-conic-gradient(from 195deg at 50% 100%,
transparent 0deg 4deg,
rgba(242, 235, 220, 0.08) 4deg 5deg,
transparent 5deg 9deg);
}
.now-playing .vu-meter::after {
content: "VU";
@@ -7109,8 +7091,7 @@ body.audio-spectrum-live .now-playing .spectrum span {
height: 88%;
background: linear-gradient(to top, var(--copper) 0%, var(--copper-hi) 70%, var(--ink) 100%);
transform-origin: bottom center;
/* Rest at full-left like a real VU meter at silence (-∞ dB) */
transform: rotate(-45deg);
transform: rotate(-22deg);
transition: transform 350ms var(--ease);
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.miniVolumeSlider.value = 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');
if (needle) {
const deg = -45 + (status.volume / 100) * 90;
const deg = -22 + (status.volume / 100) * 44;
needle.style.transform = `rotate(${deg}deg)`;
}
// 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.
let vuWobbleHandle = null;
let vuWobbleStart = 0;
let vuLevelSmoothed = 0; // Smoothed RMS of recent frequency frames
const VU_LEVEL_ATTACK = 0.55; // How fast needle climbs to a peak
const VU_LEVEL_RELEASE = 0.12; // How fast it falls back
let vuLevelSmoothed = 0;
const VU_LEVEL_ATTACK = 0.7; // Fast climb so the needle catches musical hits
const VU_LEVEL_RELEASE = 0.25; // Faster fall so it swings between hits, not pins
function readAudioLevel() {
// frequencyData is the WS-driven FFT payload from player.js scope.
if (!frequencyData || !frequencyData.frequencies) return null;
const bins = frequencyData.frequencies;
if (!bins.length) return null;
let sumSq = 0;
// Skip the very lowest bin (DC + sub-rumble) for cleaner level.
for (let i = 1; i < bins.length; i++) sumSq += bins[i] * bins[i];
const rms = Math.sqrt(sumSq / (bins.length - 1));
// The values are in 0..1 from the backend; gain a touch so quieter
// tracks still swing the needle.
return Math.min(1, rms * 1.6);
let peak = 0;
for (let i = 1; i < bins.length; i++) {
if (bins[i] > peak) peak = bins[i];
}
return Math.min(1, peak * 1.4);
}
function startVuWobble() {
@@ -839,11 +836,9 @@ function startVuWobble() {
const wanted = audioLevel * (vol / 100);
const k = wanted > vuLevelSmoothed ? VU_LEVEL_ATTACK : VU_LEVEL_RELEASE;
vuLevelSmoothed = vuLevelSmoothed * (1 - k) + wanted * k;
// Map 0..1 to -45deg..+45deg.
target = -45 + vuLevelSmoothed * 90;
target = -22 + vuLevelSmoothed * 44;
} else {
// Synthetic fallback: volume-mapped + sine wobble.
const base = -45 + (vol / 100) * 90;
const base = -22 + (vol / 100) * 44;
const mag = Math.max(2, Math.min(14, vol * 0.16));
const t = (performance.now() - vuWobbleStart) / 1000;
target = base
@@ -864,9 +859,8 @@ function stopVuWobble() {
vuWobbleHandle = null;
}
vuLevelSmoothed = 0;
// Settle needle back to the bottom of the swing.
const needle = document.getElementById('vuNeedle');
if (needle) needle.style.transform = 'rotate(-45deg)';
if (needle) needle.style.transform = 'rotate(-22deg)';
}
export function updatePlaybackState(state) {