merge(particle): integrate flow-field as default Particle Mode + ParticleBackend
# Conflicts: # manifold/src/console/flow-field.ts
This commit is contained in:
commit
cba7de6a4b
5 changed files with 56 additions and 6 deletions
|
|
@ -18,6 +18,7 @@ export { OscBridgeBackend } from './osc-backend';
|
||||||
export type { OscBackendConfig } from './osc-backend';
|
export type { OscBackendConfig } from './osc-backend';
|
||||||
export { NispsOscClient } from './osc-client';
|
export { NispsOscClient } from './osc-client';
|
||||||
export { PassthroughBackend } from './passthrough-backend';
|
export { PassthroughBackend } from './passthrough-backend';
|
||||||
|
export { ParticleBackend } from './particle-backend';
|
||||||
export {
|
export {
|
||||||
useBackendManager,
|
useBackendManager,
|
||||||
} from './useBackendManager';
|
} from './useBackendManager';
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import type { BackendId } from '../dock/output-state';
|
||||||
import { WebMidiBackend } from './midi-backend';
|
import { WebMidiBackend } from './midi-backend';
|
||||||
import { OscBridgeBackend } from './osc-backend';
|
import { OscBridgeBackend } from './osc-backend';
|
||||||
import { PassthroughBackend } from './passthrough-backend';
|
import { PassthroughBackend } from './passthrough-backend';
|
||||||
|
import { ParticleBackend } from './particle-backend';
|
||||||
|
|
||||||
/** The slice of EngineApi the manager depends on (keeps it decoupled/testable). */
|
/** The slice of EngineApi the manager depends on (keeps it decoupled/testable). */
|
||||||
export interface ManagerEngine {
|
export interface ManagerEngine {
|
||||||
|
|
@ -47,7 +48,7 @@ export class BackendManager {
|
||||||
['midi', backends?.midi ?? new WebMidiBackend()],
|
['midi', backends?.midi ?? new WebMidiBackend()],
|
||||||
['osc', backends?.osc ?? new OscBridgeBackend()],
|
['osc', backends?.osc ?? new OscBridgeBackend()],
|
||||||
['synth', backends?.synth ?? new PassthroughBackend('synth', 'Built-in Synth — audio plays in the engine')],
|
['synth', backends?.synth ?? new PassthroughBackend('synth', 'Built-in Synth — audio plays in the engine')],
|
||||||
['particles', backends?.particles ?? new PassthroughBackend('particles', 'Particle visualiser')],
|
['particles', backends?.particles ?? new ParticleBackend()],
|
||||||
['cvgate', backends?.cvgate ?? new PassthroughBackend('cvgate', 'CV / gate (via VCV bridge)')],
|
['cvgate', backends?.cvgate ?? new PassthroughBackend('cvgate', 'CV / gate (via VCV bridge)')],
|
||||||
['vcv', backends?.vcv ?? new PassthroughBackend('vcv', 'VCV bridge')],
|
['vcv', backends?.vcv ?? new PassthroughBackend('vcv', 'VCV bridge')],
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
35
manifold/src/backends/particle-backend.ts
Normal file
35
manifold/src/backends/particle-backend.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
/**
|
||||||
|
* ParticleBackend — the output backend for the Particle System mode (the DEFAULT
|
||||||
|
* mode).
|
||||||
|
*
|
||||||
|
* Transport is a NO-OP: the flow-field visualiser (console/flow-field.ts, driven
|
||||||
|
* by ParticleStage) is a SEPARATE consumer of the engine spine — it reads
|
||||||
|
* `engine.getOutputs()` directly in its own requestAnimationFrame loop and maps
|
||||||
|
* the first 20 outputs onto the visual field. So there is nothing for the
|
||||||
|
* BackendManager to "send" here.
|
||||||
|
*
|
||||||
|
* What this backend DOES contribute:
|
||||||
|
* - It is a non-synth backend, so selecting it makes the BackendManager gate
|
||||||
|
* audio (`engine.audio.setMuted(true)`) — the synth is silenced while the
|
||||||
|
* particles are on screen, exactly like the MIDI / OSC modes.
|
||||||
|
* - It reports a sensible "ready" status for the Outputs panel.
|
||||||
|
*
|
||||||
|
* It extends PassthroughBackend (the shared no-op sink) purely to keep the
|
||||||
|
* empty-`send` / nothing-to-start behaviour in one place; the audio gate lives
|
||||||
|
* in the manager (`setActive`), not here.
|
||||||
|
*/
|
||||||
|
import { PassthroughBackend } from './passthrough-backend';
|
||||||
|
import type { BackendStatus } from './backend';
|
||||||
|
|
||||||
|
export class ParticleBackend extends PassthroughBackend {
|
||||||
|
constructor() {
|
||||||
|
super('particles', 'Particle visualiser — outputs drive the flow field (no audio)');
|
||||||
|
}
|
||||||
|
|
||||||
|
override status(): BackendStatus {
|
||||||
|
return {
|
||||||
|
state: 'ready',
|
||||||
|
message: 'Particle visualiser — outputs drive the flow field (no audio)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,10 +27,10 @@ import { useEngine, useEngineVersion } from '../engine';
|
||||||
import { MF_MODES, modeEngineId, seededGradient, shapeValues } from './model';
|
import { MF_MODES, modeEngineId, seededGradient, shapeValues } from './model';
|
||||||
import type { MFParam } from './model';
|
import type { MFParam } from './model';
|
||||||
import { CompositeStage } from './CompositeStage';
|
import { CompositeStage } from './CompositeStage';
|
||||||
|
import { ParticleStage } from './ParticleStage';
|
||||||
import { SplitStage } from './SplitStage';
|
import { SplitStage } from './SplitStage';
|
||||||
import { OutputStage } from './OutputStage';
|
import { OutputStage } from './OutputStage';
|
||||||
import { InputMini } from './InputMini';
|
import { InputMini } from './InputMini';
|
||||||
import { ParticleStage } from './ParticleStage';
|
|
||||||
import { Manifold } from './Manifold';
|
import { Manifold } from './Manifold';
|
||||||
import { ReadoutStrip } from './ReadoutStrip';
|
import { ReadoutStrip } from './ReadoutStrip';
|
||||||
import { VerdictCluster } from './VerdictCluster';
|
import { VerdictCluster } from './VerdictCluster';
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,23 @@
|
||||||
/**
|
/**
|
||||||
* flow-field.ts — Canvas2D flow-field particle system, a faithful TypeScript
|
* flow-field.ts — Canvas2D flow-field particle system, a faithful TypeScript
|
||||||
* port of the a-immersive playground visualiser (`js/ui/visualizer.js`).
|
* port of the a-immersive playground visualiser
|
||||||
|
* (`/home/w1n5t0n/deployments/meml-aimmersive/js/ui/visualizer.js`).
|
||||||
*
|
*
|
||||||
* Driven by the first 20 model outputs (each ∈ [0,1]); `setParams` maps them to
|
* FAITHFULNESS (verified 2026-06-28 against the original):
|
||||||
* the visual ranges. The simulation advances on its own clock, so particles keep
|
* • 400 particles, identical permutation-table value noise (`PERM`/`noise2D`).
|
||||||
* flowing between inferences — only the *field* changes when the MLP outputs do.
|
* • The first 20 model outputs (each ∈ [0,1]) map to the visual ranges exactly
|
||||||
|
* as the original `setParams` (p0..p19 — see the per-line comments below).
|
||||||
|
* • The advection/attractor/dispersion/repulsor integration in `draw()` is a
|
||||||
|
* line-for-line port of the original.
|
||||||
|
* • Deliberate improvements over the original (NOT drift):
|
||||||
|
* - `resize()` resets the transform before `scale(dpr)` so repeated resizes
|
||||||
|
* don't compound the device-pixel-ratio scale (the original double-scaled
|
||||||
|
* on every resize); it also repaints the background so a resize doesn't
|
||||||
|
* leave stale trails.
|
||||||
|
* - `draw()` early-returns while the canvas has zero size (pre-layout).
|
||||||
|
*
|
||||||
|
* The simulation advances on its own clock, so particles keep flowing between
|
||||||
|
* inferences — only the *field* changes when the MLP outputs do.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Simple value noise (no dependencies) — identical permutation scheme to the
|
// Simple value noise (no dependencies) — identical permutation scheme to the
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue