playground: add inertia and drag velocity memory

This commit is contained in:
w1n5t0n 2026-02-11 15:48:34 +00:00
parent 06833cdd19
commit 0498bbe371
4 changed files with 46 additions and 26 deletions

View file

@ -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. 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, 15]` MLP to **15 outputs** controlling a Canvas2D flow-field particle system - **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
- **Two learning modes**: Examples (set slider targets, add examples, train) and RL Feedback (thumbs up/down with exploration noise) - **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` - **Serve statically**: `cd playground && python3 -m http.server`
- **Mobile-first**: designed for touch/foldable phone use - **Mobile-first**: designed for touch/foldable phone use

View file

@ -8,7 +8,7 @@ import { Controls } from './ui/controls.js';
import { ParamDisplay } from './ui/param-display.js'; import { ParamDisplay } from './ui/param-display.js';
const N_INPUTS = 2; const N_INPUTS = 2;
const N_OUTPUTS = 15; const N_OUTPUTS = 17;
// --- State --- // --- State ---
let iml; let iml;
@ -213,21 +213,21 @@ window.loadPreset = function(name) {
if (name === 'calm-to-chaotic') { if (name === 'calm-to-chaotic') {
// Bottom-left: slow, smooth, cool; top-right: fast, turbulent, warm // 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]); 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]); 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]); 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]);
} else if (name === 'rainbow-sweep') { } else if (name === 'rainbow-sweep') {
// Left to right sweeps through hues // 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]); 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]); 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]); 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]);
} else if (name === 'vortex') { } else if (name === 'vortex') {
// Center: tight spiral, edges: wide flow // 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]); 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]); 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]); 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]); 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]); 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]);
} }
const loss = trainModel(); const loss = trainModel();

View file

@ -1,11 +1,11 @@
// Parameter bar display // Parameter bar display
// Shows output parameters as horizontal bars, draggable in examples mode // 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']; 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']; const PARAM_COLORS = ['#00ff88', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00', '#0088ff', '#ff3366', '#9bff5f', '#59d3ff', '#ff8f3f', '#a0b7ff', '#f4ff7a', '#ffa8db', '#7dffc8', '#ffd166', '#8ad4ff'];
export class ParamDisplay { export class ParamDisplay {
constructor(container, numParams = 15) { constructor(container, numParams = 17) {
this.container = container; this.container = container;
this.numParams = numParams; this.numParams = numParams;
this.values = new Array(numParams).fill(0.5); this.values = new Array(numParams).fill(0.5);

View file

@ -1,5 +1,5 @@
// Flow field particle system with Canvas2D // Flow field particle system with Canvas2D
// Controlled by 15 output parameters from the IML network // Controlled by 17 output parameters from the IML network
// Simple value noise (no dependencies) // Simple value noise (no dependencies)
const PERM = new Uint8Array(512); const PERM = new Uint8Array(512);
@ -70,6 +70,8 @@ export class FlowFieldVisualizer {
particleLifetime: 220, // p12: average frames before respawn particleLifetime: 220, // p12: average frames before respawn
respawnStyle: 0.0, // p13: 0=random, 1=edge, 2=center-burst respawnStyle: 0.0, // p13: 0=random, 1=edge, 2=center-burst
advectionMode: 0.0, // p14: flow->orbit->radial blend advectionMode: 0.0, // p14: flow->orbit->radial blend
inertia: 0.2, // p15: velocity memory
drag: 0.02, // p16: velocity damping
}; };
this.resize(); this.resize();
@ -103,6 +105,8 @@ export class FlowFieldVisualizer {
id, id,
age: Math.floor(Math.random() * this.params.particleLifetime), age: Math.floor(Math.random() * this.params.particleLifetime),
life: this.computeLifetime(), life: this.computeLifetime(),
vx: 0,
vy: 0,
}; };
} }
@ -122,16 +126,26 @@ export class FlowFieldVisualizer {
if (side === 1) { p.x = width; p.y = Math.random() * height; } if (side === 1) { p.x = width; p.y = Math.random() * height; }
if (side === 2) { p.x = Math.random() * width; p.y = height; } if (side === 2) { p.x = Math.random() * width; p.y = height; }
if (side === 3) { p.x = 0; p.y = Math.random() * height; } if (side === 3) { p.x = 0; p.y = Math.random() * height; }
// Give edge spawns an inward impulse.
const towardCenterX = width * 0.5 - p.x;
const towardCenterY = height * 0.5 - p.y;
const inwardDist = Math.hypot(towardCenterX, towardCenterY) + 1e-6;
p.vx = (towardCenterX / inwardDist) * 2.0;
p.vy = (towardCenterY / inwardDist) * 2.0;
} else if (mode === 2) { } else if (mode === 2) {
// Center-burst respawn // Center-burst respawn
const angle = Math.random() * TWO_PI; const angle = Math.random() * TWO_PI;
const r = Math.random() * Math.min(width, height) * 0.08; const r = Math.random() * Math.min(width, height) * 0.08;
p.x = width * 0.5 + Math.cos(angle) * r; p.x = width * 0.5 + Math.cos(angle) * r;
p.y = height * 0.5 + Math.sin(angle) * r; p.y = height * 0.5 + Math.sin(angle) * r;
p.vx = Math.cos(angle) * 2.5;
p.vy = Math.sin(angle) * 2.5;
} else { } else {
// Random respawn // Random respawn
p.x = Math.random() * width; p.x = Math.random() * width;
p.y = Math.random() * height; p.y = Math.random() * height;
p.vx = (Math.random() * 2 - 1) * 0.5;
p.vy = (Math.random() * 2 - 1) * 0.5;
} }
p.age = 0; p.age = 0;
@ -140,7 +154,7 @@ export class FlowFieldVisualizer {
// Set parameters from IML output (all values 0-1) // Set parameters from IML output (all values 0-1)
setParams(outputs) { setParams(outputs) {
if (!outputs || outputs.length < 15) return; if (!outputs || outputs.length < 17) return;
this.params.angleOffset = outputs[0] * TWO_PI; this.params.angleOffset = outputs[0] * TWO_PI;
this.params.scale = 0.001 + outputs[1] * 0.009; this.params.scale = 0.001 + outputs[1] * 0.009;
this.params.speed = 0.5 + outputs[2] * 4.5; this.params.speed = 0.5 + outputs[2] * 4.5;
@ -156,6 +170,8 @@ export class FlowFieldVisualizer {
this.params.particleLifetime = 30 + outputs[12] * 470; this.params.particleLifetime = 30 + outputs[12] * 470;
this.params.respawnStyle = outputs[13]; this.params.respawnStyle = outputs[13];
this.params.advectionMode = outputs[14]; this.params.advectionMode = outputs[14];
this.params.inertia = outputs[15] * 0.98;
this.params.drag = outputs[16] * 0.35;
} }
draw() { draw() {
@ -192,17 +208,21 @@ export class FlowFieldVisualizer {
const radialVy = radialY * params.speed; const radialVy = radialY * params.speed;
const modeBlend = params.advectionMode * 2; const modeBlend = params.advectionMode * 2;
let vx; let targetVx;
let vy; let targetVy;
if (modeBlend < 1) { if (modeBlend < 1) {
vx = lerp(flowVx, orbitVx, modeBlend); targetVx = lerp(flowVx, orbitVx, modeBlend);
vy = lerp(flowVy, orbitVy, modeBlend); targetVy = lerp(flowVy, orbitVy, modeBlend);
} else { } else {
vx = lerp(orbitVx, radialVx, modeBlend - 1); targetVx = lerp(orbitVx, radialVx, modeBlend - 1);
vy = lerp(orbitVy, radialVy, modeBlend - 1); targetVy = lerp(orbitVy, radialVy, modeBlend - 1);
} }
let nextX = p.x + vx; p.vx = p.vx * params.inertia + targetVx * (1 - params.inertia);
let nextY = p.y + vy; p.vy = p.vy * params.inertia + targetVy * (1 - params.inertia);
p.vx *= (1 - params.drag);
p.vy *= (1 - params.drag);
let nextX = p.x + p.vx;
let nextY = p.y + p.vy;
// Central attractor keeps trajectories from sticking to the outer edges. // Central attractor keeps trajectories from sticking to the outer edges.
const dx = cx - nextX; const dx = cx - nextX;