IInbox
Composed

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:

GroupWhat it changes
SourceThe effect itself, before anything is done to it
DisplacementWarps that result along a flowing noise field
CompositeSoftness, 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.json

Everything 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 document

A 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.