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.
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 surface | Paper, @paper-design/shaders-react 0.0.80 |
| Shaders | HalftoneCmyk ground, Dithering above it |
| Driver | Claude Code in the terminal, reading Paper over MCP |
| Render | Headless Chromium via Playwright, WebGL on ANGLE and Metal |
| Encode | ffmpeg, x264, ping-pong segments |
| Publish | Self-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.
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.
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.
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.