// src/presets/builtIn.ts
import type { InteractionMatrix, Preset, PresetParams, RGB, SpeciesParams } from '~/engine/types';
import { CURATED_PRESETS } from './curated';
const hex = (h: string): RGB => {
const n = Number.parseInt(h.replace('#', ''), 16);
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
};
/** Builds a SpeciesParams with M1-tuned defaults; override per preset. */
export function sp(color: string, share: number, over: Partial<SpeciesParams> = {}): SpeciesParams {
return {
color: hex(color),
populationShare: share,
sensorAngle: 0.5,
sensorDistance: 24,
turnSpeed: 0.32,
moveSpeed: 1.0,
depositAmount: 9,
...over,
};
}
/** Symmetric interaction matrix: `self` on the diagonal, `cross` elsewhere. */
export function symMatrix(self: number, cross: number): InteractionMatrix {
return [
[self, cross, cross],
[cross, self, cross],
[cross, cross, self],
];
}
type PresetOverrides = Partial<Omit<PresetParams, 'species' | 'interactionMatrix'>> & {
species: readonly [SpeciesParams, SpeciesParams, SpeciesParams];
// Legacy fields kept for source-readability: the matrix + the two strengths
// are folded into a single matrix at construction time. See the fold logic
// below — the runtime PresetParams no longer carries the strengths.
interactionMatrix?: InteractionMatrix;
attractionStrength?: number;
repulsionStrength?: number;
};
/** Builds PresetParams with M1-classical defaults; override per preset.
* Pre-1.2 attractionStrength/repulsionStrength are folded into the matrix:
* matrix[r][c] *= (r === c ? attractionStrength : repulsionStrength)
* This matches the old shader behaviour exactly (own × attrK + cross × repulK)
* while consolidating to a single per-species-pair scalar. */
function mk(o: PresetOverrides): PresetParams {
const rawMatrix = o.interactionMatrix ?? symMatrix(1.0, -0.3);
const attrK = o.attractionStrength ?? 1.0;
const repulK = o.repulsionStrength ?? -0.2;
const matrix = rawMatrix.map((row, r) =>
row.map((v, c) => v * (r === c ? attrK : repulK)),
) as unknown as InteractionMatrix;
const {
interactionMatrix: _im,
attractionStrength: _a,
repulsionStrength: _r,
species,
...overrides
} = o;
return {
agentCount: 9000,
spawnPattern: 'random',
decayRate: 0.97,
diffusionFreq: 2,
fadeStrength: 0.12,
trailSaturation: 255,
chaosInterval: 0,
chaosStrength: 0,
rhythmAmplitude: 0.05,
simSpeed: 1,
blendMode: 'additive',
backgroundColor: hex('#05030a'),
exposure: 3.0,
headIntensity: 0,
kaleidoscope: 0,
feedbackAmount: 0.2,
bloomThreshold: 0.25,
bloomIntensity: 0.9,
vignette: 0.3,
saturation: 1.1,
contrast: 1.05,
audioReactive: { enabled: false, gain: 1, autoGain: false, routes: [] },
...overrides,
species,
interactionMatrix: matrix,
};
}
/** Attaches `thumbnail: /preset-thumbs/<id>.png` to every built-in so the
* baked images surface in the library without each literal having to
* repeat itself. Re-run `npm run bake:thumbnails` after edits to refresh
* the PNGs that ship in public/. */
function withThumb<T extends Preset>(p: T): T {
return { ...p, thumbnail: `preset-thumbs/${p.id}.png` };
}
// The library is exactly the 7 curated factory presets (curated.ts). The hand-built
// classics were retired 2026-06-21 (Johannes' call) — git history holds them.
export const BUILT_IN_PRESETS: readonly Preset[] = CURATED_PRESETS.map(withThumb);
// biome-ignore lint/style/noNonNullAssertion: literal index 0 is safe on a non-empty const array
export const DEFAULT_PRESET = BUILT_IN_PRESETS[0]!; // Aurora
export const SIGNAL_CSS_VAR: Record<string, string> = {
pearl: 'var(--signal-pearl)',
crimson: 'var(--signal-crimson)',
phosphor: 'var(--signal-phosphor)',
ember: 'var(--signal-ember)',
spectre: 'var(--signal-spectre)',
circuit: 'var(--signal-circuit)',
ghost: 'var(--signal-ghost)',
biolink: 'var(--signal-biolink)',
'neural-bleed': 'var(--signal-neural-bleed)',
rust: 'var(--signal-rust)',
toxic: 'var(--signal-toxic)',
voidwitch: 'var(--signal-voidwitch)',
};