Skip to content

The built-in systems

Every built-in system is addressable from a preset. Their named parameters, defaults, and per-system notes are tabulated in Parameter roster — that table is maintained alongside the presets and is the authoritative list.

system = What it draws
fragment_fieldA fullscreen domain-warped light field (fragment shader).
swarm~10k CPU-simulated particles on an evolving flow field.
parametric_curveA sampled line curve — the Maurer rose.
lsystemAn L-system turtle figure, precomputed per depth.
star_patternA Hankin star pattern over a regular tiling.
reaction_diffusionA Gray-Scott reaction-diffusion field.
attractorGPU compute particles iterating a strange attractor.
spectrumThe log-spaced band array as N elements — bars, a contour, or a ring.
emitterObjects that spawn, ride their own parabola, and die — the only system whose population varies.
shape_fieldOne mark silhouette drawn at frame scale as a signed-distance field, so banding the palette draws concentric offset contours.
warp_meshThe previous frame, resampled through a grid with one transform per vertex — the only system that draws nothing of its own.
shape_collageFlat opaque elements on their own off-white paper, composited in painter order — the only system in which one object is genuinely in front of another, and the only one that draws a graphic rather than light.

There is deliberately no per-system preset count here. A count re-drifts every time a preset is added and nothing fails when it does. presets/ is the list; ls presets/*.toml is the count.

shape_collage adds parameters, not grammar. Every variable, constant, function, operator and error message in The expression language is the same with it as without, and a collage preset is written with the same vocabulary as every other preset — worth stating, because a new system reads like it must have brought new grammar with it.

The four line systems share one stroke. parametric_curve, lsystem, star_pattern and spectrum all draw through the same line renderer, so thickness, brightness, glow and softness mean the same thing on all four and a value that reads well on one transfers. softness is the shape of the stroke across its width: 0 is solid with a one-pixel antialiased edge, 1 is a pure quadratic falloff, and the default is 0.25. It is coverage where glow is light — reach for softness when a figure reads blurred and glow when it reads too bright. The working ranges, the sub-pixel limit and the thickness dead zone that sits beside it are in Parameter roster.

Beyond a system’s own parameters, every preset may also bind the engine-wide compositing controls — the shared view transform (zoom, pan_x, pan_y), the background pass (bg_*), feedback trails, the screen-space kaleidoscope (kaleido_*), bloom_*, occlude (how much of the figure’s coverage the backdrop resolves against), the frame exposure, and the final ink-on-paper remap (ink_* / paper_*). Those are documented under Engine-wide controls.

They run in a fixed order, which is worth knowing when a look does not compose the way you expect:

|------------------ linear light, unbounded -------------------| |-- 0..1 --|
scene -> post chain (trails -> kaleidoscope -> bloom) OVER background -> [transition blend] -> tonemap/exposure -> ink -> present

Everything up to and including the post chain is per preset — during a dissolve each side composites its own backdrop and chain, independently. The blend, the tonemap and the ink remap are engine-wide: one pass each, over the frame both presets produced.

Everything left of the tonemap is floating-point linear light, so an additive accumulation is free to exceed 1.0 and no hand-off clips; the tonemap is the single place the frame becomes a displayable picture. That is why bloom_threshold can mean “brighter than the display could show” and why stacked strokes roll off with their colour intact instead of flattening to white.

The post chain renders at the render target’s own resolution. trails and kaleido_* follow the target (quantized and capped) rather than a fixed grid, so composing either one costs no sharpness at any display size.

A stage changes softness, never shape. The internal grid is a resolution, not a proportion: your scene is drawn at the window’s aspect and the stage’s present stretches it back by exactly the inverse, so the grid’s own ratio cancels out and a circle stays a circle whether trails is on or off (ADR-0037). A preset comment that blames a stage for a figure’s shape is stale.

Two things still follow from a stage being a resample:

  • On a line scene, mirror_order / mirror_reflect replicate real geometry before rasterization, so they cost nothing in resolution, while kaleido_* folds finished pixels. Prefer the mirror when either would do — see Mirror or kaleidoscope?.
  • Reaction-diffusion is a special case in the other direction. Its simulation grid is deliberately fixed and independent of the window, but the field is toroidal, so pan_* is a seamless infinite scroll and zoom > 1 tiles rather than running out of field.

The [path] table

The shape_field system draws a silhouette from a closed roster of five names. A [path] table is the escape hatch: inline SVG path data, parsed once at load into a closed contour and rendered as the same signed-distance field, so a preset can author a figure nobody put in the roster.

system = "shape_field"
[path]
d = "M 0,-1 C 0.9,-0.4 0.9,0.4 0,1 C -0.9,0.4 -0.9,-0.4 0,-1 Z"
morph_to = "M 0,-1 L 0.87,0.5 L -0.87,0.5 Z" # optional; `morph` travels to it
samples = 64 # 3..=64, default 64

The supported subset is M m L l H h V v C c S s Q q T t Z z. A/a (the elliptical arc) and a second subpath are refused by name, each with an error saying what it found — an author meeting one is holding a file a browser renders correctly, so “invalid path” would be a cruel thing to say. Anything else that goes wrong is a load error carrying the character offset into d, because a mis-parsed path renders as a plausible wrong figure rather than as a mistake.

morph is an ordinary binding under this document’s language, and [smoothing] reaches it like any other — the pair is aligned once at load (winding, start point, arc-length arity) so nothing per frame re-derives a correspondence.

Parameter roster carries the rest: the arity ceiling and the measurement it came from, which pairs morph well, and the plain fact that a long path stops a preset being readable.

The [spectrum] table

The spectrum system is the one whose figure is the analysis rather than a generator’s geometry, so what it draws is chosen structurally rather than through [params]:

system = "spectrum"
[spectrum]
elements = 26 # 2..=64, default 24
layout = "bars" # bars | polyline | radial_ring
smoothing = { attack = 0.025, release = 0.22 } # seconds; default: instant
KeyValuesNotes
elementsinteger 2..=64How many elements the frequency axis is divided into. Default 24.
layoutbars, polyline, radial_ringDefault bars.
smoothingseconds, or { attack, release }Per-element easing, in the same vocabulary as [smoothing].

Every key is optional and so is the table itself. An out-of-range elements or an unknown layout is a surfaced load error naming what it expected, like every other structural table — never a silent fallback.

Element 0 is the bottom of the spectrum and the last is the top, so no expression maps audio to position; that mapping is the scene. Three things follow that are worth knowing before you tune one:

  • 64 is the ceiling because 64 is the data. The engine analyses 64 log-spaced bands and the scene averages each element’s own contiguous slice of them — a real partition, nothing dropped or double-counted. A readout finer than its own data would be a lie rather than a feature.
  • The axis is only half logarithmic, and the low end is the coarse end. Band edges follow a log curve, but each band is floored at one FFT bin (23.4 Hz at 48 kHz), which binds all the way up to ~750 Hz — so the bottom 31 of the 64 bands are linear. Element 0 therefore covers about an octave while an element near the middle of the figure covers a semitone or two. This is the opposite of what a log axis suggests, and it is the same caveat bin(x) carries — the measured mapping is tabulated there.
  • smoothing here is the one easing an expression cannot reach. The element levels are scene state computed from the band array, not a binding, so [smoothing] has no name to attach to them. Asymmetric values earn their keep: the bands are the rawest signal in the engine, so a fast attack keeps a transient’s shape while a slow release lets the elements fall like a meter instead of strobing on every analysis hop. It eases the level after the curve param has shaped it — the value you see — so a release means the same duration whatever curve is set to (ADR-0040).

This is also the one system that reads per-element bindings: a [params] expression naming index is evaluated once for each element, so the relationship between a frequency region and what is drawn there is preset content rather than scene code.

Full parameter notes are in Parameter roster, including which composite controls this system honors, the layout-specific parameters (radius on the ring; span and baseline on bars/polyline), and the curvescale retune a level curve costs — a 5.8x amplitude change at curve = 0.5 against measured typical band levels, which is the reason the default is exactly linear.

The [feedback] table

The engine has two accumulation buffers — the trails post stage, which every scene composites through, and the attractor scene’s own internal trail field — and both read their past through the same transform (ADR-0048). The moving parts of that transform are ordinary bindables (fb_zoom, fb_rotate, fb_dx, fb_dy, fb_center_x, fb_center_y, fb_warp, all documented in Parameter roster); the two structural choices are this table:

[feedback]
warp = "swirl" # none (default) | swirl | ripple | fisheye
blend = "add" # max (default) | add
KeyValuesNotes
warpnone, swirl, ripple, fisheyeWhich curated procedural distortion rides on top of the affine. Its strength is the bindable fb_warp. Default none.
blendmax, addHow this frame’s light lands on the faded past. Default max — the engine’s only blend until this table existed.

Both keys are optional and so is the table. An unknown value is a surfaced load error naming what it expected, like every other structural table — a warp kind selects a shader path, and quietly falling back to none would render a look you never asked for with nothing on screen to say so.

Two things about this table surprise people, and both are consequences of there being two buffers rather than one:

  • One vocabulary, two sinks. A single fb_rotate on an attractor preset that also binds trails turns both accumulations — the scene’s field and the stage’s — each about its own buffer, neither about the other’s. That is deliberate (one thing to learn, and it transfers), but if you have both live and are attributing the motion to one of them, you are attributing half of it wrong. Turn trails off to see the scene’s own.
  • blend reaches the trails stage only. The attractor’s deposit has been additive since the scene was written — its points draw through an additive pipeline over the decayed bed — so there is no max to select there. warp reaches both.

The transform applies to the past, never to the light being deposited this frame: the fresh figure is always where the scene put it, and only the trail behind it travels.

Transitions between presets

A preset switch dissolves rather than cuts. Nothing about it is preset-authored today — duration and blend kind are engine policy in code — but two consequences reach your file:

  • Your preset renders live from the dissolve’s second frame. It is composited through its own background + post chain the whole way, so a trails accumulation starts from empty at the switch, not from the outgoing preset’s history.
  • ink_* / paper_* crossfade. There is one ink pass for the blended frame, so its params travel from the outgoing preset’s values to yours across the dissolve. A preset whose look depends on ink reaches it about a second after the switch.

Per-preset [transition] declarations are a deliberate follow-up, not a gap you can work around from the TOML.


Built from a8ce055 at version 0.115.0. This site tracks main and is not versioned per release.