-
-
-
-
- Calm/Chaos
- Rainbow
- Vortex
-
-
Expand
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
NISPS Playground
-
Train a neural network to map joystick positions to output parameters using interactive machine learning.
-
-
Modes
-
Open the side panel (☰) to switch between Visual and Synth modes.
-
-
Visual Mode
-
20 outputs control a flow-field particle system.
-
-
Synth Mode
-
20 outputs control a C15 synthesizer engine. Use the arpeggiator for automatic note playback.
-
-
Examples Mode
-
- Move the joystick to a position
- Drag the parameter bars to set desired output
- Press Add Example to save this mapping
- Repeat for different joystick positions
- Press Train — the network learns to interpolate between your examples
-
-
-
RL Feedback Mode
-
- Move the joystick — the network produces outputs
- See something you like? Press + (thumbs up)
- Don't like it? Press − (thumbs down) to explore more
- Double-click the joystick to toggle Follow mode
-
-
-
Tips
-
Your training data auto-saves to your browser. Gamepad supported (LB/RB for RL feedback).
-
Tap anywhere outside to close
-
-
-
-
-
diff --git a/playground/js/a-app.js b/playground/js/a-app.js
index bbf687e..cc2c0f5 100644
--- a/playground/js/a-app.js
+++ b/playground/js/a-app.js
@@ -20,7 +20,7 @@ const VISUAL_PARAM_NAMES = [
'Advection', 'Inertia', 'Drag', 'Repulse', 'RepCnt', 'RepRate',
];
const VISUAL_PARAM_COLORS = [
- '#00ff88', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00',
+ '#ff6a00', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00',
'#0088ff', '#ff3366', '#9bff5f', '#59d3ff', '#ff8f3f', '#a0b7ff',
'#f4ff7a', '#ffa8db', '#7dffc8', '#ffd166', '#8ad4ff', '#ff5f5f',
'#ffc15f', '#ff8a3d',
@@ -67,6 +67,7 @@ let arpeggiator = null;
let learningMode = 'rl';
let outputMode = 'visual';
let tameLevel = 0;
+let spreadLevel = 0.6;
let noiseLevel = 0.05;
const rlExplorationDecay = 0.97;
@@ -254,7 +255,7 @@ class SynthVisualizer {
const dpr = window.devicePixelRatio || 1;
const fontSize = 9 * dpr;
ctx.save();
- ctx.font = `${fontSize}px -apple-system, BlinkMacSystemFont, sans-serif`;
+ ctx.font = `${fontSize}px 'JetBrains Mono', monospace`;
ctx.fillStyle = color + '88';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
@@ -360,6 +361,9 @@ function init() {
// Parse ?tame URL param
const urlParams = new URLSearchParams(window.location.search);
tameLevel = parseFloat(urlParams.get('tame') ?? '0');
+ spreadLevel = parseFloat(urlParams.get('spread') ?? '0');
+ if (isNaN(spreadLevel)) spreadLevel = 0;
+ spreadLevel = Math.max(0, Math.min(1, spreadLevel));
// IML — fresh random weights each boot, no state restoration
iml = new IML(N_INPUTS, N_OUTPUTS, [32, 48, 64], 1000, 1.0, 0.00001);
@@ -557,9 +561,9 @@ function drawJoyMap() {
const py = (1 - joyY) * h;
// Glow
- ctx.shadowColor = 'rgba(0, 255, 136, 0.6)';
+ ctx.shadowColor = 'rgba(255, 106, 0, 0.6)';
ctx.shadowBlur = 12;
- ctx.fillStyle = 'rgba(0, 255, 136, 0.9)';
+ ctx.fillStyle = 'rgba(255, 106, 0, 0.9)';
ctx.beginPath();
ctx.arc(px, py, 8, 0, Math.PI * 2);
ctx.fill();
@@ -572,7 +576,7 @@ function drawJoyMap() {
ctx.fill();
// Crosshair
- ctx.strokeStyle = 'rgba(0, 255, 136, 0.2)';
+ ctx.strokeStyle = 'rgba(255, 106, 0, 0.2)';
ctx.lineWidth = 0.5;
ctx.shadowBlur = 0;
ctx.beginPath();
@@ -864,7 +868,7 @@ function onTrain() {
}
function onRandomize() {
- iml.randomiseWeights();
+ iml.randomiseWeights(spreadLevel);
const outputs = iml.getOutputs();
routeOutputs(outputs);
updateHeatmap(outputs);
@@ -982,7 +986,7 @@ function drawLossPlot() {
const maxLoss = Math.max(...history.slice(-200)) || 1;
const points = history.slice(-200);
- ctx.strokeStyle = 'rgba(0, 255, 136, 0.6)';
+ ctx.strokeStyle = 'rgba(255, 106, 0, 0.6)';
ctx.lineWidth = 1.5;
ctx.beginPath();
diff --git a/playground/js/app.js b/playground/js/app.js
index 16c9516..3970176 100644
--- a/playground/js/app.js
+++ b/playground/js/app.js
@@ -17,7 +17,7 @@ const N_OUTPUTS = N_SYNTH_OUTPUTS; // MLP always produces full output; visual us
// Visual mode param display config
const VISUAL_PARAM_NAMES = ['Flow', 'Scale', 'Speed', 'Hue', 'Spread', 'Size', 'Trail', 'Turb', 'Attract', 'Radius', 'DispRate', 'DispAmt', 'Lifetime', 'Respawn', 'Advection', 'Inertia', 'Drag', 'Repulse', 'RepCnt', 'RepRate'];
-const VISUAL_PARAM_COLORS = ['#00ff88', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00', '#0088ff', '#ff3366', '#9bff5f', '#59d3ff', '#ff8f3f', '#a0b7ff', '#f4ff7a', '#ffa8db', '#7dffc8', '#ffd166', '#8ad4ff', '#ff5f5f', '#ffc15f', '#ff8a3d'];
+const VISUAL_PARAM_COLORS = ['#ff6a00', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00', '#0088ff', '#ff3366', '#9bff5f', '#59d3ff', '#ff8f3f', '#a0b7ff', '#f4ff7a', '#ffa8db', '#7dffc8', '#ffd166', '#8ad4ff', '#ff5f5f', '#ffc15f', '#ff8a3d'];
// --- State ---
let iml;
@@ -45,9 +45,13 @@ let arpeggiator = null;
// Devmode: tame level (0 = no mitigation, 1 = strongest)
// Set via URL ?tame=0.7 or window.setTameLevel(0.7)
-let tameLevel = parseFloat(new URLSearchParams(location.search).get('tame') ?? '0.7');
+const _urlParams = new URLSearchParams(location.search);
+let tameLevel = parseFloat(_urlParams.get('tame') ?? '0.7');
if (isNaN(tameLevel)) tameLevel = 0.7;
tameLevel = Math.max(0, Math.min(1, tameLevel));
+let spreadLevel = parseFloat(_urlParams.get('spread') ?? '0');
+if (isNaN(spreadLevel)) spreadLevel = 0.6;
+spreadLevel = Math.max(0, Math.min(1, spreadLevel));
window.setTameLevel = (v) => { tameLevel = Math.max(0, Math.min(1, v)); console.log(`[NISPS] tame=${tameLevel}`); };
window.getTameLevel = () => tameLevel;
@@ -340,7 +344,7 @@ function onTrain() {
}
function onRandomize() {
- iml.randomiseWeights();
+ iml.randomiseWeights(spreadLevel);
const outputs = iml.getOutputs();
routeOutputs(outputs);
paramDisplay.update(outputs);
diff --git a/playground/js/b-app.js b/playground/js/b-app.js
index 665ada2..ee5ee8e 100644
--- a/playground/js/b-app.js
+++ b/playground/js/b-app.js
@@ -20,7 +20,7 @@ const VISUAL_PARAM_NAMES = [
'Advection', 'Inertia', 'Drag', 'Repulse', 'RepCnt', 'RepRate'
];
const VISUAL_PARAM_COLORS = [
- '#00ff88', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00',
+ '#ff6a00', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00',
'#0088ff', '#ff3366', '#9bff5f', '#59d3ff', '#ff8f3f', '#a0b7ff',
'#f4ff7a', '#ffa8db', '#7dffc8', '#ffd166', '#8ad4ff', '#ff5f5f',
'#ffc15f', '#ff8a3d'
@@ -91,6 +91,7 @@ function heatmapColor(t) {
// --- Tame URL param ---
const _urlParams = new URLSearchParams(window.location.search);
const tameLevel = parseFloat(_urlParams.get('tame') ?? '0');
+const spreadLevel = Math.max(0, Math.min(1, parseFloat(_urlParams.get('spread') ?? '0') || 0));
// --- State ---
let iml;
@@ -402,7 +403,7 @@ class MappingView {
// Current joystick position as crosshair
const px = joyX * w;
const py = joyY * h;
- const accent = outputMode === 'synth' ? '#ff6b35' : '#00ff88';
+ const accent = outputMode === 'synth' ? '#ff8c00' : '#ff6a00';
ctx.strokeStyle = accent;
ctx.lineWidth = 1;
@@ -498,7 +499,7 @@ function drawLossPlot(canvas, history) {
for (const v of history) if (v > maxLoss) maxLoss = v;
if (maxLoss === 0) maxLoss = 1;
- const accent = outputMode === 'synth' ? '#ff6b35' : '#00ff88';
+ const accent = outputMode === 'synth' ? '#ff8c00' : '#ff6a00';
// Draw line
ctx.strokeStyle = accent;
@@ -734,7 +735,7 @@ function onTrain() {
}
function onRandomize() {
- iml.randomiseWeights();
+ iml.randomiseWeights(spreadLevel);
const outputs = iml.getOutputs();
routeOutputs(outputs);
updateAllParamBars(outputs);
diff --git a/playground/js/c-app.js b/playground/js/c-app.js
index f0c2c15..6039cde 100644
--- a/playground/js/c-app.js
+++ b/playground/js/c-app.js
@@ -24,7 +24,7 @@ const VISUAL_PARAM_NAMES = [
'Advection', 'Inertia', 'Drag', 'Repulse', 'RepCnt', 'RepRate',
];
const VISUAL_PARAM_COLORS = [
- '#00ff88', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00',
+ '#ff6a00', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00',
'#0088ff', '#ff3366', '#9bff5f', '#59d3ff', '#ff8f3f', '#a0b7ff',
'#f4ff7a', '#ffa8db', '#7dffc8', '#ffd166', '#8ad4ff', '#ff5f5f',
'#ffc15f', '#ff8a3d',
@@ -49,6 +49,7 @@ const PRESETS = [
// --- Tame URL param ---
const _urlParams = new URLSearchParams(window.location.search);
const tameLevel = parseFloat(_urlParams.get('tame') ?? '0');
+const spreadLevel = Math.max(0, Math.min(1, parseFloat(_urlParams.get('spread') ?? '0') || 0));
// ============================================================
// State
@@ -180,7 +181,7 @@ class SynthVisualizer {
// Section label
const sectionWidth = barWidth * section.count;
ctx.fillStyle = 'rgba(255, 255, 255, 0.25)';
- ctx.font = '9px -apple-system, sans-serif';
+ ctx.font = '9px 'JetBrains Mono', monospace';
ctx.textAlign = 'center';
ctx.fillText(section.name, x + sectionWidth / 2, h - 6);
@@ -410,7 +411,7 @@ function buildHeatmap() {
cell.dataset.index = i;
const bar = document.createElement('div');
bar.className = 'heatmap-bar';
- bar.style.backgroundColor = currentParamColors[i] || '#00ff88';
+ bar.style.backgroundColor = currentParamColors[i] || '#ff6a00';
bar.style.width = '50%';
cell.appendChild(bar);
container.appendChild(cell);
@@ -628,7 +629,7 @@ function wireTeachActions() {
document.getElementById('btn-clear')?.addEventListener('click', () => {
iml.clearDataset();
- iml.randomiseWeights();
+ iml.randomiseWeights(spreadLevel);
selectedPreset = -1;
document.querySelectorAll('.preset-thumb').forEach(b => b.classList.remove('selected'));
updateTeachStatus();
@@ -637,7 +638,7 @@ function wireTeachActions() {
});
document.getElementById('btn-randomize')?.addEventListener('click', () => {
- iml.randomiseWeights();
+ iml.randomiseWeights(spreadLevel);
iml.setInput(0, joystickX);
iml.setInput(1, joystickY);
iml.process();
@@ -912,16 +913,16 @@ function drawJoystick() {
ctx.beginPath();
ctx.arc(knobX, knobY, knobRadius, 0, TWO_PI);
- ctx.fillStyle = 'rgba(0, 255, 136, 0.7)';
+ ctx.fillStyle = 'rgba(255, 106, 0, 0.7)';
ctx.fill();
- ctx.strokeStyle = 'rgba(0, 255, 136, 0.4)';
+ ctx.strokeStyle = 'rgba(255, 106, 0, 0.4)';
ctx.lineWidth = 2;
ctx.stroke();
// Glow
const glow = ctx.createRadialGradient(knobX, knobY, 0, knobX, knobY, knobRadius * 2.5);
- glow.addColorStop(0, 'rgba(0, 255, 136, 0.15)');
- glow.addColorStop(1, 'rgba(0, 255, 136, 0)');
+ glow.addColorStop(0, 'rgba(255, 106, 0, 0.15)');
+ glow.addColorStop(1, 'rgba(255, 106, 0, 0)');
ctx.fillStyle = glow;
ctx.beginPath();
ctx.arc(knobX, knobY, knobRadius * 2.5, 0, TWO_PI);
@@ -999,7 +1000,7 @@ function drawMinimap() {
if (!f || f.length < 2) continue;
const x = f[0] * w;
const y = f[1] * h;
- ctx.fillStyle = 'rgba(0, 255, 136, 0.8)';
+ ctx.fillStyle = 'rgba(255, 106, 0, 0.8)';
ctx.beginPath();
ctx.arc(x, y, 3, 0, TWO_PI);
ctx.fill();
diff --git a/playground/js/nisps/iml.js b/playground/js/nisps/iml.js
index d03509e..0e0acad 100644
--- a/playground/js/nisps/iml.js
+++ b/playground/js/nisps/iml.js
@@ -144,9 +144,9 @@ export class IML {
this.log('Dataset cleared.');
}
- randomiseWeights() {
+ randomiseWeights(spread = 0) {
this.storedWeights = this.mlp.getWeights();
- this.mlp.drawWeights();
+ this.mlp.drawWeights(spread);
this.weightsRandomised = true;
// Run inference to show effect
diff --git a/playground/js/nisps/mlp.js b/playground/js/nisps/mlp.js
index f5bc2b7..743d54a 100644
--- a/playground/js/nisps/mlp.js
+++ b/playground/js/nisps/mlp.js
@@ -204,13 +204,18 @@ export class MLP {
}
}
- // DrawWeights - randomize all weights uniformly in [-1, 1]
- drawWeights(scale = 1) {
- for (const layer of this.layers) {
- for (const node of layer.nodes) {
+ // DrawWeights - randomize all weights
+ // spread: 0 = uniform [-1,1] (polarised sigmoid outputs), 1 = Xavier-scaled (centered outputs)
+ drawWeights(spread = 0) {
+ for (let l = 0; l < this.layers.length; l++) {
+ const fanIn = this.layersNodes[l];
+ const xavierScale = 1 / Math.sqrt(fanIn);
+ const scale = 1 * (1 - spread) + xavierScale * spread;
+ for (const node of this.layers[l].nodes) {
for (let j = 0; j < node.weights.length; j++) {
node.weights[j] = (Math.random() * 2 - 1) * scale;
}
+ node.bias = 0;
}
}
}
diff --git a/playground/js/ui/controls.js b/playground/js/ui/controls.js
index fe698d2..b728765 100644
--- a/playground/js/ui/controls.js
+++ b/playground/js/ui/controls.js
@@ -160,14 +160,14 @@ export class Controls {
const plotW = width - leftPad - rightPad;
const plotH = height - topPad - bottomPad;
- ctx.strokeStyle = '#243523';
+ ctx.strokeStyle = '#352418';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(leftPad, topPad + plotH / 2);
ctx.lineTo(width - rightPad, topPad + plotH / 2);
ctx.stroke();
- ctx.strokeStyle = '#00ff88';
+ ctx.strokeStyle = '#ff6a00';
ctx.lineWidth = 1.6;
ctx.beginPath();
for (let i = 0; i < points.length; i++) {
diff --git a/playground/js/ui/joystick.js b/playground/js/ui/joystick.js
index 072e973..1129540 100644
--- a/playground/js/ui/joystick.js
+++ b/playground/js/ui/joystick.js
@@ -129,7 +129,7 @@ export class VirtualJoystick {
// Background circle
ctx.beginPath();
ctx.arc(r, r, r - 4, 0, Math.PI * 2);
- ctx.strokeStyle = this.followMode ? '#00cc6a' : '#333';
+ ctx.strokeStyle = this.followMode ? '#cc5500' : '#333';
ctx.lineWidth = 1.5;
ctx.stroke();
@@ -159,21 +159,21 @@ export class VirtualJoystick {
// Glow
if (this.touching) {
const grad = ctx.createRadialGradient(tx, ty, 0, tx, ty, 24);
- grad.addColorStop(0, 'rgba(0, 255, 136, 0.3)');
- grad.addColorStop(1, 'rgba(0, 255, 136, 0)');
+ grad.addColorStop(0, 'rgba(255, 106, 0, 0.3)');
+ grad.addColorStop(1, 'rgba(255, 106, 0, 0)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(tx, ty, 24, 0, Math.PI * 2);
ctx.fill();
}
- ctx.fillStyle = this.touching ? '#00ff88' : '#666';
+ ctx.fillStyle = this.touching ? '#ff6a00' : '#666';
ctx.beginPath();
ctx.arc(tx, ty, 14, 0, Math.PI * 2);
ctx.fill();
// Position text
- ctx.fillStyle = this.followMode ? '#00cc6a' : '#555';
+ ctx.fillStyle = this.followMode ? '#cc5500' : '#555';
ctx.font = '10px monospace';
ctx.textAlign = 'left';
ctx.fillText(this.followMode ? 'FOLLOW' : 'HOLD', 8, s - 6);
diff --git a/playground/js/ui/param-display.js b/playground/js/ui/param-display.js
index cb94542..bb79123 100644
--- a/playground/js/ui/param-display.js
+++ b/playground/js/ui/param-display.js
@@ -2,7 +2,7 @@
// Shows output parameters as horizontal bars, draggable in examples mode
const DEFAULT_PARAM_NAMES = ['Flow', 'Scale', 'Speed', 'Hue', 'Spread', 'Size', 'Trail', 'Turb', 'Attract', 'Radius', 'DispRate', 'DispAmt', 'Lifetime', 'Respawn', 'Advection', 'Inertia', 'Drag', 'Repulse', 'RepCnt', 'RepRate'];
-const DEFAULT_PARAM_COLORS = ['#00ff88', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00', '#0088ff', '#ff3366', '#9bff5f', '#59d3ff', '#ff8f3f', '#a0b7ff', '#f4ff7a', '#ffa8db', '#7dffc8', '#ffd166', '#8ad4ff', '#ff5f5f', '#ffc15f', '#ff8a3d'];
+const DEFAULT_PARAM_COLORS = ['#ff6a00', '#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 = 20) {