Generative grain plates for still and moving monochrome fields.
A single-page WebGL2 tool for making the grainy, near-black gradient images that fill phone wallpaper feeds — soft lit forms, folded satin, liquid chrome, light shafts — with every part of the look under a slider, exported at any resolution.
One fragment shader draws the whole image in a single pass. There is no photo input, no library of textures, no upscaling: a scalar field is evaluated per pixel, tone-mapped, colored, and grained at the exact output resolution you export. A 4K plate is not a stretched preview — it is the same math sampled more finely.
The pipeline, in order:
| Stage | What happens |
|---|---|
| Field | One of eight shape functions returns a luminance value for the pixel — a lit column, a beam, folded waves, ridges, smoke, a sphere, warped chrome contours, or merging blobs. Value-noise fBm with adjustable octaves drives the warping. |
| Optics | A 10-tap kernel blurs the field in scene units, stretchable along an axis for directional motion blur. Because it is applied to the field and not to pixels, blur is resolution-independent. |
| Motion | The field walks a closed path in noise space and band phase advances exactly 2π per loop, so an animation returns to frame one exactly — loops are seamless by construction, not by crossfade. |
| Tone | Exposure, black point, gamma, contrast pivot, gloss (a specular contour sharpener), vignette. |
| Color | Luminance maps through a three-stop gradient — shadow, mid, light — with a movable mid stop and an invert. Fifteen presets from Silver and Steel to Cyanotype, Rosé, and Neon, or set the three stops by hand. |
| Grain | Cell noise sized in output pixels, with density (what fraction of cells carry a speck) and midtone bias (film weights grain toward mid values; dial it down for grain that reaches into the shadows). Optional chroma grain. |
| Surface | An optional overlay: diagonal dashes, brushed streaks, weave, or a luminance-sized halftone dot screen applied after tone mapping. |
Fourteen presets, each setting the full form/optics/tone/colour/grain stack — and each carrying its own three-stop ramp, so no two plates land on the same colour. A plate is a look, not a working setup: picking one moves only the keys it owns and resets the ones it does not name, while your mid stop, output size, seed, and motion settings stay exactly where you put them. The ramp is still yours to change afterwards; the plate only sets where you start.
- Export — one still as PNG, WebP, or JPEG at the configured resolution (up to 8192 px a side). PNG over 16 MB falls back to WebP automatically.
- Export set — 4–12 seed variations of the current look, back to back.
- Record loop — one full animation cycle as MP4 (H.264). Frames are
rendered one at a time, encoded through WebCodecs with exact timestamps, and
muxed by
src/js/05-video.js, so a plate that renders at 14 fps still yields a true 24/30/60 fps video — it just takes longer to write. Browsers without WebCodecs H.264 fall back to real-timeMediaRecordercapture, which shrinks the frame to whatever size the machine can paint fast enough.
Presets cover iPhone 16 Pro (1206 × 2622) and Pro Max, common phone and desktop sizes, and a 4:5 print ratio; any custom size works too.
The plate is the page. It sits whole above a bottom sheet — never cropped, so what you see is what lands on the lock screen — and the sheet keeps one height whichever control you are in, so the picture never jumps as you move around.
A row of category tiles never leaves the bottom: Recipes, Form, Optics, Motion, Tone, Color, Grain, Surface, Output. Recipes opens onto a horizontally scrolling strip of plate thumbnails, so you are picking from pictures rather than from fourteen identical words. Every other tile shows that group's own controls right there — label on the left, slider, switch or picker on the right — so changing anything is one tap and a drag, with nothing to open and nothing to back out of. Pickers that would swallow the panel as a grid, the shapes and the palettes, scroll sideways instead and keep the current choice in view.
Shuffle, Save, and Share sit as icons in the top right. Share hands the rendered PNG to the OS share sheet, which on iOS is the shortest path into Photos.
Tapping the wallpaper hides the entire interface for a clean look at it, and tapping again brings it back. The sheet handle collapses the panel to just the tile row when you want more picture without losing the controls.
On arrival the page walks the plates in order — the same order as the strip, so the row reads as a filmstrip — naming each one until you touch it, so the range is visible before you know there are any controls.
iOS tints the strip behind the clock with the page's theme colour, so the app samples the top of the plate after each change and hands that colour over, which stops the band reading as a black bar. Add to Home Screen goes further: launched from there it runs with a translucent status bar and the plate really does reach the top of the screen.
Copy link to this look (in Output) encodes the entire plate — form, optics, tone, palette, grain, size, seed — into the URL, so a look can be sent to someone and opens exactly as you left it.
iOS has no third-party live-wallpaper API — motion is possible only on the Lock Screen, and only via a Live Photo. Two routes work:
Animated Lock Screen. Set Loop length to 3.0 s (Live Photos are ~3 s), turn on Animate, and Record loop. Move the MP4 to the phone, convert it to a Live Photo (intoLive or similar), then Settings → Wallpaper → Add New Wallpaper → Photos, confirm the motion toggle is on, and Set as Wallpaper Pair. The Home Screen still shows a frozen frame — that part is Apple's limit.
Rotating stills. Export set a batch, drop them into an album, then Settings → Wallpaper → Add New Wallpaper → Photo Shuffle → Album, and pick a shuffle frequency. For finer control, a Shortcuts Time of Day automation with the Set Wallpaper action can swap plates on a schedule — that action needs a plain Photo wallpaper, not a Photo Shuffle one.
The Lock screen guides toggle overlays the Dynamic Island, clock, widget row, and bottom controls so the bright part of a plate can be placed where the clock will not sit on it. Guides are preview-only and never exported.
The build output is one self-contained file. Open it directly — no server, no install:
xdg-open dist/index.html # or: open dist/index.htmlSaving from a file:// copy writes straight to disk with no prompts, which is
the fastest way to run Export set.
build.mjs inlines the stylesheet, both shaders, and the JavaScript modules
into dist/index.html. It needs Node 18+ and has no dependencies:
node build.mjsRun the production-build checks after building:
node test.mjssrc/
├── index.html page shell and markup
├── styles.css full stylesheet (single dark theme)
├── shaders/
│ ├── plate.vert.glsl 3-vertex fullscreen triangle
│ └── plate.frag.glsl field, optics, tone, palette, grain, surface
└── js/
├── 01-data.js parameters, shapes, palettes, plates, sizes
├── 02-renderer.js WebGL2 program and uniform upload
├── 03-stage.js canvas sizing, paint loop, animation clock
├── 04-controls.js control factory and the rail
└── 05-actions.js plates, shuffle, export, batch, recording
The JS modules are concatenated in filename order and share one top-level scope, so ordering is load-bearing: data, then renderer, then stage, then controls, then the actions that wire them together.
Needs WebGL2 — current Chrome, Firefox, Safari, and Edge all qualify. Loop
recording additionally needs MediaRecorder with canvas capture; the button
disables itself and says so where that is missing. Google Fonts is the only
external request.
MIT — see LICENSE.

