playground: add particle lifetime and respawn modes
This commit is contained in:
parent
0550088516
commit
97acfd2824
4 changed files with 68 additions and 23 deletions
|
|
@ -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, 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)
|
- **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
|
||||||
|
|
|
||||||
|
|
@ -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 = 12;
|
const N_OUTPUTS = 14;
|
||||||
|
|
||||||
// --- 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]);
|
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]);
|
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]);
|
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') {
|
} 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]);
|
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]);
|
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]);
|
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') {
|
} 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]);
|
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]);
|
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]);
|
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]);
|
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]);
|
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();
|
const loss = trainModel();
|
||||||
|
|
|
||||||
|
|
@ -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'];
|
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'];
|
const PARAM_COLORS = ['#00ff88', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00', '#0088ff', '#ff3366', '#9bff5f', '#59d3ff', '#ff8f3f', '#a0b7ff', '#f4ff7a', '#ffa8db'];
|
||||||
|
|
||||||
export class ParamDisplay {
|
export class ParamDisplay {
|
||||||
constructor(container, numParams = 12) {
|
constructor(container, numParams = 14) {
|
||||||
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);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
// Flow field particle system with Canvas2D
|
// 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)
|
// Simple value noise (no dependencies)
|
||||||
const PERM = new Uint8Array(512);
|
const PERM = new Uint8Array(512);
|
||||||
|
|
@ -67,6 +67,8 @@ export class FlowFieldVisualizer {
|
||||||
attractRadius: 200, // p9: radius where attraction is strongest
|
attractRadius: 200, // p9: radius where attraction is strongest
|
||||||
dispersionRate: 2.0, // p10: speed of outward dispersion pulses
|
dispersionRate: 2.0, // p10: speed of outward dispersion pulses
|
||||||
dispersionAmount: 1.0, // p11: strength of outward dispersion
|
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();
|
this.resize();
|
||||||
|
|
@ -86,20 +88,58 @@ export class FlowFieldVisualizer {
|
||||||
initParticles() {
|
initParticles() {
|
||||||
this.particles = [];
|
this.particles = [];
|
||||||
for (let i = 0; i < this.numParticles; i++) {
|
for (let i = 0; i < this.numParticles; i++) {
|
||||||
this.particles.push({
|
this.particles.push(this.makeParticle(i));
|
||||||
x: Math.random() * this.width,
|
|
||||||
y: Math.random() * this.height,
|
|
||||||
id: i,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// Clear canvas to black
|
// Clear canvas to black
|
||||||
this.ctx.fillStyle = '#0d0d0d';
|
this.ctx.fillStyle = '#0d0d0d';
|
||||||
this.ctx.fillRect(0, 0, this.width, this.height);
|
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)
|
// Set parameters from IML output (all values 0-1)
|
||||||
setParams(outputs) {
|
setParams(outputs) {
|
||||||
if (!outputs || outputs.length < 12) return;
|
if (!outputs || outputs.length < 14) 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;
|
||||||
|
|
@ -112,6 +152,8 @@ export class FlowFieldVisualizer {
|
||||||
this.params.attractRadius = 40 + outputs[9] * 420;
|
this.params.attractRadius = 40 + outputs[9] * 420;
|
||||||
this.params.dispersionRate = 0.2 + outputs[10] * 8;
|
this.params.dispersionRate = 0.2 + outputs[10] * 8;
|
||||||
this.params.dispersionAmount = outputs[11] * 3;
|
this.params.dispersionAmount = outputs[11] * 3;
|
||||||
|
this.params.particleLifetime = 30 + outputs[12] * 470;
|
||||||
|
this.params.respawnStyle = outputs[13];
|
||||||
}
|
}
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
|
|
@ -163,6 +205,9 @@ export class FlowFieldVisualizer {
|
||||||
if (p.y < 0) p.y += height;
|
if (p.y < 0) p.y += height;
|
||||||
if (p.y > height) 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
|
// Color based on particle id + hue params
|
||||||
const hue = (params.hueBase + (p.id / this.numParticles) * params.hueSpread) % 360;
|
const hue = (params.hueBase + (p.id / this.numParticles) * params.hueSpread) % 360;
|
||||||
const lightness = 50 + Math.sin(p.id * 0.1 + this.time) * 15;
|
const lightness = 50 + Math.sin(p.id * 0.1 + this.time) * 15;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue