Ten more interactive camera families: bubbles, balloons, particle mirror, cloth, light painting, touch fluid, electric silhouette, swarm, tile wall, firewall

interactive.js becomes a multi-mode engine sharing camera, motion grid,
gradient/centroid tracking and the mouse fallback. New renderers: point/line
sprites (mirror/swarm/cloth/pops) and a ping-pong trail FBO (painting).
fluid.js gains a 'touch' emitter mode fed by motion splats from the camera.
Mirror mode reforms the custom SVG/image if loaded, else a DJ LUZA wordmark.
UNPACK_ALIGNMENT set to 1 for non-multiple-of-4 single-channel uploads.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Fable 5 committed 2026-07-29 23:26:34 +02:00
1 parent ea31d68350
commit 7e745743c5
4 files changed
+886 -154

No files matched your search

+16 -5
View File
@@ -54,10 +54,20 @@ const FAMILIES = [
{ name: 'Tunnel Neon', scale: 1.0 },
{ name: 'Tempesta Plasma', scale: 1.0 },
{ name: 'Supernova', scale: 1.0 },
// Camera-interactive family (interactive.js): webcam motion pushes physical
// balls around the screen. Excluded from Auto VJ (turning the camera on must
// stay a deliberate user choice).
{ name: 'Interattivo Palle', scale: 1.0, interactive: true }
// Camera-interactive families (interactive.js): webcam motion drives the
// scene (interMode picks the behaviour). Excluded from Auto VJ (turning the
// camera on must stay a deliberate user choice). Append-only.
{ name: 'Interattivo Palle', scale: 1.0, interactive: true },
{ name: 'Interattivo Bolle', scale: 1.0, interactive: true, interMode: 'bubbles' },
{ name: 'Interattivo Palloncini', scale: 1.0, interactive: true, interMode: 'balloons' },
{ name: 'Interattivo Specchio', scale: 1.0, interactive: true, interMode: 'mirror' },
{ name: 'Interattivo Tessuto', scale: 1.0, interactive: true, interMode: 'cloth' },
{ name: 'Interattivo Pittura', scale: 1.0, interactive: true, interMode: 'paint' },
{ name: 'Interattivo Fluido', scale: 1.0, interactive: true, interMode: 'fluid' },
{ name: 'Interattivo Silhouette', scale: 1.0, interactive: true, interMode: 'silhouette' },
{ name: 'Interattivo Sciame', scale: 1.0, interactive: true, interMode: 'swarm' },
{ name: 'Interattivo Piastrelle', scale: 1.0, interactive: true, interMode: 'tiles' },
{ name: 'Interattivo Firewall', scale: 1.0, interactive: true, interMode: 'firewall' }
];
// Palettes: low colour (a) -> high colour (b), plus optional hue-cycle/sat.
@@ -117,7 +127,8 @@ function makeEffect(fi, pi, vi) {
// setEffect can't leave a stale flag behind when switching presets.
isFluid: fam.fluid ? 1 : 0,
fluidMode: fam.fluidMode || 'ink',
isInteractive: fam.interactive ? 1 : 0
isInteractive: fam.interactive ? 1 : 0,
interactiveMode: fam.interMode || 'balls'
};
}
+24
View File
@@ -279,6 +279,30 @@ class FluidSim {
this._splat(0.06, 0.5 + 0.3 * Math.sin(t * 5.3), 1000 * (0.4 + bass), 0,
c[0] * 0.5, c[1] * 0.5, c[2] * 0.5, 0.0018);
}
} else if (mode === 'touch') {
// Camera-interactive (InteractiveSim): splats come from detected motion,
// pushed along the direction the person is moving; a faint idle plume
// keeps the screen alive when nobody moves.
const list = this.extSplats || [];
const FT = (300 + 420 * bass) * dt * 60;
for (const s of list) {
const c = dyeCol(s.ph);
const k = Math.min(1, s.k * 1.8);
this._splat(s.x, s.y, s.dx * FT * (0.6 + k), s.dy * FT * (0.6 + k),
c[0] * inj * 9.0 * k, c[1] * inj * 9.0 * k, c[2] * inj * 9.0 * k,
0.004 + 0.004 * k);
}
this.extSplats = null;
if (!list.length) {
const c = dyeCol(0.2);
const x = 0.5 + 0.25 * Math.sin(t * 0.35);
this._splat(x, 0.35, Math.sin(t * 0.9) * force * 0.3, force * 0.22,
c[0] * inj * 1.2, c[1] * inj * 1.2, c[2] * inj * 1.2, rad * 1.2);
}
if (onBeat) {
const c = dyeCol(0.6);
this._splat(0.5, 0.5, 0, 0, c[0] * 0.3, c[1] * 0.3, c[2] * 0.3, 0.004);
}
} else {
// 'ink': wandering brushes (the original mode)
for (let i = 0; i < this.emitters.length; i++) {
+842 -149
View File
File diff suppressed because it is too large. Load diff
+4
View File
@@ -90,6 +90,9 @@ class Visualizer {
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) {
@@ -98,6 +101,7 @@ class Visualizer {
if (this.inter && !e.isInteractive) this.inter.suspend();
if (e.isInteractive && window.InteractiveSim) {
if (!this.inter) this.inter = new window.InteractiveSim(gl);
this.inter.customSource = this.customSource || null;
this.inter.render(timeSec, audio, e, this.canvas);
return;
}