Skip to content

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

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 this

A 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.94

Reflections 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

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 main

Do the same

  1. Start a static Astro + Tailwind project.
  2. Add one full-viewport, fixed, transparent canvas behind your content.
  3. Give it a noise-displaced mesh and a physically based material, and drive one or two parameters from scroll.
  4. Provide a static first frame for reduced-motion and a CSS fallback for no-WebGL.
  5. Turn on a strict CSP and fix everything it breaks. Deploy to the edge.