|
|
@@ -269,6 +269,21 @@ function renderFrame(ts = 0) { |
|
|
// Check if real viz data is fresh (< 1.5 s old) and Winamp is playing. |
|
|
// Check if real viz data is fresh (< 1.5 s old) and Winamp is playing. |
|
|
const hasSignal = winampPlaying && (performance.now() - lastVizAt) < 1500; |
|
|
const hasSignal = winampPlaying && (performance.now() - lastVizAt) < 1500; |
|
|
|
|
|
|
|
|
|
|
|
// Classic fixed vertical gradient: green at bottom → yellow → red at top. |
|
|
|
|
|
// Defined in canvas coords so every bar shows the same colour at the same |
|
|
|
|
|
// height — no per-frame hue calculation, no flicker. |
|
|
|
|
|
const grad = ctx2d.createLinearGradient(0, h, 0, 0); |
|
|
|
|
|
if (hasSignal) { |
|
|
|
|
|
grad.addColorStop(0, '#00aa00'); |
|
|
|
|
|
grad.addColorStop(0.6, '#aaaa00'); |
|
|
|
|
|
grad.addColorStop(1, '#cc0000'); |
|
|
|
|
|
} else { |
|
|
|
|
|
// Idle: same palette but much dimmer |
|
|
|
|
|
grad.addColorStop(0, '#003300'); |
|
|
|
|
|
grad.addColorStop(0.6, '#333300'); |
|
|
|
|
|
grad.addColorStop(1, '#330000'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const n = NUM_BARS; |
|
|
const n = NUM_BARS; |
|
|
const gap = 1; |
|
|
const gap = 1; |
|
|
const barW = Math.max(1, (w - gap * (n - 1)) / n); |
|
|
const barW = Math.max(1, (w - gap * (n - 1)) / n); |
|
|
@@ -277,34 +292,29 @@ function renderFrame(ts = 0) { |
|
|
let val; |
|
|
let val; |
|
|
|
|
|
|
|
|
if (hasSignal) { |
|
|
if (hasSignal) { |
|
|
// Real spectrum data |
|
|
|
|
|
val = lastBars[i] || 0; |
|
|
val = lastBars[i] || 0; |
|
|
if (val > peaks[i]) peaks[i] = val; |
|
|
if (val > peaks[i]) peaks[i] = val; |
|
|
else peaks[i] = Math.max(0, peaks[i] - 0.012); |
|
|
|
|
|
|
|
|
else peaks[i] = Math.max(0, peaks[i] - 0.008); // gentle decay |
|
|
} else { |
|
|
} else { |
|
|
// Idle animation: slow sine "breathing" across the bars. |
|
|
|
|
|
// Amplitude fades out when paused/stopped. |
|
|
|
|
|
|
|
|
// Idle: slow sine breathing |
|
|
const phase = (ts / 1800) + (i / n) * Math.PI * 2; |
|
|
const phase = (ts / 1800) + (i / n) * Math.PI * 2; |
|
|
const breath = (Math.sin(ts / 2000) * 0.5 + 0.5) * 0.08; // 0..0.08 |
|
|
|
|
|
|
|
|
const breath = (Math.sin(ts / 2000) * 0.5 + 0.5) * 0.08; |
|
|
val = Math.max(0, Math.sin(phase) * breath); |
|
|
val = Math.max(0, Math.sin(phase) * breath); |
|
|
peaks[i] = Math.max(0, peaks[i] - 0.02); // let peaks fall quickly |
|
|
|
|
|
|
|
|
peaks[i] = Math.max(0, peaks[i] - 0.02); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const x = Math.round(i * (barW + gap)); |
|
|
const x = Math.round(i * (barW + gap)); |
|
|
const barH = val * h; |
|
|
const barH = val * h; |
|
|
|
|
|
|
|
|
if (barH > 0.5) { |
|
|
if (barH > 0.5) { |
|
|
// Bar colour: green (120°) → yellow (60°) → red (0°) |
|
|
|
|
|
const hue = Math.round((1 - val) * 120); |
|
|
|
|
|
const lit = hasSignal ? 42 : 25; // dimmer in idle |
|
|
|
|
|
ctx2d.fillStyle = `hsl(${hue},100%,${lit}%)`; |
|
|
|
|
|
|
|
|
ctx2d.fillStyle = grad; |
|
|
ctx2d.fillRect(x, h - barH, barW, barH); |
|
|
ctx2d.fillRect(x, h - barH, barW, barH); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Peak indicator |
|
|
|
|
|
|
|
|
// Peak dot — 1 px taller slice of the same gradient, slightly brighter |
|
|
if (peaks[i] > 0.02) { |
|
|
if (peaks[i] > 0.02) { |
|
|
const py = Math.round(h - peaks[i] * h) - 1; |
|
|
const py = Math.round(h - peaks[i] * h) - 1; |
|
|
ctx2d.fillStyle = hasSignal ? 'rgba(255,255,255,0.75)' : 'rgba(255,255,255,0.2)'; |
|
|
|
|
|
|
|
|
ctx2d.fillStyle = hasSignal ? 'rgba(255,255,255,0.6)' : 'rgba(255,255,255,0.15)'; |
|
|
ctx2d.fillRect(x, py, barW, 2); |
|
|
ctx2d.fillRect(x, py, barW, 2); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|