Colophon · reproducible
How this was made.
LUSTRE is one HTML page with a real-time 3D object, shipped as a static site under a strict Content-Security-Policy. Here is the whole recipe, so you can build your own.
Nothing here is filmed, baked or pre-rendered. The object is computed in your browser, sixty times a second. The rest is deliberately boring: static HTML, a handful of kilobytes of CSS, and one JavaScript module.
The stack
- Astro for static HTML with zero framework runtime.
- Tailwind CSS v4 (CSS-first tokens) for the design system.
- Three.js, bundled and served from our own origin, for the object.
- Bricolage Grotesque and Space Mono, self hosted.
- Cloudflare Pages for hosting on the edge.
The object
The signature is a single mesh: a moderately subdivided icosahedron. Every frame, each vertex is pushed along its original direction by two octaves of 3D simplex noise, giving a slow, liquid morph. The trick that makes it read as metal rather than a wobbling blob is recomputing the surface normals after each displacement, so the reflections are always physically correct.
// every frame: displace the sphere along its base normals by flowing noise,
// then recompute real normals so the PBR reflections stay correct.
for (let i = 0; i < count; i++) {
const [dx, dy, dz] = baseDir[i]; // unit direction of vertex i
const n1 = simplex3(dx*f + t, dy*f, dz*f - t);
const n2 = 0.5 * simplex3(dx*2.3 - t, dy*2.3 + t*0.6, dz*2.3);
const d = RADIUS + (n1 + n2) * amplitude; // liquid, two octaves
positions[i] = [dx*d, dy*d, dz*d];
}
geometry.computeVertexNormals(); // the reflections depend on thisA single MeshPhysicalMaterial is then interpolated across your scroll position, so the same geometry travels through three states: mirror chrome, an iridescent oil film, and frosted transmissive glass.
// one MeshPhysicalMaterial, interpolated across scroll (0..1)
chrome -> metalness 1.0 roughness 0.06 iridescence 0.35 transmission 0
iridescent -> metalness 0.9 roughness 0.11 iridescence 1.0 transmission 0
glass -> metalness 0.06 roughness 0.15 iridescence 0.55 transmission 0.94Reflections come from a RoomEnvironment baked into a PMREM cube map at load, so there is no multi-megabyte HDR download. Three coloured point lights orbit slowly, pouring cyan, magenta and gold into the metal. Scroll position and cursor are both smoothed with a simple lerp so nothing snaps.
Performance and the first paint
- A pure-CSS chrome sphere paints instantly and is the fallback when WebGL is missing. The canvas fades in over it once ready, so the largest paint never waits on JavaScript.
- Device pixel ratio is capped (2 on desktop, 1.6 on mobile) and the mesh is coarser on small screens.
- The render loop pauses when the tab is hidden.
Accessibility, always on
With prefers-reduced-motion: reduce the animation loop never starts. Instead the object renders one complete, beautiful frame and holds it. Reveals appear immediately, focus states are visible, contrast passes AA, and every control is reachable by keyboard.
A strict Content-Security-Policy
The production build ships a hash based CSP with no 'unsafe-inline'. That rules out inline styles and inline event handlers entirely, which is a useful discipline.
// astro.config.mjs : strict hash CSP, no 'unsafe-inline'
security: {
csp: { directives: ["default-src 'self'", "img-src 'self' data:", ...] }
}
// consequence: no inline style="" and no onclick="" anywhere.
// stagger delays and swatch colours become CSS classes; motion lives in
// bundled, same-origin scripts that Astro hashes automatically.The still images
The three sculptures in the Collection are not the live object. They are separate stills, generated with Recraft as one family, then optimised to responsive WebP by Astro. Every prompt is published verbatim on the prompts page.
Ship it
Two commands. The first run creates the Pages project; pass the production branch so the clean URL serves the latest build.
npm run build
wrangler pages deploy dist --project-name lustre --branch mainDo the same
- Start a static Astro + Tailwind project.
- Add one full-viewport, fixed, transparent canvas behind your content.
- Give it a noise-displaced mesh and a physically based material, and drive one or two parameters from scroll.
- Provide a static first frame for reduced-motion and a CSS fallback for no-WebGL.
- Turn on a strict CSP and fix everything it breaks. Deploy to the edge.