Skip to the content.

Preset SDK — add a visual mode

Three ways in:

Everything below applies to the WGSL paths; an imported Shadertoy visual is a complete translated program and uses its own uniform contract instead of the u./P_ ABI.

The two laws

  1. Determinism — nothing wall-clock may touch a pixel. Seed randomness from u.time or a hash of position; never Math.random/Date.now.
  2. Deterministic render input — a preset is a pure function of (features, time, params). Purity makes export repeatable; live feature timing still follows the preview/export truth contract.

PresetDef

export const myMode: PresetDef = {
  id: "my-mode", // stable — projects reference it
  name: "My Mode",
  description: "One user-facing line.",
  styles: [
    // curated looks; first = defaults. The shipped modes carry 6–15 each
    { id: "default", name: "Default", values: {} },
    { id: "ember", name: "Ember", values: { hue: 20, glow: 0.8 } },
  ],
  params: [
    // main knobs (schema -> auto UI)
    {
      key: "hue",
      label: "Hue",
      min: 0,
      max: 360,
      step: 1,
      default: 200,
      hint: "What turning this visibly does",
    },
  ],
  advanced: [/* every internal constant worth touching */],
  wgsl: /* wgsl */ `
fn preset(uv: vec2f) -> vec4f {
  // your fragment shader — uv is 0..1
  return vec4f(0.0, 0.0, 0.0, 1.0);
}`,
};

Params become WGSL accessors P_<key>(). A param with min:0, max:1, step:1 renders as a toggle. styles values are partial overrides — machine-check yours against the schema like themes.test.ts does.

Groups and tiers

Give every param a group (shape, color, motion, reaction, glow, image, camera, backdrop, or one your preset declares); anything without one lands in More. The panel renders one collapsible section per group, and inside it the params entries sit above the group’s expert line while the advanced entries hide behind it.

The two arrays are the ABI. allParams packs params then advanced in declaration order, and a spec’s position is its shader accessor index — move one between the arrays and every GPU pixel hash shifts. To show an advanced spec above the expert line instead, set tier: "curated" on it in place; that changes nothing about packing. Every group must have at least one control above its expert line, or it renders as a bare header — curation.test.ts enforces that and abiOrder.test.ts catches a moved spec in seconds.

Audio uniforms (u.)

Field Meaning
time Track time, seconds. THE animation clock.
drive Smoothed envelope of the user’s Sync source. Use this so the Sync panel matters.
driveBeat Onset pulse of the Sync source: 1 on a hit, exponential decay.
bpm, beatPhase, barPhase Beat grid: tempo, 0..1 within the beat, 0..1 within a 4-beat bar. 0 when no grid.
bass, mid, treble, voice Band energies 0..1.
kick, snare, hat Per-drum onset envelopes.
rms, energy Instant / slow (~0.8 s) loudness.
beatIntensity Legacy low-end beat pulse.
dt Seconds of state time this invocation covers. Ordinary presets receive presentation delta. Presets that read feedback history receive 1/60 on fixed state ticks and 0 on presentation-only frames, so history mutation must be gated by positive dt. Seeks and pauses clamp safely.
width Stereo width.
spin, pulse, detail, specSmooth Motion masters — multiply your rotation / beat-scale / element count by these.
aspect, binCount, waveCount, smoothBins, bgMode, bgColor Housekeeping.

WGSL helpers

Special paths: particles: { count } runs a built-in GPU compute simulation instead of your fragment (see particleFlow.ts); mesh3d: { grid } runs a depth-tested instanced 3D grid (see spectrumScape.ts).

Craft notes