Manic Meets Shaders
A shader is the most intimidating corner of graphics: per-pixel math, running in parallel on the GPU, with no loops you can read and no objects you can name. Manic is a declarative language built for people who do not want to think that way. This is the story of how the two met — and why the meeting made both of them better.
Manic began with a simple wager: you should be able to describe an animation the way you would describe it out loud. Name the things that exist, name what happens to them, and let the engine handle frames, interpolation, seeking, and framing.
point(A, (260, 720), "A");
circle(omega, A, 180);
draw(omega, 0.8, smooth);That works beautifully for geometry, calculus, algorithms, and everything else made of objects. But there is a whole family of visuals that has no objects at all. A plasma field. A Julia set. Flowing cellular noise. A glitch. These are not shapes you place — they are a colour assigned to every pixel by a formula.
For a long time that family lived on the other side of a wall. To make it you opened a GLSL editor, thought in gl_FragCoord, and accepted that the result was a black box the rest of your scene could not touch.
Manic’s shader system is an attempt to take that wall down without dumbing the idea down.
▶ Try Manic: 8gwifi.org/manic · Explore the gallery and documentation: 8gwifi.org/manic/docs
Manic Official https://maniclang.com
The insight: a shader is cloud turned inside out
Manic already had a primitive for fields of particles. cloud gives every one of tens of thousands of points a position, size, and colour as a closed-form function of its index i and time t:
cloud(swarm, 40000, cyan, 0.9) {
let a = i / 40000 * tau;
let x = cx + 300 * sin(a + t);
let y = cy + 300 * cos(a * 3.0);
let hue = mod(a * 57 + t * 40, 360);
}A shader is the same idea, turned inside out. Instead of a formula per particle, it is a formula per pixel. Every pixel already knows its normalized coordinates u and v, the time t, and the aspect ratio asp. You just say what colour it should be:
shader(bg) {
let x = (u - 0.5) * asp;
let y = v - 0.5;
let d = hypot(x, y);
let plasma = 0.5 + 0.5 * sin(x * 6.0 + t) * cos(y * 6.0 - t * 0.8);
let hue = mod(200 + plasma * 120, 360);
let sat = 0.7;
let val = 0.4 + 0.5 * plasma;
}No loop. No buffer. No assets. Just algebra over (u, v, t). The per-particle world and the per-pixel world are now siblings in the same language, and they share the same shaping vocabulary—mix, smoothstep, clamp, fract, length—so a trick you learn in one carries into the other.
Purity is the whole point
The most important rule is invisible: a shader must be pure in t.
Give it the same time and it produces the same frame, every time, with no hidden state carried from the last one. That sounds like a restriction. It is actually the feature that makes a shader a first-class citizen of an explainer.
Because the field is pure, it scrubs. Drag the playhead to eight seconds and you get exactly the frame that eight seconds means — not “wherever the simulation happened to drift.” Because it is pure, it records deterministically: the mp4 is bit-for-bit the thing you previewed. A typical creative-coding sketch draws once and evolves by mutating variables; you can screenshot it, but you cannot seek it. Manic asks for purity up front and gives you a timeline in return.
This is the same discipline that lets a geometry construction survive a moved vertex. The medium is different; the wager is identical. Describe the truth as a function, and time becomes something you can travel through instead of something you have to babysit.
Two doors into the same room
Not everyone wants to write raw field math, and not everyone wants training wheels. So there are two paths to the same look.
The high-level door. Common shader ideas are single builtins. The escape math that would be a hand-written loop becomes one call:
shader(julia) {
let cx = 0.7 * sin(t * 0.3);
let cy = 0.7 * cos(t * 0.2);
let e = julia(u * 3.0 - 1.5, v * 3.0 - 1.5, cx, cy); // escape fraction
let hue = mod(220 + e * 160, 360);
let val = e;
}julia, mandelbrot, and voronoi run their inner iteration in the engine and hand back a clean scalar—so the DSL formula stays closed-form and seekable even though the mathematics underneath is a loop. Drawing with distance is the same story: a whole library of signed-distance shapes—sdcircle, sdtriangle, sdhexagon, sdstar, sdpolygon, sdroundbox—compose with smin, sdsub, and sdint, exactly as they do on Shadertoy, but named in words.
The low-level door. When you already know GLSL, glsl() lets you drop a raw expression straight in, and vector types (vec2, vec3) with dot, length, normalize, reflect, cross, and .x/.y/.z swizzles are there when the math genuinely wants them. Reach for raymarch and the same signed-distance thinking lifts into 3D worlds.
The two doors open into one room. A field you prototype with builtins and a field you hand-tune in GLSL are the same kind of object to the rest of the scene.
Reimagining creative coding, faithfully
A good way to test a shader system is to take work that was born in another tool and ask whether the idea survives the translation.
Manic’s gallery does exactly this. The Book of Shaders chapters — plasma, fractals, shapes, cellular noise, patterns — are rebuilt as pure (u, v, t) fields. A Shadertoy classic returns as a faithful DSL twin using the SDF library. And two translations show the range:
Get Anish Nath’s stories in your inbox
Join Medium for free to get updates from this writer.
A p5 glitch-grid sketch originally pre-renders four grid and stripe layers into off-screen buffers, composites them, captures the result, and tears it with a second pass. Manic cannot sample render targets — but the outcome is closed-form. Build the nested random grid procedurally per pixel with floor/fract/rand2, then chromatically tear it by sampling each colour channel at a per-scanline offset:
shader(glitch) {
let band = floor(v * 40.0);
let off = (rand2(band, floor(t * 3.0)) - 0.5) * 0.06;
let ru = u + off; // red sampled here
let bu = u + off * 1.2; // blue torn wider — chromatic split
// ...nested grid + white noise, one formula per channel...
}Four passes and a capture collapse into one expression that scrubs and records where the original could only draw once.
The cloud sibling gets the same treatment. An OpenProcessing "Pottery" challenge—thousands of stippled dots forming wavy vases—becomes one particle formula: dotted ellipse rings grown bottom-up, hash-selected flowering sprigs, and a glaze that re-colours itself live. The reference draws a still. Manic makes it a timeline.
The test is not “can it copy pixels.” It is “does the idea translate into something seekable, composable, and yours.” It does.
The real payoff: shaders that coexist with everything else
Here is the part that matters most, and the part a standalone shader toy can never do.
A shader in Manic is not a separate app you embed. It is a primitive in the same scene as your triangles, your equations, your captions, and your camera. Which means a shader does not have to be the video. It can elevate one.
Consider a vertical Short proving the Pythagorean theorem. The mathematics wants a clean black canvas — coloured squares blooming on the sides of a 3–4–5 triangle until 9 + 16 = 25. But a black canvas is also an opportunity. So the frame splits: the top third is a living, domain-warped aurora plasma — pure per-pixel art — and the bottom two-thirds is the proof, on black, with a crisp accent seam between them.
canvas("9:16");
template("black");// TOP STAGE — the shader as art, confined to the top band, black below
shader(art) {
let inside = 1.0 - smoothstep(0.32, 0.34, v); // 1 up top, 0 below
let x = (u - 0.5) * asp; let y = v / 0.34;
let w1 = sin(x*3.0 + t*0.5) + cos(y*4.0 - t*0.3);
let w2 = sin((x+w1)*2.6 - t*0.4) * cos((y+w1)*3.2 + t*0.25);
let flow = 0.5 + 0.5 * sin(x*4.0 + w2*2.2 + t*0.6);
let hue = mod(244 + flow*118 + w2*44, 360); // magenta → cyan
let sat = 0.82;
let val = (0.14 + 0.62 * flow) * inside;
}// BOTTOM STAGE — the proof, DRAWN OUT on black
polygon(tri, (ox,oy), (ox+4*ux,oy), (ox,oy-3*ux), cyan);
untraced(tri); // start at 0% drawn (opacity stays 1)
draw(tri, 1.4, smooth); // pen the outline on while the fill washes in
Notice the two builtins on the last lines. untraced starts a filled polygon at zero percent drawn without hiding it, so draw can trace its perimeter like a pen while the colour washes in behind the stroke. That is a geometry verb. It runs in the same timeline as the shader above it. Neither knows the other exists, and that is exactly the point: the aurora is art, the triangle is truth, and they share one frame at full value.
This is the argument for putting shaders inside an explanation language rather than beside one. A shader alone is a wallpaper. A shader that can sit above a proof, behind a chart, inside a masked region, or as the mood of a Short becomes a production tool. The elevation is truthful — the plasma is computed, not painted — and it costs one block of algebra.
What this says about the language
Manic’s rule for new vocabulary has always been strict: a primitive has to make a recurring explanation simpler without turning the language into a programming framework. Shaders passed that test in an unusual way.
They did not add objects. They added a second kind of truth — a field instead of a figure — while keeping every promise the rest of the language makes:
- it is declarative: you say what each pixel is, not how to loop over them;
- it is pure: same
t, same frame, so it scrubs and records; - it is composable: it lives in the same scene as geometry, text, and camera;
- it has two doors: named builtins for intent, raw
glsl()for control; - it is responsive:
u,v, andaspkeep it framed on any canvas.
The engine handles the pixels. The creator handles the meaning. A plasma is no longer a black box bolted onto a video — it is a first-class actor that a non-programmer can read, seek, recolour, confine to a region, and compose with a theorem.
The larger idea
For most of graphics history, the beautiful per-pixel worlds and the readable object worlds have lived apart. One was for people fluent in the GPU; the other was for people who just wanted to explain something. You picked a side.
Manic’s wager is that you should not have to.
Name the field. Describe the story. Let the pixels move — next to everything else.
A shader does not have to be a separate craft you learn to earn art. It can be one more sentence in a language you already read: a colour for every pixel, pure in time, sitting comfortably beside the triangle it was made to lift.
Build your first shader scene at 8gwifi.org/manic. Explore the shader gallery and the “Shaders — per-pixel worlds” chapter at 8gwifi.org/manic/docs. Follow Manic on YouTube at zarigatongy and on X at @anish2good.
Made with Manic — the animation language where a formula for every pixel stays true while the story moves.








