export type RGB = readonly [number, number, number];
export interface PresetParams {
agentCount: number;
spawnPattern: SpawnPattern;
species: readonly [SpeciesParams, SpeciesParams, SpeciesParams];
interactionMatrix: InteractionMatrix;
// Pheromone medium (global) — M1 classical model
decayRate: number; // 0.85..0.999 trail persistence per diffuse pass
diffusionFreq: number; // 1..10 — diffuse runs every Nth frame
fadeStrength: number; // 0..0.4 — extra decay above the fade threshold
trailSaturation: number; // 8..300 — trail value cap
// Motion perturbation (global)
chaosInterval: number; // 0..500 — frames between chaos kicks (0 = off)
chaosStrength: number; // 0..1 — chaos kick magnitude
rhythmAmplitude: number; // 0..0.3 — per-agent sine-wander amplitude
// Pacing — how many sim steps to run per rendered frame (1..8). Multiplies
// physics speed without changing rendering / audio cadence.
simSpeed: number;
// Render
backgroundColor: RGB;
blendMode: BlendMode; // how agents compose over bg
exposure: number; // tone-map exposure
// Long-exposure agent-head highlight: how bright the current-frame
// agent positions appear on top of the persistent pheromone trail.
// 0 = off (trail only). 1 ≈ matches trail brightness. 2..3 = comet head.
headIntensity: number;
// Kaleidoscope mirror axes (0-6). 0 = off; otherwise the composite pass
// folds the image into N-fold rotational symmetry about the centre.
kaleidoscope: number;
// Post-processing effects
feedbackAmount: number; // 0..0.97 — echo/trail persistence
bloomThreshold: number; // 0..2 — HDR brightness where bloom starts
bloomIntensity: number; // 0..3 — glow strength
vignette: number; // 0..1 — edge darkening
saturation: number; // 0..2 — colour saturation (1 = neutral)
contrast: number; // 0..2 — contrast (1 = neutral)
audioReactive: AudioReactive;
}
export const AUDIO_SOURCES = ['bass', 'mid', 'high'] as const;
export type AudioBand = (typeof AUDIO_SOURCES)[number];
// Modulation targets that the engine knows how to multiply by an audio factor.
// Split into agent-side (applied per agent in agentStep/deposit shaders) and
// post-side (applied in the composite shader). The shader uniforms have
// dedicated factor slots for each — adding a target here requires wiring
// the matching uniform in engine/params.ts and the matching shader code.
export const MOD_TARGETS_AGENT = [
'moveSpeed',
'turnSpeed',
'depositAmount',
'sensorDistance',
'rhythmAmplitude',
'chaosStrength',
] as const;
export const MOD_TARGETS_POST = ['exposure', 'bloomIntensity', 'saturation', 'contrast'] as const;
export const MOD_TARGETS = [...MOD_TARGETS_AGENT, ...MOD_TARGETS_POST] as const;
export type ModTarget = (typeof MOD_TARGETS)[number];
export interface AudioRoute {
source: AudioBand;
target: ModTarget;
depth: number; // -2..+2 — multiplier on the source band's 0..1 level
}
export interface AudioReactive {
enabled: boolean;
/** Manual multiplier applied to every band before routing. 0.1..10. */
gain: number;
/** If true, AudioCapture continuously normalises levels to a target range. */
autoGain: boolean;
/** Free routing from audio bands to modulation targets. */
routes: readonly AudioRoute[];
}
export interface Preset {
id: string;
name: string;
author?: string;
builtIn: boolean;
tags: readonly string[];
tagSignal: SignalName;
params: PresetParams;
/** Optional URL to a pre-baked thumbnail. Built-ins ship with one in
* /preset-thumbs/<id>.png; custom presets rely on the runtime
* thumbnailStore (snapshot at frame ~240, cached in localStorage). */
thumbnail?: string;
}
export const SIGNAL_NAMES = [
'pearl',
'crimson',
'phosphor',
'ember',
'spectre',
'circuit',
'ghost',
'biolink',
'neural-bleed',
'rust',
'toxic',
'voidwitch',
] as const;
export type SignalName = (typeof SIGNAL_NAMES)[number];
export const SPAWN_PATTERNS = ['random', 'clusters', 'center', 'ring'] as const;
export type SpawnPattern = (typeof SPAWN_PATTERNS)[number];
// How agents are composited over the background:
// additive — agents emit light on top of bg. Works with dark backgrounds.
// Bright bg saturates everything to white (ACES tonemap clips).
// paint — agents are coloured ink, bg is paper. Tonemap agents alone,
// then alpha-blend over bg in display space. Works at any bg.
export const BLEND_MODES = ['additive', 'paint'] as const;
export type BlendMode = (typeof BLEND_MODES)[number];
export interface SpeciesParams {
color: RGB;
populationShare: number; // relative weight ≥ 0
sensorAngle: number; // radians
sensorDistance: number; // sim-grid pixels
turnSpeed: number; // radians per step
moveSpeed: number; // sim-grid pixels per frame
depositAmount: number; // trail units deposited per frame
}
// matrix[sensingSpecies][channel] — Wertebereich -2..+2
export type MatrixRow = readonly [number, number, number];
export type InteractionMatrix = readonly [MatrixRow, MatrixRow, MatrixRow];
export type PerfTier = 'desktop' | 'mid' | 'mobile' | 'minimum';
export interface PerfBudget {
agentCount: number;
fieldScale: number; // 1.0 = full canvas, 0.5 = half resolution
}
export interface AudioFrame {
bass: number; // 0..1 RMS
mid: number;
high: number;
}
export interface EngineHandle {
setPreset(preset: PresetParams): void;
respawn(): void;
setAudio(frame: AudioFrame | null): void;
resize(width: number, height: number): void;
tick(): void;
/** Returns a downscaled JPEG data URL of the current canvas, or '' on failure. */
snapshot(longEdge?: number): string;
dispose(): void;
readonly canvas: HTMLCanvasElement;
readonly tier: PerfTier;
}