Effects engine, SVG/VU/silhouette categories, pad bank, MP4 recording, dual-window UI
- Parametric effect engine: 23 shader families × palettes × variants (~2300 presets) with a filterable library and an effect sequence (auto-cycle by time or beat). - Effect categories: fractals/abstract, dancing silhouettes, SVG/Image source (loads any SVG/image as a recolourable audio-reactive effect; 50+ bundled CC0 SVGs), and VU-Meter (32-band spectrum bars, analogue needle, stereo LEDs). - Dual-window architecture: control panel + fullscreen output on an external display (reliable via simple-fullscreen); tabbed control UI (Audio, Effetti, Pad, Immagini, Testo, Registra, Schermo). - Audio: file playback + live input + output device selection, per-band visual EQ, audio playlist (reorder, hotkeys, per-track play/pause, repeat). - Pad bank: 5×4 launchpad with persistent assignments; pressing a pad stops the current. - Overlays: slideshow images (resizable), two freely positioned/resizable logos, scrolling ticker text. - MP4 recording of the composited output (visual + overlays + audio) via MediaRecorder + ffmpeg, with selectable aspect ratios (16:9, 9:16, 1:1, 4:3, 3:4, 21:9). - App icon (.icns) and dock icon; author Dj LuZa. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
b32ea47ae5
commit
47728d0d2a
76 files changed
+21794
-459
No files matched your search
+84
-48
@@ -1,50 +1,61 @@
|
||||
// WebGL2 engine: renders a fullscreen quad through one of the scene shaders.
|
||||
// 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;
|
||||
const gl = canvas.getContext('webgl2', { antialias: false, alpha: false, powerPreference: 'high-performance' });
|
||||
// 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.programs = window.SHADERS.scenes.map(src => this._buildProgram(window.SHADERS.vert, src));
|
||||
this.sceneIndex = 0;
|
||||
this.program = this._buildProgram(window.SHADERS.vert, window.SHADERS.frag);
|
||||
|
||||
// Fullscreen triangle covering the viewport.
|
||||
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;
|
||||
|
||||
// Per-program attribute/uniform locations.
|
||||
this.locs = this.programs.map(p => {
|
||||
gl.useProgram(p);
|
||||
const aPos = gl.getAttribLocation(p, 'aPos');
|
||||
return {
|
||||
aPos,
|
||||
uRes: gl.getUniformLocation(p, 'uRes'),
|
||||
uTime: gl.getUniformLocation(p, 'uTime'),
|
||||
uBass: gl.getUniformLocation(p, 'uBass'),
|
||||
uMid: gl.getUniformLocation(p, 'uMid'),
|
||||
uTreble: gl.getUniformLocation(p, 'uTreble'),
|
||||
uLevel: gl.getUniformLocation(p, 'uLevel'),
|
||||
uBeat: gl.getUniformLocation(p, 'uBeat')
|
||||
};
|
||||
});
|
||||
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')
|
||||
};
|
||||
|
||||
// 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)) {
|
||||
const log = gl.getShaderInfoLog(sh);
|
||||
throw new Error('Errore compilazione shader:\n' + log + '\n\n' + src);
|
||||
}
|
||||
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS))
|
||||
throw new Error('Errore compilazione shader:\n' + gl.getShaderInfoLog(sh));
|
||||
return sh;
|
||||
}
|
||||
|
||||
@@ -54,45 +65,70 @@ class Visualizer {
|
||||
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)) {
|
||||
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);
|
||||
const w = Math.floor(window.innerWidth * dpr);
|
||||
const h = Math.floor(window.innerHeight * dpr);
|
||||
if (this.canvas.width !== w || this.canvas.height !== h) {
|
||||
this.canvas.width = w;
|
||||
this.canvas.height = h;
|
||||
}
|
||||
const w = Math.floor(window.innerWidth * dpr), h = Math.floor(window.innerHeight * 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);
|
||||
}
|
||||
|
||||
setScene(i) {
|
||||
this.sceneIndex = Math.max(0, Math.min(this.programs.length - 1, i));
|
||||
setEffect(effect) {
|
||||
// Merge onto current so partial updates (e.g. only family) are safe.
|
||||
this.effect = Object.assign({}, this.effect, effect);
|
||||
}
|
||||
|
||||
// audio: { bass, mid, treble, level, beat }
|
||||
render(timeSec, audio) {
|
||||
// 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;
|
||||
const p = this.programs[this.sceneIndex];
|
||||
const l = this.locs[this.sceneIndex];
|
||||
gl.useProgram(p);
|
||||
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);
|
||||
}
|
||||
|
||||
render(timeSec, audio) {
|
||||
const gl = this.gl, u = this.u, e = this.effect;
|
||||
gl.useProgram(this.program);
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
|
||||
gl.enableVertexAttribArray(l.aPos);
|
||||
gl.vertexAttribPointer(l.aPos, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.enableVertexAttribArray(this.aPos);
|
||||
gl.vertexAttribPointer(this.aPos, 2, gl.FLOAT, false, 0, 0);
|
||||
|
||||
gl.uniform2f(l.uRes, this.canvas.width, this.canvas.height);
|
||||
gl.uniform1f(l.uTime, timeSec);
|
||||
gl.uniform1f(l.uBass, audio.bass);
|
||||
gl.uniform1f(l.uMid, audio.mid);
|
||||
gl.uniform1f(l.uTreble, audio.treble);
|
||||
gl.uniform1f(l.uLevel, audio.level);
|
||||
gl.uniform1f(l.uBeat, audio.beat);
|
||||
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);
|
||||
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.tex);
|
||||
gl.uniform1i(u.uTex, 0);
|
||||
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user