playground: add attractor and dispersion controls
This commit is contained in:
parent
57aae34870
commit
0550088516
4 changed files with 179 additions and 25 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, 8]` MLP to **8 outputs** controlling a Canvas2D flow-field particle system
|
- **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
|
||||||
- **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 = 8;
|
const N_OUTPUTS = 12;
|
||||||
|
|
||||||
// --- State ---
|
// --- State ---
|
||||||
let iml;
|
let iml;
|
||||||
|
|
@ -20,6 +20,10 @@ let learningMode = 'examples'; // 'examples' | 'rl'
|
||||||
let noiseLevel = 0.05;
|
let noiseLevel = 0.05;
|
||||||
let rlExplorationDecay = 0.97;
|
let rlExplorationDecay = 0.97;
|
||||||
let animating = true;
|
let animating = true;
|
||||||
|
let gamepadIndex = -1;
|
||||||
|
let gamepadButtonsPrev = [];
|
||||||
|
let gamepadConnected = false;
|
||||||
|
let gamepadLastAxes = [0.5, 0.5];
|
||||||
|
|
||||||
// --- Init ---
|
// --- Init ---
|
||||||
function init() {
|
function init() {
|
||||||
|
|
@ -65,12 +69,18 @@ function init() {
|
||||||
helpOverlay.addEventListener('click', () => helpOverlay.classList.add('hidden'));
|
helpOverlay.addEventListener('click', () => helpOverlay.classList.add('hidden'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener('gamepadconnected', () => refreshDashboard());
|
||||||
|
window.addEventListener('gamepaddisconnected', () => refreshDashboard());
|
||||||
|
|
||||||
// Run initial inference to populate outputs
|
// Run initial inference to populate outputs
|
||||||
iml.setInput(0, 0.5);
|
iml.setInput(0, 0.5);
|
||||||
iml.setInput(1, 0.5);
|
iml.setInput(1, 0.5);
|
||||||
iml.process();
|
iml.process();
|
||||||
visualizer.setParams(iml.getOutputs());
|
visualizer.setParams(iml.getOutputs());
|
||||||
paramDisplay.update(iml.getOutputs());
|
paramDisplay.update(iml.getOutputs());
|
||||||
|
controls.updateStatus(iml.exampleCount, iml.lastLoss, noiseLevel);
|
||||||
|
controls.updateLossPlot(iml.lossHistory);
|
||||||
|
refreshDashboard();
|
||||||
|
|
||||||
// Start animation
|
// Start animation
|
||||||
animate();
|
animate();
|
||||||
|
|
@ -82,6 +92,7 @@ function init() {
|
||||||
// --- Animation loop ---
|
// --- Animation loop ---
|
||||||
function animate() {
|
function animate() {
|
||||||
if (!animating) return;
|
if (!animating) return;
|
||||||
|
pollGamepad();
|
||||||
visualizer.draw();
|
visualizer.draw();
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
}
|
}
|
||||||
|
|
@ -99,6 +110,8 @@ function onJoystickMove(x, y) {
|
||||||
if (learningMode !== 'examples' || paramDisplay.activeBar < 0) {
|
if (learningMode !== 'examples' || paramDisplay.activeBar < 0) {
|
||||||
paramDisplay.update(outputs);
|
paramDisplay.update(outputs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshDashboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Examples mode callbacks ---
|
// --- Examples mode callbacks ---
|
||||||
|
|
@ -108,17 +121,20 @@ function onAddExample() {
|
||||||
const outputs = [...paramDisplay.values];
|
const outputs = [...paramDisplay.values];
|
||||||
iml.addExample(inputs, outputs);
|
iml.addExample(inputs, outputs);
|
||||||
controls.updateStatus(iml.exampleCount, iml.lastLoss, noiseLevel);
|
controls.updateStatus(iml.exampleCount, iml.lastLoss, noiseLevel);
|
||||||
|
refreshDashboard();
|
||||||
flash('btn-add');
|
flash('btn-add');
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTrain() {
|
function onTrain() {
|
||||||
const loss = iml.train();
|
const loss = trainModel();
|
||||||
if (loss !== null) {
|
if (loss !== null) {
|
||||||
// After training, switch back to inference and update display
|
// After training, switch back to inference and update display
|
||||||
const outputs = iml.getOutputs();
|
const outputs = iml.getOutputs();
|
||||||
visualizer.setParams(outputs);
|
visualizer.setParams(outputs);
|
||||||
paramDisplay.update(outputs);
|
paramDisplay.update(outputs);
|
||||||
controls.updateStatus(iml.exampleCount, loss, noiseLevel);
|
controls.updateStatus(iml.exampleCount, loss, noiseLevel);
|
||||||
|
controls.updateLossPlot(iml.lossHistory);
|
||||||
|
refreshDashboard();
|
||||||
flash('btn-train');
|
flash('btn-train');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -130,12 +146,18 @@ function onRandomize() {
|
||||||
paramDisplay.update(outputs);
|
paramDisplay.update(outputs);
|
||||||
noiseLevel = 0.05; // reset noise
|
noiseLevel = 0.05; // reset noise
|
||||||
controls.updateStatus(iml.exampleCount, iml.lastLoss, noiseLevel);
|
controls.updateStatus(iml.exampleCount, iml.lastLoss, noiseLevel);
|
||||||
|
refreshDashboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClear() {
|
function onClear() {
|
||||||
iml.clearDataset();
|
iml.clearDataset();
|
||||||
|
iml.lossHistory = [];
|
||||||
|
iml.bestLoss = null;
|
||||||
|
iml.totalTrainingIterations = 0;
|
||||||
noiseLevel = 0.05;
|
noiseLevel = 0.05;
|
||||||
controls.updateStatus(0, null, noiseLevel);
|
controls.updateStatus(0, null, noiseLevel);
|
||||||
|
controls.updateLossPlot(iml.lossHistory);
|
||||||
|
refreshDashboard();
|
||||||
clearState();
|
clearState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,13 +169,15 @@ function onThumbsUp() {
|
||||||
iml.addExample(inputs, outputs);
|
iml.addExample(inputs, outputs);
|
||||||
|
|
||||||
// Retrain incrementally
|
// Retrain incrementally
|
||||||
iml.train();
|
trainModel();
|
||||||
|
|
||||||
// Decay noise - more positive examples = less exploration
|
// Decay noise - more positive examples = less exploration
|
||||||
noiseLevel *= rlExplorationDecay;
|
noiseLevel *= rlExplorationDecay;
|
||||||
noiseLevel = Math.max(noiseLevel, 0.005);
|
noiseLevel = Math.max(noiseLevel, 0.005);
|
||||||
|
|
||||||
controls.updateStatus(iml.exampleCount, iml.lastLoss, noiseLevel);
|
controls.updateStatus(iml.exampleCount, iml.lastLoss, noiseLevel);
|
||||||
|
controls.updateLossPlot(iml.lossHistory);
|
||||||
|
refreshDashboard();
|
||||||
flash('btn-thumbsup');
|
flash('btn-thumbsup');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,6 +192,7 @@ function onThumbsDown() {
|
||||||
visualizer.setParams(outputs);
|
visualizer.setParams(outputs);
|
||||||
paramDisplay.update(outputs);
|
paramDisplay.update(outputs);
|
||||||
controls.updateStatus(iml.exampleCount, iml.lastLoss, noiseLevel);
|
controls.updateStatus(iml.exampleCount, iml.lastLoss, noiseLevel);
|
||||||
|
refreshDashboard();
|
||||||
flash('btn-thumbsdown');
|
flash('btn-thumbsdown');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,6 +204,7 @@ function onModeChange(mode) {
|
||||||
paramDisplay.setDraggable(false);
|
paramDisplay.setDraggable(false);
|
||||||
}
|
}
|
||||||
controls.updateStatus(iml.exampleCount, iml.lastLoss, noiseLevel);
|
controls.updateStatus(iml.exampleCount, iml.lastLoss, noiseLevel);
|
||||||
|
refreshDashboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Presets ---
|
// --- Presets ---
|
||||||
|
|
@ -187,28 +213,30 @@ 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]);
|
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]);
|
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]);
|
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]);
|
||||||
} 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]);
|
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]);
|
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]);
|
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]);
|
||||||
} 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]);
|
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]);
|
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]);
|
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]);
|
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]);
|
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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const loss = iml.train();
|
const loss = trainModel();
|
||||||
const outputs = iml.getOutputs();
|
const outputs = iml.getOutputs();
|
||||||
visualizer.setParams(outputs);
|
visualizer.setParams(outputs);
|
||||||
paramDisplay.update(outputs);
|
paramDisplay.update(outputs);
|
||||||
controls.updateStatus(iml.exampleCount, loss, noiseLevel);
|
controls.updateStatus(iml.exampleCount, loss, noiseLevel);
|
||||||
|
controls.updateLossPlot(iml.lossHistory);
|
||||||
|
refreshDashboard();
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Persistence ---
|
// --- Persistence ---
|
||||||
|
|
@ -229,11 +257,13 @@ function loadState() {
|
||||||
for (let i = 0; i < data.features.length; i++) {
|
for (let i = 0; i < data.features.length; i++) {
|
||||||
iml.addExample(data.features[i], data.labels[i]);
|
iml.addExample(data.features[i], data.labels[i]);
|
||||||
}
|
}
|
||||||
iml.train();
|
trainModel();
|
||||||
const outputs = iml.getOutputs();
|
const outputs = iml.getOutputs();
|
||||||
visualizer.setParams(outputs);
|
visualizer.setParams(outputs);
|
||||||
paramDisplay.update(outputs);
|
paramDisplay.update(outputs);
|
||||||
controls.updateStatus(iml.exampleCount, iml.lastLoss, noiseLevel);
|
controls.updateStatus(iml.exampleCount, iml.lastLoss, noiseLevel);
|
||||||
|
controls.updateLossPlot(iml.lossHistory);
|
||||||
|
refreshDashboard();
|
||||||
}
|
}
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
@ -253,6 +283,100 @@ function flash(id) {
|
||||||
setTimeout(() => el.classList.remove('flash'), 200);
|
setTimeout(() => el.classList.remove('flash'), 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function trainModel() {
|
||||||
|
let lastPlotUpdate = 0;
|
||||||
|
const loss = iml.train({
|
||||||
|
onIteration: (iter, iterLoss) => {
|
||||||
|
if (iter - lastPlotUpdate < 8) return;
|
||||||
|
lastPlotUpdate = iter;
|
||||||
|
controls.updateLossPlot([...iml.lossHistory, iterLoss]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return loss;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshDashboard() {
|
||||||
|
const outputs = iml.getOutputs();
|
||||||
|
let mean = 0;
|
||||||
|
for (let i = 0; i < outputs.length; i++) mean += outputs[i];
|
||||||
|
mean /= Math.max(outputs.length, 1);
|
||||||
|
|
||||||
|
let variance = 0;
|
||||||
|
for (let i = 0; i < outputs.length; i++) {
|
||||||
|
const diff = outputs[i] - mean;
|
||||||
|
variance += diff * diff;
|
||||||
|
}
|
||||||
|
variance /= Math.max(outputs.length, 1);
|
||||||
|
|
||||||
|
controls.updateMetrics({
|
||||||
|
mode: learningMode,
|
||||||
|
joystickX: joystick.x,
|
||||||
|
joystickY: joystick.y,
|
||||||
|
outputMean: mean,
|
||||||
|
outputSpread: Math.sqrt(variance),
|
||||||
|
bestLoss: iml.bestLoss,
|
||||||
|
totalTrainingIterations: iml.totalTrainingIterations,
|
||||||
|
gamepadConnected,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pollGamepad() {
|
||||||
|
if (!navigator.getGamepads) return;
|
||||||
|
const gamepads = navigator.getGamepads();
|
||||||
|
let gp = null;
|
||||||
|
|
||||||
|
if (gamepadIndex >= 0 && gamepads[gamepadIndex] && gamepads[gamepadIndex].connected) {
|
||||||
|
gp = gamepads[gamepadIndex];
|
||||||
|
} else {
|
||||||
|
gamepadIndex = -1;
|
||||||
|
for (let i = 0; i < gamepads.length; i++) {
|
||||||
|
if (gamepads[i] && gamepads[i].connected) {
|
||||||
|
gp = gamepads[i];
|
||||||
|
gamepadIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wasConnected = gamepadConnected;
|
||||||
|
gamepadConnected = !!gp;
|
||||||
|
if (wasConnected !== gamepadConnected) refreshDashboard();
|
||||||
|
if (!gp) {
|
||||||
|
gamepadButtonsPrev = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deadzone = 0.08;
|
||||||
|
const rawX = gp.axes[0] || 0;
|
||||||
|
const rawY = gp.axes[1] || 0;
|
||||||
|
const axisX = Math.abs(rawX) < deadzone ? 0 : rawX;
|
||||||
|
const axisY = Math.abs(rawY) < deadzone ? 0 : rawY;
|
||||||
|
const mappedX = (axisX + 1) * 0.5;
|
||||||
|
const mappedY = (axisY + 1) * 0.5;
|
||||||
|
|
||||||
|
const moved = Math.abs(mappedX - gamepadLastAxes[0]) > 0.002 || Math.abs(mappedY - gamepadLastAxes[1]) > 0.002;
|
||||||
|
if (moved && paramDisplay.activeBar < 0) {
|
||||||
|
gamepadLastAxes = [mappedX, mappedY];
|
||||||
|
joystick.setPosition(mappedX, mappedY, { emit: true, touching: true });
|
||||||
|
} else if (!moved && joystick.touching) {
|
||||||
|
joystick.setPosition(joystick.x, joystick.y, { emit: false, touching: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard gamepad mapping: LB=4, RB=5
|
||||||
|
const lbPressed = !!gp.buttons[4]?.pressed;
|
||||||
|
const rbPressed = !!gp.buttons[5]?.pressed;
|
||||||
|
const lbPrev = !!gamepadButtonsPrev[4];
|
||||||
|
const rbPrev = !!gamepadButtonsPrev[5];
|
||||||
|
|
||||||
|
if (learningMode === 'rl') {
|
||||||
|
if (rbPressed && !rbPrev) onThumbsUp();
|
||||||
|
if (lbPressed && !lbPrev) onThumbsDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
gamepadButtonsPrev[4] = lbPressed;
|
||||||
|
gamepadButtonsPrev[5] = rbPressed;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Start ---
|
// --- Start ---
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
init();
|
init();
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
// Parameter bar display
|
// Parameter bar display
|
||||||
// Shows 8 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'];
|
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'];
|
const PARAM_COLORS = ['#00ff88', '#00ccff', '#ff6600', '#ff00cc', '#ffcc00', '#88ff00', '#0088ff', '#ff3366', '#9bff5f', '#59d3ff', '#ff8f3f', '#a0b7ff'];
|
||||||
|
|
||||||
export class ParamDisplay {
|
export class ParamDisplay {
|
||||||
constructor(container, numParams = 8) {
|
constructor(container, numParams = 12) {
|
||||||
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 8 output parameters from the IML network
|
// Controlled by 12 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);
|
||||||
|
|
@ -63,6 +63,10 @@ export class FlowFieldVisualizer {
|
||||||
particleSize: 3, // p5: dot radius
|
particleSize: 3, // p5: dot radius
|
||||||
fadeRate: 0.05, // p6: trail length
|
fadeRate: 0.05, // p6: trail length
|
||||||
turbulence: 1, // p7: chaos
|
turbulence: 1, // p7: chaos
|
||||||
|
attractStrength: 0.8, // p8: pull toward screen center
|
||||||
|
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
|
||||||
};
|
};
|
||||||
|
|
||||||
this.resize();
|
this.resize();
|
||||||
|
|
@ -95,7 +99,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 < 8) return;
|
if (!outputs || outputs.length < 12) 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;
|
||||||
|
|
@ -104,6 +108,10 @@ export class FlowFieldVisualizer {
|
||||||
this.params.particleSize = 1 + outputs[5] * 5;
|
this.params.particleSize = 1 + outputs[5] * 5;
|
||||||
this.params.fadeRate = 0.01 + outputs[6] * 0.14;
|
this.params.fadeRate = 0.01 + outputs[6] * 0.14;
|
||||||
this.params.turbulence = outputs[7] * 2;
|
this.params.turbulence = outputs[7] * 2;
|
||||||
|
this.params.attractStrength = 0.1 + outputs[8] * 2.9;
|
||||||
|
this.params.attractRadius = 40 + outputs[9] * 420;
|
||||||
|
this.params.dispersionRate = 0.2 + outputs[10] * 8;
|
||||||
|
this.params.dispersionAmount = outputs[11] * 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
|
|
@ -124,8 +132,30 @@ export class FlowFieldVisualizer {
|
||||||
// Move particle
|
// Move particle
|
||||||
const vx = Math.cos(angle + curl) * params.speed;
|
const vx = Math.cos(angle + curl) * params.speed;
|
||||||
const vy = Math.sin(angle + curl) * params.speed;
|
const vy = Math.sin(angle + curl) * params.speed;
|
||||||
p.x += vx;
|
let nextX = p.x + vx;
|
||||||
p.y += vy;
|
let nextY = p.y + vy;
|
||||||
|
|
||||||
|
// Central attractor keeps trajectories from sticking to the outer edges.
|
||||||
|
const cx = width * 0.5;
|
||||||
|
const cy = height * 0.5;
|
||||||
|
const dx = cx - nextX;
|
||||||
|
const dy = cy - nextY;
|
||||||
|
const dist = Math.hypot(dx, dy) + 1e-6;
|
||||||
|
const nxCenter = dx / dist;
|
||||||
|
const nyCenter = dy / dist;
|
||||||
|
const normalizedDist = Math.min(dist / params.attractRadius, 2);
|
||||||
|
const falloff = 1 / (1 + normalizedDist * normalizedDist);
|
||||||
|
nextX += nxCenter * params.attractStrength * falloff;
|
||||||
|
nextY += nyCenter * params.attractStrength * falloff;
|
||||||
|
|
||||||
|
// Time-varying dispersion pushes particles outward near the center.
|
||||||
|
const dispersionPulse = 0.5 + 0.5 * Math.sin(this.time * params.dispersionRate + p.id * 0.07);
|
||||||
|
const dispersionForce = params.dispersionAmount * dispersionPulse * falloff;
|
||||||
|
nextX -= nxCenter * dispersionForce;
|
||||||
|
nextY -= nyCenter * dispersionForce;
|
||||||
|
|
||||||
|
p.x = nextX;
|
||||||
|
p.y = nextY;
|
||||||
|
|
||||||
// Wrap around edges
|
// Wrap around edges
|
||||||
if (p.x < 0) p.x += width;
|
if (p.x < 0) p.x += width;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue