Live · July 2026
Aurora
A browser playground for writing GLSL in real time

- 417
- Tests
- 8
- Presets
The idea
Aurora is a browser playground for GLSL: write a fragment shader in one pane, watch it compile and draw in the other, then share the whole thing, code and every knob value, through a compressed link. There is no backend and no account behind it.
The interesting problem
Running the shader is the easy part. Telling the author what broke is not. The playground wraps user code in a prelude, the precision line and the uniform declarations, so every line number the driver reports is shifted by a fixed offset. Drivers do not even agree on how to report it: ANGLE emits ERROR: 0:12:, Mesa emits 0:12(5): with a column. Aurora parses both forms, subtracts the prelude offset, and puts the marker on the line the compiler actually named. An error that lands inside the prelude clamps to line one rather than disappearing, because a marker in the wrong place still beats no marker at all.

A missing semicolon on line 14, and the marker sits on line 15, where the compiler points. The preview keeps the last frame that compiled instead of going black, and the knobs the shader declares stay live underneath it.
One context for the whole wall
Every tile in the preset gallery is a running shader, not a thumbnail. The obvious build gives each tile its own canvas and falls over, because a browser only hands out around sixteen live WebGL contexts. So the gallery keeps a single offscreen renderer: it draws each preset there in turn, and every cell takes a cheap 2D context and receives the finished frame through drawImage. Cells cost nothing against the WebGL budget. A browser test pins the invariant down by counting real getContext calls, using the preview’s own context as a positive control, and asserting the wall adds exactly one.

Eight presets, all animating at once, all drawn by one renderer. The panel says so in its own subtitle: live previews on one shared canvas.
What shipped
Two modes over one model: Config, where a shader’s uniform declarations become live controls, and Code, where the editor takes over. The controls are hand-built rather than native inputs, so a slider announces the value a screen reader should hear instead of a step count that lies. Sharing compresses the source and every knob value into the link, and because that link is untrusted input coming back in, a mangled one loads the default instead of crashing. The app records itself too: a PNG still, a WebM clip, and an animated GIF encoded in a worker. Underneath sit the unglamorous parts, recovery from a lost WebGL context, a frame cap, and a static frame when the visitor asks for reduced motion. 417 unit tests across 37 files, plus 26 browser scenarios that run against the production build.
What I learned
The WebGL recovery path taught me that a browser event can be a question rather than a notification. When the context is lost, the webglcontextlost event has to be cancelled with preventDefault before the browser will ever fire the matching restore. Skip that one line and the recovery code below it is unreachable, silently, forever, and every test that mocks the event still passes. It only shows up if you take a real context away from a real browser and wait for it to come back.