Bri-Tunes/public/js/player-viz.js

125 lines
4.1 KiB
JavaScript

// Audio-reactive visualizer for the player bar.
// Uses Canvas2D (simpler + lighter than Three.js for a tiny radial visualizer).
// Feeds bass energy to the synthwave grid background via window.briTunesBg.
const canvas = document.getElementById('player-viz');
const audio = document.getElementById('player-audio');
if (canvas && audio) {
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const ctx = canvas.getContext('2d');
let audioCtx = null;
let analyser = null;
let freqData = null;
let hooked = false;
let rafId = null;
let visible = true;
function resize() {
const rect = canvas.getBoundingClientRect();
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.max(1, Math.round(rect.width * dpr));
canvas.height = Math.max(1, Math.round(rect.height * dpr));
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
resize();
window.addEventListener('resize', resize);
function hookAudio() {
if (hooked) return;
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const src = audioCtx.createMediaElementSource(audio);
analyser = audioCtx.createAnalyser();
analyser.fftSize = 128; // 64 freq bins
analyser.smoothingTimeConstant = 0.82;
freqData = new Uint8Array(analyser.frequencyBinCount);
src.connect(analyser);
analyser.connect(audioCtx.destination);
hooked = true;
window.briTunesAnalyser = analyser;
} catch (err) {
console.warn('[player-viz] could not hook audio:', err.message);
}
}
window.addEventListener('briTunes:play', () => {
hookAudio();
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
if (!rafId && !reduced) rafId = requestAnimationFrame(frame);
});
audio.addEventListener('play', () => {
// Covers the case where native <audio> controls are used.
hookAudio();
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
if (!rafId && !reduced) rafId = requestAnimationFrame(frame);
});
document.addEventListener('visibilitychange', () => {
visible = !document.hidden;
});
function frame() {
rafId = requestAnimationFrame(frame);
if (!visible || !analyser) return;
analyser.getByteFrequencyData(freqData);
const w = canvas.clientWidth;
const h = canvas.clientHeight;
const cx = w / 2;
const cy = h / 2;
const innerR = Math.min(w, h) * 0.10; // small hub — no cover to ring
const maxBarLen = Math.min(w, h) * 0.40;
ctx.clearRect(0, 0, w, h);
const bars = 36;
const binStep = Math.floor(freqData.length / bars);
let bassSum = 0;
const bassBins = 6;
for (let i = 0; i < bassBins; i++) bassSum += freqData[i];
const bassEnergy = (bassSum / (bassBins * 255));
// Pass bass energy to the background grid animator.
if (window.briTunesBg && typeof window.briTunesBg.setBeat === 'function') {
window.briTunesBg.setBeat(bassEnergy);
}
// Draw each bar as a short line sprouting outward from innerR.
for (let i = 0; i < bars; i++) {
const v = freqData[i * binStep] / 255;
if (v < 0.02) continue;
const angle = (i / bars) * Math.PI * 2 - Math.PI / 2;
const len = v * maxBarLen;
const x1 = cx + Math.cos(angle) * innerR;
const y1 = cy + Math.sin(angle) * innerR;
const x2 = cx + Math.cos(angle) * (innerR + len);
const y2 = cy + Math.sin(angle) * (innerR + len);
// Color by frequency band: bass → magenta, mid → violet, high → cyan
let color;
if (i < bars * 0.25) color = 'rgba(255, 43, 214, 1)'; // magenta
else if (i < bars * 0.6) color = 'rgba(143, 91, 255, 1)'; // violet
else color = 'rgba(0, 229, 255, 1)'; // cyan
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.shadowColor = color;
ctx.shadowBlur = 8;
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
ctx.shadowBlur = 0;
}
if (!reduced) {
// Idle ambient frame (does nothing until audio hooks up).
rafId = requestAnimationFrame(frame);
}
}