From 6187a4b4aeafe8b26560f629f2773e178963353f Mon Sep 17 00:00:00 2001 From: w1n5t0n Date: Wed, 11 Feb 2026 15:50:06 +0000 Subject: [PATCH] playground: add orbiting repulsor field controls --- CLAUDE.md | 2 +- playground/js/app.js | 24 ++++++++++++------------ playground/js/ui/param-display.js | 6 +++--- playground/js/ui/visualizer.js | 26 ++++++++++++++++++++++++-- 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 18736f7..2bcfc3b 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, 17]` MLP to **17 outputs** controlling a Canvas2D flow-field particle system +- **2 inputs** (virtual joystick X/Y) mapped through a `[3, 10, 10, 14, 20]` MLP to **20 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 d4bc148..3f1abea 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 = 17; +const N_OUTPUTS = 20; // --- 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, 0.0, 0.2, 0.05]); - 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, 0.92, 0.02]); - 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, 0.65, 0.08]); + 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, 0.2, 0.05, 0.0, 0.0, 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, 0.25, 0.55, 1.0, 0.92, 0.02, 0.95, 0.8, 0.85]); + 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, 0.65, 0.08, 0.45, 0.4, 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, 0.0, 0.45, 0.08]); - 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, 0.45, 0.08]); - 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, 0.45, 0.08]); + 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, 0.45, 0.08, 0.2, 0.25, 0.45]); + 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, 0.45, 0.08, 0.45, 0.4, 0.6]); + 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, 0.45, 0.08, 0.7, 0.55, 0.75]); } 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, 0.5, 0.95, 0.01]); - 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, 0.35, 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, 0.35, 0.2]); - 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, 0.7, 0.1]); - 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, 0.7, 0.1]); + 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, 0.95, 0.01, 1.0, 1.0, 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, 0.2, 0.35, 0.2, 0.15, 0.2, 0.25]); + 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, 0.35, 0.2, 0.15, 0.2, 0.25]); + 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, 0.7, 0.1, 0.5, 0.4, 0.55]); + 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, 0.7, 0.1, 0.5, 0.4, 0.55]); } const loss = trainModel(); diff --git a/playground/js/ui/param-display.js b/playground/js/ui/param-display.js index b4cdb98..9f7c3c0 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', 'Advection', 'Inertia', 'Drag']; -const PARAM_COLORS = ['#00ff88', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00', '#0088ff', '#ff3366', '#9bff5f', '#59d3ff', '#ff8f3f', '#a0b7ff', '#f4ff7a', '#ffa8db', '#7dffc8', '#ffd166', '#8ad4ff']; +const PARAM_NAMES = ['Flow', 'Scale', 'Speed', 'Hue', 'Spread', 'Size', 'Trail', 'Turb', 'Attract', 'Radius', 'DispRate', 'DispAmt', 'Lifetime', 'Respawn', 'Advection', 'Inertia', 'Drag', 'Repulse', 'RepCnt', 'RepRate']; +const PARAM_COLORS = ['#00ff88', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00', '#0088ff', '#ff3366', '#9bff5f', '#59d3ff', '#ff8f3f', '#a0b7ff', '#f4ff7a', '#ffa8db', '#7dffc8', '#ffd166', '#8ad4ff', '#ff5f5f', '#ffc15f', '#ff8a3d']; export class ParamDisplay { - constructor(container, numParams = 17) { + constructor(container, numParams = 20) { 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 9e08f62..904a41b 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 17 output parameters from the IML network +// Controlled by 20 output parameters from the IML network // Simple value noise (no dependencies) const PERM = new Uint8Array(512); @@ -72,6 +72,9 @@ export class FlowFieldVisualizer { advectionMode: 0.0, // p14: flow->orbit->radial blend inertia: 0.2, // p15: velocity memory drag: 0.02, // p16: velocity damping + repulsorStrength: 0.0, // p17: repulsor force amount + repulsorCount: 0, // p18: number of active repulsors + repulsorOrbitRate: 0.8, // p19: repulsor orbital speed }; this.resize(); @@ -154,7 +157,7 @@ export class FlowFieldVisualizer { // Set parameters from IML output (all values 0-1) setParams(outputs) { - if (!outputs || outputs.length < 17) return; + if (!outputs || outputs.length < 20) 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; @@ -172,6 +175,9 @@ export class FlowFieldVisualizer { this.params.advectionMode = outputs[14]; this.params.inertia = outputs[15] * 0.98; this.params.drag = outputs[16] * 0.35; + this.params.repulsorStrength = outputs[17] * 4.5; + this.params.repulsorCount = Math.floor(outputs[18] * 4.999); + this.params.repulsorOrbitRate = 0.1 + outputs[19] * 2.9; } draw() { @@ -241,6 +247,22 @@ export class FlowFieldVisualizer { nextX -= nxCenter * dispersionForce; nextY -= nyCenter * dispersionForce; + // Orbiting repulsor points carve dynamic voids and bursts. + const repulsorRadius = Math.min(width, height) * 0.28; + for (let r = 0; r < params.repulsorCount; r++) { + const phase = this.time * params.repulsorOrbitRate + (r / 4) * TWO_PI; + const wobble = 0.6 + 0.15 * r; + const rx = cx + Math.cos(phase * (1.0 + wobble)) * repulsorRadius; + const ry = cy + Math.sin(phase * (1.3 + wobble)) * repulsorRadius; + const repulseDx = nextX - rx; + const repulseDy = nextY - ry; + const distSq = repulseDx * repulseDx + repulseDy * repulseDy + 160; + const distInv = 1 / Math.sqrt(distSq); + const force = params.repulsorStrength * (650 / distSq); + nextX += repulseDx * distInv * force; + nextY += repulseDy * distInv * force; + } + p.x = nextX; p.y = nextY;