IrisBackgroundsAperture
Aperture
Twenty near-black glass blades fanned around the centre like a camera aperture caught part-open, an iridescent lip walking once around the ring; the pointer spins it and swings the light.
Aperture
Twenty raytraced glass blades fan around the centre of the frame and tilt out of the screen, like the leaves of a camera aperture caught part-open. The fan occludes itself — one ray per pixel, nearest of twenty ray/plane hits — so you read real depth through the overlaps. The faces stay almost black; the light lives on the bevels, a thin iridescent lip that walks violet → fuchsia → copper → azure once around the ring, so one side reads warm and the opposite side cool, converging on a lit hub.
With the cursor away the iris turns on its own at a slow constant rate under a fixed key light up and to the left. As the cursor enters, its horizontal position adds spin and swings the key light around the ring — the iridescent lip sweeps the blades to follow it — while vertical position tilts the camera. The grip eases in and out, so a cursor that leaves lets the iris settle back to its idle turn rather than snapping. The four rim hues come from the site's shader-role ramp, read live, so a theme change or a context restore picks them up.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { ApertureField } from "./ApertureField";
export default function Example() {
return (
// Fills its nearest positioned ancestor (it renders itself `absolute
// inset-0`) — give it a sized, relatively positioned box.
<div className="relative isolate h-[32rem] w-full overflow-hidden rounded-2xl">
<ApertureField />
</div>
);
}Component
The real source, exactly as it ships — multiple files, kept together.
"use client";
import { useEffect, useRef } from "react";
import { mountShaderSurface } from "@/lib/shader-surface";
/**
* A live WebGL iris — twenty near-black glass blades fanned around the centre
* of the frame and tilted out of the screen, like the leaves of a camera
* aperture caught part-open. Each blade is raytraced (one ray per pixel,
* nearest of twenty ray/plane hits), so the fan occludes itself and you read
* real depth through the overlaps. The faces stay almost black; the light
* lives on the bevels — a thin iridescent lip that walks violet → fuchsia →
* copper → azure once around the ring, so one side reads warm and the
* opposite side cool, converging on a lit hub.
*
* The pointer is real. With the cursor away, the iris turns on its own at a
* slow constant rate and a fixed key light sits up and to the left. As the
* cursor enters, its horizontal position adds spin and swings the key light
* around the ring — the iridescent lip sweeps the blades to follow it — while
* vertical position tilts the camera. The grip eases in and out (presence,
* handled by `shader-surface`), so a cursor that leaves lets the iris settle
* back to its idle turn rather than snapping.
*
* Drop it into any `position: relative`/`isolate` parent — it fills the box.
* Built on `lib/shader-surface.ts`, so every degradation path is already
* handled: no WebGL, a blocked or lost context, a hidden tab, or
* `prefers-reduced-motion` all leave the CSS `.iris-aperturefield__floor`
* underneath visible — a still radial composition in the same palette, never
* a blank box.
*
* The four rim hues come from the site's shader-role ramp
* (`--shader-role-2-a` violet, `--shader-role-2-b` fuchsia, `--shader-role-3-b`
* copper, `--shader-role-1-b` azure) and the ground from `--iris-bg-sunken`,
* read live via `colors` so a theme change or a context restore picks them
* up. Every one has a fallback in `shader-surface.ts`, so a blocked token
* readback still draws in the right colours.
*/
const FRAG = `
uniform vec2 u_res;
uniform float u_time;
uniform float u_scale;
uniform vec3 u_pointer; /* x, y (0 bottom .. 1 top), presence 0..1 */
uniform vec3 u_rimA; /* --shader-role-2-a, violet */
uniform vec3 u_rimB; /* --shader-role-2-b, fuchsia */
uniform vec3 u_rimC; /* --shader-role-3-b, copper */
uniform vec3 u_rimD; /* --shader-role-1-b, azure */
uniform vec3 u_ground; /* --iris-bg-sunken */
#define NBLADES 20
#define PI 3.14159265
/* blade geometry, in world units */
const float RAD_MID = 0.75; /* spoke offset of a blade's centre from the hub */
const float HALF_LEN = 0.64; /* blade half-length, along its own spoke */
const float HALF_W = 0.175; /* blade half-width */
const float TWIST = 0.48; /* how far each blade tilts out of the screen */
mat2 r2(float a) { float c = cos(a), s = sin(a); return mat2(c, -s, s, c); }
vec3 rotX(vec3 p, float a) { p.yz = r2(a) * p.yz; return p; }
vec3 rotY(vec3 p, float a) { p.xz = r2(a) * p.xz; return p; }
/* the iridescent lip: a closed loop violet -> fuchsia -> copper -> azure -> violet */
vec3 lip(float t) {
t = fract(t);
vec3 c = mix(u_rimA, u_rimB, smoothstep(0.0, 0.30, t));
c = mix(c, u_rimC, smoothstep(0.30, 0.58, t));
c = mix(c, u_rimD, smoothstep(0.58, 0.82, t));
c = mix(c, u_rimA, smoothstep(0.82, 1.0, t));
return c;
}
vec3 render(vec3 ro, vec3 rd, float roll, vec3 lightDir, float lightPhase, float glow) {
float bestT = 1e9;
float bU = 0.0, bV = 0.0, bPy = 0.0, bAz = 0.0;
vec3 bN = vec3(0.0);
bool hit = false;
for (int i = 0; i < NBLADES; i++) {
float az = float(i) * (2.0 * PI / float(NBLADES)) + roll;
float ca = cos(az), sa = sin(az);
vec3 sp = vec3(ca, sa, 0.0); /* spoke: blade length axis */
vec3 tg = vec3(-sa, ca, 0.0); /* in-screen width axis */
float tw = TWIST * clamp(ca * 2.6, -1.0, 1.0); /* tilt, mirrored across x */
vec3 w = tg * cos(tw) + vec3(0.0, 0.0, 1.0) * sin(tw);
vec3 nrm = vec3(0.0, 0.0, cos(tw)) - tg * sin(tw);
vec3 ctr = sp * RAD_MID;
float denom = dot(rd, nrm);
if (abs(denom) < 1e-4) continue;
float t = dot(ctr - ro, nrm) / denom;
if (t <= 0.05 || t >= bestT) continue;
vec3 P = ro + rd * t;
float u = dot(P - ctr, sp) / HALF_LEN;
float v = dot(P - ctr, w) / HALF_W;
if (abs(u) < 1.0 && abs(v) < 1.0) {
bestT = t; bU = u; bV = v; bPy = P.y; bAz = az; bN = nrm; hit = true;
}
}
/* ground: near-black, with a faint warm core where the blades converge */
float core = 1.0 - clamp(length(rd.xy) * 7.0, 0.0, 1.0);
vec3 bg = u_ground * 0.22 + lip(lightPhase + 0.15) * core * core * 0.5;
if (!hit) return bg;
float notInner = smoothstep(-1.0, -0.45, bU); /* fade edges at the hub end */
float longEdge = smoothstep(0.42, 1.0, abs(bV)) * notInner;
float tipEdge = smoothstep(0.74, 1.0, abs(bU));
float lipHi = (smoothstep(0.87, 0.999, abs(bV))
+ smoothstep(0.93, 0.999, abs(bU))) * notInner;
float fres = pow(1.0 - abs(dot(rd, bN)), 3.5);
float hue = bAz * (0.5 / PI) + lightPhase;
vec3 rim = lip(hue);
vec3 h = normalize(lightDir - rd);
float spec = pow(max(dot(bN, h), 0.0), 70.0);
/* the face: a near-black glossy plane, lit toward the top of the frame */
float faceLit = 0.28 + 0.72 * smoothstep(-1.4, 1.3, bPy);
vec3 col = u_ground * 0.34 * faceLit;
col += rim * 0.035 * faceLit;
col += rim * fres * (0.10 + glow * 0.30) * faceLit;
col += rim * (longEdge * 0.16 + tipEdge * 0.10);
col += rim * lipHi * (0.60 + glow * 0.70);
col += mix(rim, vec3(1.0), 0.40) * spec * 0.80 * faceLit;
col *= mix(1.0, 0.46, smoothstep(3.6, 6.6, bestT)); /* far blades recede */
return col;
}
void main() {
vec2 res = u_res / u_scale;
vec2 fc = gl_FragCoord.xy / u_scale;
float pres = u_pointer.z;
float yaw = (u_pointer.x - 0.5) * 0.50 * pres;
float pitch = 0.05 + (u_pointer.y - 0.5) * 0.22 * pres;
float roll = u_time * 0.012 + (u_pointer.x - 0.5) * 0.12 * pres;
/* key light: up and to the left by default; the cursor swings it around
the ring (x) and lifts or drops it (y) */
vec3 lightDir = normalize(vec3(-0.45, 0.62, 0.64));
lightDir = rotY(lightDir, (u_pointer.x - 0.5) * PI * (0.35 + 0.65 * pres));
lightDir = rotX(lightDir, (u_pointer.y - 0.5) * 0.55 * pres);
float lightPhase = u_time * 0.028 + (u_pointer.x - 0.5) * 0.5 * pres;
float glow = 0.14 + 0.86 * pres;
/* 2x2 supersample — the blade silhouettes are thin and bright against
black, so a single sample crawls badly along their edges */
vec3 acc = vec3(0.0);
for (int sx = 0; sx < 2; sx++) {
for (int sy = 0; sy < 2; sy++) {
vec2 o = (vec2(float(sx), float(sy)) + 0.25) * 0.5 - 0.5;
vec2 p = (fc + o - 0.5 * res) / res.y;
vec3 ro = vec3(0.0, 0.0, 6.6);
vec3 rd = normalize(vec3(p, -2.1));
ro = rotX(ro, pitch); rd = rotX(rd, pitch);
ro = rotY(ro, yaw); rd = rotY(rd, yaw);
acc += render(ro, rd, roll, lightDir, lightPhase, glow);
}
}
vec3 col = max(acc * 0.25, 0.0);
col += (bayer8(gl_FragCoord.xy) - 0.5) * (2.5 / 255.0);
gl_FragColor = vec4(col, 1.0);
}
`;
export interface ApertureFieldProps {
className?: string;
}
export function ApertureField({ className }: ApertureFieldProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
return mountShaderSurface(canvas, {
fragment: FRAG,
colors: {
u_rimA: "--shader-role-2-a",
u_rimB: "--shader-role-2-b",
u_rimC: "--shader-role-3-b",
u_rimD: "--shader-role-1-b",
u_ground: "--iris-bg-sunken",
},
onPainted: () => canvas.setAttribute("data-shader", "on"),
/* No onIdle: the last frame stays on screen while the surface is
parked off-view. onLost drops back to the CSS floor. */
onLost: () => canvas.removeAttribute("data-shader"),
maxPixels: 1_300_000,
dprCap: 1.5,
});
}, []);
return (
<div
className={`absolute inset-0 overflow-hidden${className ? ` ${className}` : ""}`}
aria-hidden="true"
>
<div className="absolute inset-0 [background:oklch(0.115_0.012_285)] before:content-[''] before:absolute before:[inset:50%_auto_auto_50%] before:[width:min(72vh,92%)] before:[aspect-ratio:1] before:[transform:translate(-50%,-50%)] before:[border-radius:50%] before:[background:repeating-conic-gradient(_from_-99deg_at_50%_50%,oklch(0.6_0.24_315_/_0.5)_0deg_1.6deg,transparent_1.6deg_18deg_),conic-gradient(_from_90deg_at_50%_50%,oklch(0.74_0.17_45_/_0.55)_0deg,oklch(0.6_0.24_315_/_0.55)_130deg,oklch(0.62_0.18_235_/_0.55)_215deg,oklch(0.74_0.17_45_/_0.55)_360deg_)] before:[background-blend-mode:screen] before:[-webkit-mask-image:radial-gradient(_circle_at_50%_50%,#000_6%,#000_88%,transparent_100%_)] before:[mask-image:radial-gradient(_circle_at_50%_50%,#000_6%,#000_88%,transparent_100%_)] after:content-[''] after:absolute after:inset-0 after:[background:radial-gradient(_34%_40%_at_50%_50%,oklch(0.55_0.19_40_/_0.35)_0%,transparent_70%_)]" />
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full opacity-0 transition-opacity duration-[--duration-slow] ease-[--ease-standard] data-[shader=on]:opacity-100" />
</div>
);
}Custom work
Need one that isn't in the catalogue?
Describe what you're after and I'll reply by email — no promise on turnaround yet, this is a new channel, not a service with a set price or queue.
Fardin Omor Afnan
Reads and answers every request himself