Skip to content

Build an Operator Graph

std/visuals has ready-made operators — gfx.Noise, gfx.Blur, gfx.Displace, gfx.Composite and more — that you wire into a graph instead of writing GLSL. Every operator, with its uniforms and inputs, is in the std/visuals reference.

Each constructor takes a name. Set uniforms with <<, and chain with >>, which feeds the left node into the right one’s main input:

use "std/visuals" as gfx;
let field = gfx.Noise("field");
field.parameter("period") << 2.0;
let soft = gfx.Blur("soft");
soft.parameter("radius") << 8.0;
field >> soft >> screen;
PLAY;

A uniform takes a number, a signal, a meter feed, or an array of 2–4 numbers for a vector:

edge.parameter("strength") << Sine(8).range(0.5, 2.0);
blur.parameter("radius") << drums.rms.smooth(80).range(0, 20);
ramp.parameter("color0") << #[0.1, 0.0, 0.2];

An operator with two inputs names its second one. Bind a node to it with <<:

comp.parameter("b") << other; // Composite's input B
disp.parameter("by") << field; // what Displace displaces by
look.parameter("gradient") << ramp; // Lookup's colour gradient

Keep the node in a variable and wire it wherever it is needed. It is drawn once per frame, and every consumer reads that result:

use "std/visuals" as gfx;
let field = gfx.Noise("field");
field.parameter("period") << 2.0;
let soft = gfx.Blur("soft");
soft.parameter("radius") << 6.0;
let softer = gfx.Blur("softer");
softer.parameter("radius") << 24.0;
let comp = gfx.Composite("comp");
comp.parameter("operand") << 2.0; // subtract: A - B
field >> soft >> comp; // A
comp.parameter("b") << (field >> softer); // B
comp >> screen;
PLAY;

The name is the operator: the same name gives back the same node. A helper that builds its nodes with fixed names hands every caller the same ones, so take the name as an argument:

use "std/visuals" as gfx;
fn glow(name, src) {
let b = gfx.Blur(name);
b.parameter("radius") << 12.0;
return src >> b;
}
glow("halo", gfx.Noise("field")) >> screen;
PLAY;

To read what the graph drew in the previous frame, build it inside gfx.feedback. The function gets prev, last frame’s output; wire it into an input, and return the node that is the output:

use "std/visuals" as gfx;
let look = gfx.feedback(fn(prev) {
let field = gfx.Noise("field");
field.parameter("period") << 3.0;
let disp = gfx.Displace("trail");
disp.parameter("amount") << 0.1;
disp.parameter("by") << prev;
return field >> disp;
});
look >> screen;
PLAY;

Each frame displaces the noise by the frame before, which leaves a flowing trail.

The trail survives your edits. To wipe it and let it build again, reset the node the function returned:

look.reset();

examples/05_visuals/04_voronoi_feedback.non is a whole patch built this way: cells that swell and warp with a polyrhythm, using fan-out, feedback and audio-driven uniforms together.