S2S / frontend /js /orb-visualizer.js
Krishnakkp's picture
feat: initial deployment to HF spaces
ecef9b0
class OrbVisualizer {
constructor(canvas, audio) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.audio = audio;
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
this.analyser = this.audioCtx.createAnalyser();
this.analyser.fftSize = 256;
try {
this.source = this.audioCtx.createMediaElementSource(this.audio);
this.source.connect(this.analyser);
this.analyser.connect(this.audioCtx.destination);
} catch (e) {
console.warn("Audio source already connected");
}
this.bufferLength = this.analyser.frequencyBinCount;
this.dataArray = new Uint8Array(this.bufferLength);
this.isPlaying = false;
this.animationId = null;
this.particles = [];
this.resize();
window.addEventListener('resize', () => this.resize());
}
resize() {
const rect = this.canvas.parentElement.getBoundingClientRect();
this.canvas.width = rect.width * 1.5; // Higher res
this.canvas.height = rect.height * 1.5;
this.canvas.style.width = `${rect.width}px`;
this.canvas.style.height = `${rect.height}px`;
}
play() {
if (this.audioCtx.state === 'suspended') {
this.audioCtx.resume();
}
this.isPlaying = true;
this.draw();
}
pause() {
this.isPlaying = false;
cancelAnimationFrame(this.animationId);
this.drawIdle();
}
destroy() {
this.pause();
}
drawIdle() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
const cx = this.canvas.width / 2;
const cy = this.canvas.height / 2;
const radius = Math.min(cx, cy) * 0.4;
// Inner core
this.ctx.beginPath();
this.ctx.arc(cx, cy, radius, 0, 2 * Math.PI);
const gradient = this.ctx.createRadialGradient(cx, cy, 0, cx, cy, radius);
gradient.addColorStop(0, '#7c3aed');
gradient.addColorStop(1, '#2563eb');
this.ctx.fillStyle = gradient;
this.ctx.fill();
// Idle glow
this.ctx.shadowBlur = 40;
this.ctx.shadowColor = 'rgba(124, 58, 237, 0.4)';
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
this.ctx.lineWidth = 2;
this.ctx.stroke();
this.ctx.shadowBlur = 0;
}
draw() {
if (!this.isPlaying) return;
this.animationId = requestAnimationFrame(() => this.draw());
this.analyser.getByteFrequencyData(this.dataArray);
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
const cx = this.canvas.width / 2;
const cy = this.canvas.height / 2;
// Split frequencies into bass, mid, treble
let bass = 0, mid = 0, treble = 0;
for(let i=0; i<40; i++) bass += this.dataArray[i];
for(let i=40; i<80; i++) mid += this.dataArray[i];
for(let i=80; i<120; i++) treble += this.dataArray[i];
bass /= 40; mid /= 40; treble /= 40;
const baseRadius = Math.min(cx, cy) * 0.35;
// Draw Outer Ring (Treble)
this.ctx.beginPath();
this.ctx.arc(cx, cy, baseRadius + (treble * 0.5) + 30, 0, 2 * Math.PI);
this.ctx.strokeStyle = `rgba(37, 99, 235, ${treble/255})`;
this.ctx.lineWidth = 4;
this.ctx.stroke();
// Draw Mid Ring
this.ctx.beginPath();
this.ctx.arc(cx, cy, baseRadius + (mid * 0.4) + 15, 0, 2 * Math.PI);
this.ctx.strokeStyle = `rgba(244, 63, 94, ${mid/255})`;
this.ctx.lineWidth = 6;
this.ctx.stroke();
// Draw Core (Bass)
const coreRadius = baseRadius + (bass * 0.3);
this.ctx.beginPath();
this.ctx.arc(cx, cy, coreRadius, 0, 2 * Math.PI);
const gradient = this.ctx.createRadialGradient(cx, cy, 0, cx, cy, coreRadius);
gradient.addColorStop(0, '#7c3aed');
gradient.addColorStop(0.7, '#2563eb');
gradient.addColorStop(1, 'transparent');
this.ctx.fillStyle = gradient;
this.ctx.shadowBlur = bass;
this.ctx.shadowColor = '#7c3aed';
this.ctx.fill();
this.ctx.shadowBlur = 0; // reset
// Particles emission on heavy bass
if (bass > 180 && Math.random() > 0.5) {
this.particles.push({
x: cx, y: cy,
vx: (Math.random() - 0.5) * 10,
vy: (Math.random() - 0.5) * 10,
life: 1.0,
color: Math.random() > 0.5 ? '#7c3aed' : '#2563eb'
});
}
// Draw and update particles
for (let i = this.particles.length - 1; i >= 0; i--) {
let p = this.particles[i];
p.x += p.vx;
p.y += p.vy;
p.life -= 0.05;
if (p.life <= 0) {
this.particles.splice(i, 1);
continue;
}
this.ctx.beginPath();
this.ctx.arc(p.x, p.y, 3 * p.life, 0, 2 * Math.PI);
this.ctx.fillStyle = p.color;
this.ctx.globalAlpha = p.life;
this.ctx.fill();
this.ctx.globalAlpha = 1.0;
}
}
}