From 97acfd282477d42b100ec53c535a8cec1f196814 Mon Sep 17 00:00:00 2001 From: w1n5t0n Date: Wed, 11 Feb 2026 15:45:50 +0000 Subject: [PATCH] playground: add particle lifetime and respawn modes --- CLAUDE.md | 2 +- playground/js/app.js | 24 ++++++------- playground/js/ui/param-display.js | 6 ++-- playground/js/ui/visualizer.js | 59 +++++++++++++++++++++++++++---- 4 files changed, 68 insertions(+), 23 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4739185..cf8caf1 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, 12]` MLP to **12 outputs** controlling a Canvas2D flow-field particle system +- **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 - **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 8104658..6e68469 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 = 12; +const N_OUTPUTS = 14; // --- 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]); - 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]); - 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]); + 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]); } 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]); - 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]); - 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]); + 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]); } 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]); - 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]); - 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]); - 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]); - 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]); + 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]); } const loss = trainModel(); diff --git a/playground/js/ui/param-display.js b/playground/js/ui/param-display.js index 38b0415..b7e5380 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']; -const PARAM_COLORS = ['#00ff88', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00', '#0088ff', '#ff3366', '#9bff5f', '#59d3ff', '#ff8f3f', '#a0b7ff']; +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']; export class ParamDisplay { - constructor(container, numParams = 12) { + constructor(container, numParams = 14) { 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 59e4d54..e62b65f 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 12 output parameters from the IML network +// Controlled by 14 output parameters from the IML network // Simple value noise (no dependencies) const PERM = new Uint8Array(512); @@ -67,6 +67,8 @@ export class FlowFieldVisualizer { attractRadius: 200, // p9: radius where attraction is strongest dispersionRate: 2.0, // p10: speed of outward dispersion pulses 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 }; this.resize(); @@ -86,20 +88,58 @@ export class FlowFieldVisualizer { initParticles() { this.particles = []; for (let i = 0; i < this.numParticles; i++) { - this.particles.push({ - x: Math.random() * this.width, - y: Math.random() * this.height, - id: i, - }); + this.particles.push(this.makeParticle(i)); } // Clear canvas to black this.ctx.fillStyle = '#0d0d0d'; this.ctx.fillRect(0, 0, this.width, this.height); } + makeParticle(id) { + return { + x: Math.random() * this.width, + y: Math.random() * this.height, + id, + age: Math.floor(Math.random() * this.params.particleLifetime), + life: this.computeLifetime(), + }; + } + + computeLifetime() { + const variance = 0.65 + Math.random() * 0.7; + return Math.max(10, Math.floor(this.params.particleLifetime * variance)); + } + + respawnParticle(p) { + const mode = Math.min(2, Math.floor(this.params.respawnStyle * 2.999)); + const { width, height } = this; + + if (mode === 1) { + // Edge respawn + const side = Math.floor(Math.random() * 4); + if (side === 0) { p.x = Math.random() * width; p.y = 0; } + if (side === 1) { p.x = width; p.y = Math.random() * height; } + if (side === 2) { p.x = Math.random() * width; p.y = height; } + if (side === 3) { p.x = 0; p.y = Math.random() * height; } + } else if (mode === 2) { + // Center-burst respawn + const angle = Math.random() * TWO_PI; + const r = Math.random() * Math.min(width, height) * 0.08; + p.x = width * 0.5 + Math.cos(angle) * r; + p.y = height * 0.5 + Math.sin(angle) * r; + } else { + // Random respawn + p.x = Math.random() * width; + p.y = Math.random() * height; + } + + p.age = 0; + p.life = this.computeLifetime(); + } + // Set parameters from IML output (all values 0-1) setParams(outputs) { - if (!outputs || outputs.length < 12) return; + if (!outputs || outputs.length < 14) 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; @@ -112,6 +152,8 @@ export class FlowFieldVisualizer { this.params.attractRadius = 40 + outputs[9] * 420; this.params.dispersionRate = 0.2 + outputs[10] * 8; this.params.dispersionAmount = outputs[11] * 3; + this.params.particleLifetime = 30 + outputs[12] * 470; + this.params.respawnStyle = outputs[13]; } draw() { @@ -163,6 +205,9 @@ export class FlowFieldVisualizer { if (p.y < 0) p.y += height; if (p.y > height) p.y -= height; + p.age += 1; + if (p.age >= p.life) this.respawnParticle(p); + // Color based on particle id + hue params const hue = (params.hueBase + (p.id / this.numParticles) * params.hueSpread) % 360; const lightness = 50 + Math.sin(p.id * 0.1 + this.time) * 15;