robot-dance-party / js /visualizer.js
autonomous-ai's picture
Upload folder using huggingface_hub
61bd138 verified
Raw
History Blame Contribute Delete
4.21 kB
// visualizer.js — Canvas frequency visualizer
//
// Duo uses Three.js for 3D robot visualization + MusicNoteSystem particles.
// We use a 2D canvas with frequency bars + waveform + beat flash,
// since Autonomous OS robots don't have a standard 3D model to render.
export class Visualizer {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this._resize();
// Smooth values for animation
this._smoothBars = null;
this._beatFlash = 0;
// Resize on window change
this._resizeHandler = () => this._resize();
window.addEventListener('resize', this._resizeHandler);
}
destroy() {
window.removeEventListener('resize', this._resizeHandler);
}
_resize() {
const rect = this.canvas.parentElement.getBoundingClientRect();
this.canvas.width = rect.width * window.devicePixelRatio;
this.canvas.height = rect.height * window.devicePixelRatio;
this.ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
this.w = rect.width;
this.h = rect.height;
}
// Called every frame from dance engine tick
draw(freqData, timeData, analysis) {
const { w, h, ctx } = this;
const { bass, mid, high, energy, isBeat } = analysis;
// Beat flash decay
if (isBeat) this._beatFlash = 1.0;
else this._beatFlash *= 0.9;
// --- Background fade (creates trail effect) ---
ctx.fillStyle = `rgba(10, 10, 15, 0.3)`;
ctx.fillRect(0, 0, w, h);
// Beat flash overlay
if (this._beatFlash > 0.05) {
ctx.fillStyle = `rgba(108, 92, 231, ${this._beatFlash * 0.12})`;
ctx.fillRect(0, 0, w, h);
}
// --- Frequency bars ---
if (freqData) {
this._drawBars(freqData);
}
// --- Waveform ---
if (timeData) {
this._drawWaveform(timeData);
}
// --- Center energy circle ---
this._drawEnergyCircle(energy, isBeat);
}
_drawBars(freqData) {
const { w, h, ctx } = this;
const barCount = Math.min(freqData.length, 96);
// Initialize smooth bars
if (!this._smoothBars || this._smoothBars.length !== barCount) {
this._smoothBars = new Float32Array(barCount);
}
const barW = (w / barCount) * 0.8;
const gap = (w / barCount) * 0.2;
for (let i = 0; i < barCount; i++) {
const raw = freqData[i] / 255;
// Smooth falloff (bars fall slower than they rise)
if (raw > this._smoothBars[i]) {
this._smoothBars[i] = raw;
} else {
this._smoothBars[i] *= 0.92; // Decay
}
const val = this._smoothBars[i];
const barH = val * h * 0.75;
const x = i * (barW + gap);
// Color gradient based on frequency position
const ratio = i / barCount;
const r = ratio < 0.1 ? 160 : ratio < 0.4 ? 60 : Math.floor(180 * ratio);
const g = ratio < 0.1 ? 50 : ratio < 0.4 ? Math.floor(160 + 95 * ratio) : 230;
const b = ratio < 0.1 ? 255 : ratio < 0.4 ? 255 : Math.floor(80 * (1 - ratio));
const alpha = 0.3 + val * 0.7;
ctx.fillStyle = `rgba(${r},${g},${b},${alpha})`;
// Mirror bars (top and bottom)
const halfH = barH / 2;
const centerY = h / 2;
ctx.fillRect(x, centerY - halfH, barW, halfH);
ctx.fillRect(x, centerY, barW, halfH * 0.6);
}
}
_drawWaveform(timeData) {
const { w, h, ctx } = this;
ctx.beginPath();
ctx.strokeStyle = `rgba(108, 92, 231, ${0.3 + this._beatFlash * 0.4})`;
ctx.lineWidth = 1.5;
const sliceW = w / timeData.length;
for (let i = 0; i < timeData.length; i++) {
const v = timeData[i] / 128.0;
const y = (v * h) / 2;
if (i === 0) ctx.moveTo(0, y);
else ctx.lineTo(i * sliceW, y);
}
ctx.stroke();
}
_drawEnergyCircle(energy, isBeat) {
const { w, h, ctx } = this;
const cx = w / 2;
const cy = h / 2;
const norm = Math.min(energy / 200, 1);
const radius = 20 + norm * 40 + (isBeat ? 15 : 0);
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.fillStyle = `rgba(108, 92, 231, ${0.05 + norm * 0.1})`;
ctx.fill();
ctx.strokeStyle = `rgba(108, 92, 231, ${0.2 + norm * 0.3 + this._beatFlash * 0.3})`;
ctx.lineWidth = 2;
ctx.stroke();
}
}