Files
djaudiovisualizer/src/visualizer.js
T
lucianoandClaude Fable 5 08414070a9 Interattivo Avatar: a robot whose limbs mirror your tracked body 1:1
Raise your arm and the robot raises its arm. ModelSim gains primitive
meshes (cube/sphere) and renderAvatar(): parts placed by limb matrices
(basis aligned to each bone segment) with the same lighting as the GLB
renderer. interactive.js keeps the mirrored raw landmarks (with depth,
smoothed) and builds the robot every frame: torso sized from the
shoulders, head with accent visor and antenna, articulated arms/legs
with joint spheres, glowing hands, feet. Falls back to the motion mist
when no body is tracked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 18:39:24 +02:00

180 lines
7.6 KiB
JavaScript

// WebGL2 engine: renders a fullscreen quad through the parametric uber-shader,
// driven by the current effect's parameters + live audio.
class Visualizer {
constructor(canvas) {
this.canvas = canvas;
// preserveDrawingBuffer keeps the rendered frame readable so canvas
// captureStream() (used by the MP4 recorder) captures real frames.
const gl = canvas.getContext('webgl2', { antialias: false, alpha: false, preserveDrawingBuffer: true, powerPreference: 'high-performance' });
if (!gl) throw new Error('WebGL2 non disponibile su questo sistema.');
this.gl = gl;
this.program = this._buildProgram(window.SHADERS.vert, window.SHADERS.frag);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
this.vbo = buf;
gl.useProgram(this.program);
const U = (n) => gl.getUniformLocation(this.program, n);
this.aPos = gl.getAttribLocation(this.program, 'aPos');
this.u = {
uRes: U('uRes'), uTime: U('uTime'),
uBass: U('uBass'), uMid: U('uMid'), uTreble: U('uTreble'), uLevel: U('uLevel'), uBeat: U('uBeat'),
uFamily: U('uFamily'), uScale: U('uScale'), uRot: U('uRot'), uRotSpeed: U('uRotSpeed'), uSym: U('uSym'),
uHueBase: U('uHueBase'), uHueCycle: U('uHueCycle'), uSat: U('uSat'), uContrast: U('uContrast'),
uInvert: U('uInvert'), uWarp: U('uWarp'), uAudioMix: U('uAudioMix'), uSpeed: U('uSpeed'),
uColorA: U('uColorA'), uColorB: U('uColorB'), uTex: U('uTex'), uSpectrum: U('uSpectrum'), uBgDark: U('uBgDark'), uWave: U('uWave'), uWaveHist: U('uWaveHist')
};
// Custom-source texture (for the SVG/Image effect family). Starts empty.
this.tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, this.tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 0]));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
this.effect = (window.EFFECTS && window.EFFECTS.defaults()) || this._fallbackEffect();
this.resize();
window.addEventListener('resize', () => this.resize());
}
_fallbackEffect() {
return { family: 2, scale: 1.0, rot: 0, rotSpeed: 0, sym: 0, hueBase: 0, hueCycle: 0.02,
sat: 1, contrast: 0.8, invert: 0, warp: 0, audioMix: 1, speed: 1, colorA: [0.1, 0.2, 0.8], colorB: [1, 0.4, 0.9] };
}
_compile(type, src) {
const gl = this.gl;
const sh = gl.createShader(type);
gl.shaderSource(sh, src);
gl.compileShader(sh);
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS))
throw new Error('Errore compilazione shader:\n' + gl.getShaderInfoLog(sh));
return sh;
}
_buildProgram(vsrc, fsrc) {
const gl = this.gl;
const p = gl.createProgram();
gl.attachShader(p, this._compile(gl.VERTEX_SHADER, vsrc));
gl.attachShader(p, this._compile(gl.FRAGMENT_SHADER, fsrc));
gl.linkProgram(p);
if (!gl.getProgramParameter(p, gl.LINK_STATUS))
throw new Error('Errore link programma: ' + gl.getProgramInfoLog(p));
return p;
}
resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
// The canvas fills the #stage box (the LED active area), which is
// normally the whole window but can be a smaller mapping rectangle.
const cw = this.canvas.clientWidth || window.innerWidth;
const ch = this.canvas.clientHeight || window.innerHeight;
const w = Math.floor(cw * dpr), h = Math.floor(ch * dpr);
if (this.canvas.width !== w || this.canvas.height !== h) { this.canvas.width = w; this.canvas.height = h; }
this.gl.viewport(0, 0, w, h);
}
setEffect(effect) {
// Merge onto current so partial updates (e.g. only family) are safe.
this.effect = Object.assign({}, this.effect, effect);
}
// Load a GLB model for the 'Modello 3D' family. Parsed lazily on the next
// render so the engine is only created when the family is active.
setModelData(buf) {
if (this.model3d) return this.model3d.setModel(buf);
this._pendingGlb = buf;
return true;
}
// Upload a custom source (HTMLImageElement / HTMLCanvasElement) for the
// SVG/Image effect family. Flipped on the Y axis to match screen space.
setTexture(source) {
const gl = this.gl;
gl.bindTexture(gl.TEXTURE_2D, this.tex);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
// Also kept for the interactive "mirror" mode, whose particles reform
// the custom image instead of the default wordmark.
this.customSource = source;
}
render(timeSec, audio) {
const gl = this.gl, u = this.u, e = this.effect;
// Release the camera as soon as a non-interactive preset takes over.
if (this.inter && !e.isInteractive) this.inter.suspend();
if (e.isModel3d && window.ModelSim) {
if (!this.model3d) this.model3d = new window.ModelSim(gl);
if (this._pendingGlb) { this.model3d.setModel(this._pendingGlb); this._pendingGlb = null; }
this.model3d.render(timeSec, audio, e, this.canvas);
return;
}
if (e.isInteractive && window.InteractiveSim) {
if (!this.inter) this.inter = new window.InteractiveSim(gl);
this.inter.customSource = this.customSource || null;
// The model-puppet and avatar modes drive the shared 3D renderer.
if ((e.interactiveMode === 'model' || e.interactiveMode === 'avatar') && window.ModelSim) {
if (!this.model3d) this.model3d = new window.ModelSim(gl);
if (this._pendingGlb) { this.model3d.setModel(this._pendingGlb); this._pendingGlb = null; }
this.inter.modelSim = this.model3d;
}
this.inter.render(timeSec, audio, e, this.canvas);
return;
}
if (e.isFluid && window.FluidSim) {
if (!this.fluid) this.fluid = new window.FluidSim(gl);
this.fluid.render(timeSec, audio, e, this.canvas);
return;
}
gl.useProgram(this.program);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
gl.enableVertexAttribArray(this.aPos);
gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, false, 0, 0);
gl.uniform2f(u.uRes, this.canvas.width, this.canvas.height);
gl.uniform1f(u.uTime, timeSec);
gl.uniform1f(u.uBass, audio.bass);
gl.uniform1f(u.uMid, audio.mid);
gl.uniform1f(u.uTreble, audio.treble);
gl.uniform1f(u.uLevel, audio.level);
gl.uniform1f(u.uBeat, audio.beat);
gl.uniform1i(u.uFamily, e.family | 0);
gl.uniform1f(u.uScale, e.scale);
gl.uniform1f(u.uRot, e.rot);
gl.uniform1f(u.uRotSpeed, e.rotSpeed);
gl.uniform1f(u.uSym, e.sym);
gl.uniform1f(u.uHueBase, e.hueBase);
gl.uniform1f(u.uHueCycle, e.hueCycle);
gl.uniform1f(u.uSat, e.sat);
gl.uniform1f(u.uContrast, e.contrast);
gl.uniform1f(u.uInvert, e.invert);
gl.uniform1f(u.uWarp, e.warp);
gl.uniform1f(u.uAudioMix, e.audioMix);
gl.uniform1f(u.uSpeed, e.speed);
gl.uniform3fv(u.uColorA, e.colorA);
gl.uniform3fv(u.uColorB, e.colorB);
gl.uniform1f(u.uBgDark, e.bgDark || 0);
if (u.uSpectrum && audio.spectrum) gl.uniform1fv(u.uSpectrum, audio.spectrum);
if (u.uWave && audio.wave) gl.uniform1fv(u.uWave, audio.wave);
if (u.uWaveHist && audio.waveHist) gl.uniform1fv(u.uWaveHist, audio.waveHist);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.tex);
gl.uniform1i(u.uTex, 0);
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
}
window.Visualizer = Visualizer;