Shader studio
The tool around the shaders — compose in the aspect you will ship, tune by pass, and take the result with you.
A gallery you can only look at is a demo of the gallery. The studio at /lab/shaders is the same thirty-two shaders with the four things that make them usable: a frame, an inspector that matches the pipeline, a history, and a way out.
Compose in the aspect you will ship
The stage is an aspect ratio, not a viewport. A shader composed in a wide strip is a different image at 9:16 — the wells sit somewhere else and the vignette crops differently. So the frame comes first and the export follows it: the PNG buttons set the long edge, and the short edge comes from the shape you composed in. A 9:16 export at 1× is 900×1600, not a widescreen render with the sides cut off.
The inspector is the pipeline
Controls are grouped by pass, not alphabetically:
| Group | What it changes |
|---|---|
| Source | The effect itself, before anything is done to it |
| Displacement | Warps that result along a flowing noise field |
| Composite | Softness, bloom, lens and grain, applied last |
Eleven flat sliders hid the fact that order matters — scale changes what is
drawn, warp distorts that result, and bloom only ever sees what survived the
warp. Grouping them is the cheapest documentation the pipeline will ever get.
Controls that cannot currently do anything say so rather than sitting there
inert: Levels only affects the two dithered shaders, and Cursor dims while
cursor distortion is switched off.
Four ways out
npx shadcn@latest add https://ui.whoisroktim.lol/r/shader-canvas.jsonEverything below lives in shader-export.ts and depends on nothing in the page
it was tuned on.
PNG at 1×, 2× or 4K. Rendered offscreen through the shared context at full size — the same five passes with bigger targets, so a 4K still is genuinely 4K rather than an enlargement of the viewport.
A six-second WebM, captured off the live canvas. VP9 where the browser supports it: VP8 bands badly on exactly the gradients these shaders exist to avoid banding on. It is a clip, not a seamless loop — most of these are driven by non-repeating noise and have no period to loop on.
A standalone HTML file. Not a snippet that imports the component — an actual page carrying all five programs, the palette and the tuned values, with no imports, no CDN and no build step. Open it from disk or drop it on any host.
import { standaloneHtml } from "@/components/app/shader-export";
const html = standaloneHtml("dusk", params); // a complete documentA link. Only values that differ from that shader's own defaults are encoded,
so ?s=dusk&bl=1.25&sc=1.6 shows the two things you changed instead of burying
them under eleven parameters.
import { decodeState, encodeState } from "@/components/app/shader-export";The studio reads that state at render, via useSearchParams, rather than in
an effect. Reading it in an effect paints the default look first and then snaps
to the shared one — precisely the flicker a shared link must not have. On a
prerendered route that requires a <Suspense> boundary, which is why there is
one.
History
Undo and redo commit on discrete actions only — changing shader, resetting, randomising. A slider drag is thousands of state changes, and recording each would turn undo into a frame-stepper.
Surprise me jitters around a shader's own defaults rather than sampling each
parameter across its full range. Uniform random over eleven parameters reliably
produces mud.