diff --git a/CLAUDE.md b/CLAUDE.md index cf8caf1..ad74e94 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,7 +28,7 @@ See `nisps-core/README.md` for complete documentation and examples. The `playground/` directory contains a browser-based interactive demo of the NISPS ML engine. It's a faithful JavaScript port of nisps-core's MLP + IML, with no build step or dependencies. -- **2 inputs** (virtual joystick X/Y) mapped through a `[3, 10, 10, 14, 14]` MLP to **14 outputs** controlling a Canvas2D flow-field particle system +- **2 inputs** (virtual joystick X/Y) mapped through a `[3, 10, 10, 14, 15]` MLP to **15 outputs** controlling a Canvas2D flow-field particle system - **Two learning modes**: Examples (set slider targets, add examples, train) and RL Feedback (thumbs up/down with exploration noise) - **Serve statically**: `cd playground && python3 -m http.server` - **Mobile-first**: designed for touch/foldable phone use diff --git a/playground/js/app.js b/playground/js/app.js index 6e68469..f028c9a 100644 --- a/playground/js/app.js +++ b/playground/js/app.js @@ -8,7 +8,7 @@ import { Controls } from './ui/controls.js'; import { ParamDisplay } from './ui/param-display.js'; const N_INPUTS = 2; -const N_OUTPUTS = 14; +const N_OUTPUTS = 15; // --- State --- let iml; @@ -213,21 +213,21 @@ window.loadPreset = function(name) { if (name === 'calm-to-chaotic') { // Bottom-left: slow, smooth, cool; top-right: fast, turbulent, warm - iml.addExample([0.1, 0.9], [0.25, 0.3, 0.1, 0.55, 0.2, 0.3, 0.02, 0.05, 0.9, 0.45, 0.25, 0.2, 0.9, 0.0]); - iml.addExample([0.9, 0.1], [0.75, 0.7, 0.9, 0.05, 0.8, 0.7, 0.9, 0.95, 0.3, 0.2, 0.85, 0.7, 0.25, 0.55]); - iml.addExample([0.5, 0.5], [0.5, 0.5, 0.5, 0.3, 0.5, 0.5, 0.4, 0.5, 0.7, 0.6, 0.5, 0.45, 0.55, 0.9]); + iml.addExample([0.1, 0.9], [0.25, 0.3, 0.1, 0.55, 0.2, 0.3, 0.02, 0.05, 0.9, 0.45, 0.25, 0.2, 0.9, 0.0, 0.0]); + iml.addExample([0.9, 0.1], [0.75, 0.7, 0.9, 0.05, 0.8, 0.7, 0.9, 0.95, 0.3, 0.2, 0.85, 0.7, 0.25, 0.55, 1.0]); + iml.addExample([0.5, 0.5], [0.5, 0.5, 0.5, 0.3, 0.5, 0.5, 0.4, 0.5, 0.7, 0.6, 0.5, 0.45, 0.55, 0.9, 0.5]); } else if (name === 'rainbow-sweep') { // Left to right sweeps through hues - iml.addExample([0.0, 0.5], [0.5, 0.5, 0.4, 0.0, 0.3, 0.4, 0.05, 0.3, 0.8, 0.55, 0.4, 0.3, 0.8, 0.0]); - iml.addExample([0.5, 0.5], [0.5, 0.5, 0.4, 0.5, 0.3, 0.4, 0.05, 0.3, 0.8, 0.55, 0.55, 0.35, 0.7, 0.0]); - iml.addExample([1.0, 0.5], [0.5, 0.5, 0.4, 1.0, 0.3, 0.4, 0.05, 0.3, 0.8, 0.55, 0.75, 0.45, 0.6, 0.0]); + iml.addExample([0.0, 0.5], [0.5, 0.5, 0.4, 0.0, 0.3, 0.4, 0.05, 0.3, 0.8, 0.55, 0.4, 0.3, 0.8, 0.0, 0.0]); + iml.addExample([0.5, 0.5], [0.5, 0.5, 0.4, 0.5, 0.3, 0.4, 0.05, 0.3, 0.8, 0.55, 0.55, 0.35, 0.7, 0.0, 0.4]); + iml.addExample([1.0, 0.5], [0.5, 0.5, 0.4, 1.0, 0.3, 0.4, 0.05, 0.3, 0.8, 0.55, 0.75, 0.45, 0.6, 0.0, 0.8]); } else if (name === 'vortex') { // Center: tight spiral, edges: wide flow - iml.addExample([0.5, 0.5], [0.0, 0.8, 0.8, 0.6, 0.1, 0.15, 0.02, 1.0, 1.0, 0.3, 0.95, 0.85, 0.25, 1.0]); - iml.addExample([0.0, 0.0], [0.5, 0.2, 0.3, 0.8, 0.9, 0.6, 0.08, 0.1, 0.35, 0.8, 0.25, 0.15, 0.8, 0.5]); - iml.addExample([1.0, 1.0], [0.5, 0.2, 0.3, 0.2, 0.9, 0.6, 0.08, 0.1, 0.35, 0.8, 0.25, 0.15, 0.8, 0.5]); - iml.addExample([0.0, 1.0], [0.3, 0.4, 0.5, 0.4, 0.5, 0.4, 0.05, 0.5, 0.65, 0.5, 0.55, 0.45, 0.45, 0.2]); - iml.addExample([1.0, 0.0], [0.7, 0.4, 0.5, 0.0, 0.5, 0.4, 0.05, 0.5, 0.65, 0.5, 0.55, 0.45, 0.45, 0.2]); + iml.addExample([0.5, 0.5], [0.0, 0.8, 0.8, 0.6, 0.1, 0.15, 0.02, 1.0, 1.0, 0.3, 0.95, 0.85, 0.25, 1.0, 0.5]); + iml.addExample([0.0, 0.0], [0.5, 0.2, 0.3, 0.8, 0.9, 0.6, 0.08, 0.1, 0.35, 0.8, 0.25, 0.15, 0.8, 0.5, 0.2]); + iml.addExample([1.0, 1.0], [0.5, 0.2, 0.3, 0.2, 0.9, 0.6, 0.08, 0.1, 0.35, 0.8, 0.25, 0.15, 0.8, 0.5, 0.8]); + iml.addExample([0.0, 1.0], [0.3, 0.4, 0.5, 0.4, 0.5, 0.4, 0.05, 0.5, 0.65, 0.5, 0.55, 0.45, 0.45, 0.2, 0.4]); + iml.addExample([1.0, 0.0], [0.7, 0.4, 0.5, 0.0, 0.5, 0.4, 0.05, 0.5, 0.65, 0.5, 0.55, 0.45, 0.45, 0.2, 0.9]); } const loss = trainModel(); diff --git a/playground/js/ui/param-display.js b/playground/js/ui/param-display.js index b7e5380..378cfc5 100644 --- a/playground/js/ui/param-display.js +++ b/playground/js/ui/param-display.js @@ -1,11 +1,11 @@ // Parameter bar display // Shows output parameters as horizontal bars, draggable in examples mode -const PARAM_NAMES = ['Flow', 'Scale', 'Speed', 'Hue', 'Spread', 'Size', 'Trail', 'Turb', 'Attract', 'Radius', 'DispRate', 'DispAmt', 'Lifetime', 'Respawn']; -const PARAM_COLORS = ['#00ff88', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00', '#0088ff', '#ff3366', '#9bff5f', '#59d3ff', '#ff8f3f', '#a0b7ff', '#f4ff7a', '#ffa8db']; +const PARAM_NAMES = ['Flow', 'Scale', 'Speed', 'Hue', 'Spread', 'Size', 'Trail', 'Turb', 'Attract', 'Radius', 'DispRate', 'DispAmt', 'Lifetime', 'Respawn', 'Advection']; +const PARAM_COLORS = ['#00ff88', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00', '#0088ff', '#ff3366', '#9bff5f', '#59d3ff', '#ff8f3f', '#a0b7ff', '#f4ff7a', '#ffa8db', '#7dffc8']; export class ParamDisplay { - constructor(container, numParams = 14) { + constructor(container, numParams = 15) { this.container = container; this.numParams = numParams; this.values = new Array(numParams).fill(0.5); diff --git a/playground/js/ui/visualizer.js b/playground/js/ui/visualizer.js index e62b65f..bfee5ad 100644 --- a/playground/js/ui/visualizer.js +++ b/playground/js/ui/visualizer.js @@ -1,5 +1,5 @@ // Flow field particle system with Canvas2D -// Controlled by 14 output parameters from the IML network +// Controlled by 15 output parameters from the IML network // Simple value noise (no dependencies) const PERM = new Uint8Array(512); @@ -69,6 +69,7 @@ export class FlowFieldVisualizer { dispersionAmount: 1.0, // p11: strength of outward dispersion particleLifetime: 220, // p12: average frames before respawn respawnStyle: 0.0, // p13: 0=random, 1=edge, 2=center-burst + advectionMode: 0.0, // p14: flow->orbit->radial blend }; this.resize(); @@ -139,7 +140,7 @@ export class FlowFieldVisualizer { // Set parameters from IML output (all values 0-1) setParams(outputs) { - if (!outputs || outputs.length < 14) return; + if (!outputs || outputs.length < 15) return; this.params.angleOffset = outputs[0] * TWO_PI; this.params.scale = 0.001 + outputs[1] * 0.009; this.params.speed = 0.5 + outputs[2] * 4.5; @@ -154,6 +155,7 @@ export class FlowFieldVisualizer { this.params.dispersionAmount = outputs[11] * 3; this.params.particleLifetime = 30 + outputs[12] * 470; this.params.respawnStyle = outputs[13]; + this.params.advectionMode = outputs[14]; } draw() { @@ -165,21 +167,44 @@ export class FlowFieldVisualizer { ctx.fillRect(0, 0, width, height); for (const p of this.particles) { + const cx = width * 0.5; + const cy = height * 0.5; + // Sample flow field const nx = p.x * params.scale; const ny = p.y * params.scale; const angle = noise2D(nx + this.time, ny) * TWO_PI + params.angleOffset; const curl = noise2D(nx + 100, ny + 100 + this.time * 0.5) * params.turbulence; - // Move particle - const vx = Math.cos(angle + curl) * params.speed; - const vy = Math.sin(angle + curl) * params.speed; + // Mix between three advection fields for larger visual mode changes. + const flowVx = Math.cos(angle + curl) * params.speed; + const flowVy = Math.sin(angle + curl) * params.speed; + const fromCenterX = p.x - cx; + const fromCenterY = p.y - cy; + const centerDist = Math.hypot(fromCenterX, fromCenterY) + 1e-6; + const radialX = fromCenterX / centerDist; + const radialY = fromCenterY / centerDist; + const orbitX = -radialY; + const orbitY = radialX; + const orbitVx = orbitX * params.speed; + const orbitVy = orbitY * params.speed; + const radialVx = radialX * params.speed; + const radialVy = radialY * params.speed; + + const modeBlend = params.advectionMode * 2; + let vx; + let vy; + if (modeBlend < 1) { + vx = lerp(flowVx, orbitVx, modeBlend); + vy = lerp(flowVy, orbitVy, modeBlend); + } else { + vx = lerp(orbitVx, radialVx, modeBlend - 1); + vy = lerp(orbitVy, radialVy, modeBlend - 1); + } let nextX = p.x + vx; let nextY = p.y + vy; // Central attractor keeps trajectories from sticking to the outer edges. - const cx = width * 0.5; - const cy = height * 0.5; const dx = cx - nextX; const dy = cy - nextY; const dist = Math.hypot(dx, dy) + 1e-6;