The store is open·Caps, tees, posters, zines, and stickers, shipped from Watsonville·Shop now →
← Studio Notes
2026-09-07 · Studio update

Rendering shader motion without screen recording

Echo Chamber is a dragonfly run through a CMYK halftone with a dither layer on top. Getting it out as video meant driving the time uniform from a script instead of recording the screen, which turns a shader loop into something you can re-run.

Extreme close up of the CMYK halftone dots over the dragonfly body.
One ground, seen close. The inks are not process CMYK.

Three variants, one halftone underneath all of them. The artwork was built on the Paper canvas. The interesting part is what it took to get it out as motion.

The stack

Design surfacePaper, @paper-design/shaders-react 0.0.80
ShadersHalftoneCmyk ground, Dithering above it
DriverClaude Code in the terminal, reading Paper over MCP
RenderHeadless Chromium via Playwright, WebGL on ANGLE and Metal
Encodeffmpeg, x264, ping-pong segments
PublishSelf-hosted Postiz in Docker, assets on Cloudflare R2

Every parameter came out of the Paper file over MCP, not off a screenshot. get_jsx on a shader node returns the component with its real props: dot size 0.24, grid noise 0.23, ink type, softness 0.4, contrast 1.25. The inks are #59AFC5, #D8697C, #FAD85C, #2D2824.

Close up of the 4x4 Bayer dither grid breaking up over a wing.
Wave, 4x4 Bayer, cyan on lighten. The grid is doing the work.

Only the dither layer changes between variants. Ripple at 2x2 in copper on color dodge. Wave at 4x4 in cyan on lighten. Simplex at 2x2 in green on color dodge.

Variant one, ripple dither in copper. Variant two, wave dither in cyan. Variant three, simplex dither in green.
Ripple, wave, simplex. Same ground every time.

Why not just record the screen

Recording gives you a file and nothing else. The output depends on frame timing, GPU load and when you hit stop, so no two captures match and the loop rarely closes.

This one never plays. Speed is zero and the script sets the time uniform for each frame before grabbing it:

page.evaluate("ms => window.__setFrame(ms)", i * (SPAN_MS / FRAMES))
stage.screenshot(path=vdir / f"{i:04d}.png")

Sixty frames per variant, each segment played forward then reversed, so the turnaround lands on a frame that already exists. No crossfade, no seam hunting.

Close up where the radial diagram type crosses the dithered ground.
Vector line work sits above the shader stack, unblended.

Why it is worth doing

The render becomes an input instead of an artifact. Run it tomorrow and it produces identical frames. Change ripple to swirl, or 2x2 to 8x8, and the new variant is directly comparable because nothing else moved. That is the difference between having a video and having something that emits videos.

It also means the work survives its tools. Paper's raster export returned solid black the whole time this was built. It did not matter, because the parameters were already out of the file and the render happens somewhere else.

Two things that cost time

Missing uniforms fail silently in these shaders. No error, just quietly wrong output, so the full sizing set gets passed every time and the values were lifted from a board that already rendered.

The first encode had no audio track. It decoded fine and looked fine, but silent video trips up players and Instagram expects an audio stream on a reel even when there is nothing to hear.