diff --git a/manifold/src/backends/index.ts b/manifold/src/backends/index.ts index a0bea84..d7e30a4 100644 --- a/manifold/src/backends/index.ts +++ b/manifold/src/backends/index.ts @@ -18,6 +18,7 @@ export { OscBridgeBackend } from './osc-backend'; export type { OscBackendConfig } from './osc-backend'; export { NispsOscClient } from './osc-client'; export { PassthroughBackend } from './passthrough-backend'; +export { ParticleBackend } from './particle-backend'; export { useBackendManager, } from './useBackendManager'; diff --git a/manifold/src/backends/manager.ts b/manifold/src/backends/manager.ts index 4afe0c2..38cdb24 100644 --- a/manifold/src/backends/manager.ts +++ b/manifold/src/backends/manager.ts @@ -21,6 +21,7 @@ import type { BackendId } from '../dock/output-state'; import { WebMidiBackend } from './midi-backend'; import { OscBridgeBackend } from './osc-backend'; import { PassthroughBackend } from './passthrough-backend'; +import { ParticleBackend } from './particle-backend'; /** The slice of EngineApi the manager depends on (keeps it decoupled/testable). */ export interface ManagerEngine { @@ -47,7 +48,7 @@ export class BackendManager { ['midi', backends?.midi ?? new WebMidiBackend()], ['osc', backends?.osc ?? new OscBridgeBackend()], ['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)')], ['vcv', backends?.vcv ?? new PassthroughBackend('vcv', 'VCV bridge')], ]); diff --git a/manifold/src/backends/particle-backend.ts b/manifold/src/backends/particle-backend.ts new file mode 100644 index 0000000..fee1ef0 --- /dev/null +++ b/manifold/src/backends/particle-backend.ts @@ -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)', + }; + } +} diff --git a/manifold/src/console/ConsoleApp.tsx b/manifold/src/console/ConsoleApp.tsx index f1b9fd5..d910694 100644 --- a/manifold/src/console/ConsoleApp.tsx +++ b/manifold/src/console/ConsoleApp.tsx @@ -27,10 +27,10 @@ import { useEngine, useEngineVersion } from '../engine'; import { MF_MODES, modeEngineId, seededGradient, shapeValues } from './model'; import type { MFParam } from './model'; import { CompositeStage } from './CompositeStage'; +import { ParticleStage } from './ParticleStage'; import { SplitStage } from './SplitStage'; import { OutputStage } from './OutputStage'; import { InputMini } from './InputMini'; -import { ParticleStage } from './ParticleStage'; import { Manifold } from './Manifold'; import { ReadoutStrip } from './ReadoutStrip'; import { VerdictCluster } from './VerdictCluster'; diff --git a/manifold/src/console/flow-field.ts b/manifold/src/console/flow-field.ts index 2ad27e2..4c2f51a 100644 --- a/manifold/src/console/flow-field.ts +++ b/manifold/src/console/flow-field.ts @@ -1,10 +1,23 @@ /** * 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 - * the visual ranges. The simulation advances on its own clock, so particles keep - * flowing between inferences — only the *field* changes when the MLP outputs do. + * FAITHFULNESS (verified 2026-06-28 against the original): + * • 400 particles, identical permutation-table value noise (`PERM`/`noise2D`). + * • 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