Build notes
How this was built
MIRABILIS is six AI oil paintings and one small shader that makes them behave like wet paint. Static assets, no framework runtime, a strict CSP. Here is the whole thing.
The paintings
Every image is a Recraft V4.1 still life, generated as one cohesive family: the same warm candlelit chiaroscuro, the same dark ground, only the subject changing. Keeping the light and background fixed across prompts is what makes six separate images read as one atelier. The exact prompts are on theprompts page.
They are imported through Astro's image pipeline, which resizes each and re-encodes to WebP, turning a 1.7 megabyte source into a right-sized asset without any manual work.
The signature: liquid paint
Each painting is a texture on a single fullscreen WebGL triangle. The whole effect is one idea: before sampling the image, offset the coordinates. A little flowing noise gives a constant stir; a ripple centred on the cursor gives the interaction.
// warp the image's UVs, then sample the texture there
vec2 disp;
disp.x = fbm(uv * 2.6 + vec2(time, 0.0)) - 0.5; // flowing noise
disp.y = fbm(uv * 2.6 + vec2(0.0, time)) - 0.5;
disp *= 0.010; // a small, constant stir
// a ripple that follows the cursor
float d = distance(uv, pointer);
disp += normalize(uv - pointer) * sin(d*26.0 - time*3.2) * exp(-d*7.0) * hover;
vec3 col = texture2D(image, coverUV(uv + disp)).rgb;Sampling the red, green and blue channels at slightly different offsets adds a thread of chromatic aberration, which reads as a wet, glassy edge, exactly the thing that sells "liquid" over "wobbly".
// sample the three colour channels at slightly different offsets
col.r = texture2D(image, uv + disp + ca).r;
col.g = texture2D(image, uv + disp).g;
col.b = texture2D(image, uv + disp - ca).b; // a wet, glassy edgeFeeding the shader without a second download
The texture is not a separate fetch. The shader uploads the very<img> element the browser already decoded, so it inherits the responsive image Astro picked and costs nothing extra. If WebGL is missing, or the visitor prefers reduced motion, that same image simply stays on screen, unmoved.
// use the <img> the browser already decoded as the texture, so it costs
// no extra download and respects the responsive srcset Astro generated
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, imgElement);Keeping it cheap
Each canvas caps its resolution, pauses when it scrolls out of view, and stops when the tab is hidden. The shader is a few kilobytes of JavaScript, so there is almost nothing for the main thread to do, which is what keeps the page fast even with the paint moving.
Deploy in two commands
npx wrangler pages project create mirabilis --production-branch main
npm run build
npx wrangler pages deploy dist --project-name mirabilis --branch mainThis is one of three showcase sites. See the prompts, or go back to the atelier.