IrisBackgroundsCrease Field
Crease Field
A grid of flat glossy periwinkle panels with a thin rounded bevel at every seam, swept by one soft diagonal shadow the pointer can drag.
Crease Field
Built to one reference, colour-sampled from it rather than eyeballed: flat, uniform panel faces — no dome, no per-tile gradient — ringed by a thin rounded bevel that curves down to a sharp seam line between neighbours, catching a bright specular ridge on the way. One broad, soft diagonal band darkens the panel like a shadow sweeping across it, multiplicatively rather than toward a flat colour, which is what lets the already-darker seams crush to near-black inside the band while the flat tops merely dim — the same ratio the reference holds between its lit tile centres and its pinched seam crossings, all the way through the shadow's core.
The pointer is real: the shadow band bends toward the cursor, eased, as if it could be dragged by the hand hovering over it, and the panel nearest the cursor lifts and brightens slightly. With no pointer it idles on a slow organic wobble so a frozen frame still reads as a cast shadow, not a flat grid. Fixed periwinkle palette, not the token ramp — this hue lives nowhere in the site's own accent ramp, so the CSS floor carries the colour.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { CreaseField } from "./CreaseField";
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">
{/* Defaults to guarding null — pass a selector to protect copy laid
over the field. */}
<CreaseField guardSelector={null} />
</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 lattice of glossy periwinkle panels — flat, uniform tops (no
* dome, no per-tile gradient: the reference's tile faces really are flat)
* ringed by a thin, smoothly rounded bevel that curves down to a sharp seam
* line between neighbours, catching a bright specular ridge on the way. One
* broad, soft diagonal band darkens the panel like a shadow sweeping across
* it — pointer-driven, not fixed — and it darkens multiplicatively rather
* than mixing toward a flat colour, which is what makes the already-darker
* seams crush to near-black inside the band while the flat tops merely dim:
* the same ratio the reference holds between its lit tile centres and its
* pinched seam crossings, all the way through the shadow's core.
*
* Every number here — the flat top colour, the groove's resting darkness,
* the shadow's core colour, the seam's proportion of the tile — was sampled
* directly from the reference photograph rather than eyeballed.
*
* The bevel is real geometry, not a texture: a rounded-box signed distance
* per tile drives a height field, finite-differenced into an actual normal
* (the field has no closed form worth hand-deriving), fed through a small
* Blinn-Phong light. That's what gives the seam its bright catch-ridge
* before it drops into the dark line, rather than a flat painted gradient.
*
* The pointer is real: the shadow band bends toward the cursor, eased, as
* if it could be dragged by the hand hovering over it, and the surface
* nearest the cursor lifts and brightens slightly. With no pointer the band
* idles on a slow organic wobble so a frozen frame still reads as a cast
* shadow, not a flat grid.
*
* 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-creasefield__floor`
* underneath visible (a still composed frame in the same palette, never a
* blank box). The one frame a reduced-motion visitor gets is drawn by the
* surface at a fixed time with the pointer withdrawn.
*
* Like `TileField` and `SlabField`, the palette is a fixed periwinkle set
* passed as uniforms rather than read from the token ramp — these hues live
* nowhere in the token set, so the CSS floor is the degradation fallback
* instead of a live token read.
*
* Reading guard: when `guardSelector` resolves to an element, the field
* measures that block every frame and clamps its own luminance under a
* ceiling inside that region (hue and saturation untouched), so copy laid
* over the field holds WCAG AA with no overlay layer. `null` (the default)
* turns the guard off — for decorative use where nothing sits on top.
*/
/* Palette, sRGB 0–1 — sampled from the reference image, not eyeballed. */
const PALETTE: Record<string, [number, number, number]> = {
u_top: [0.745, 0.835, 0.992],
u_groove: [0.357, 0.42, 0.737],
u_valley: [0.075, 0.098, 0.208],
u_ice: [0.95, 0.97, 1.0],
};
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_top; /* the flat panel face */
uniform vec3 u_groove; /* the seam's resting darkness, away from the shadow */
uniform vec3 u_valley; /* the shadow band's own dark core */
uniform vec3 u_ice; /* near-white — the bevel's specular catch-ridge */
/* Reading region for the contrast guard: xy = centre (uv, y up),
zw = half-extent. Measured from the live copy block every frame.
u_guard is 0 when the guard is off. */
uniform vec4 u_readA;
uniform float u_guard;
/* Tiles per unit of centred, aspect-corrected space. One line to retune
the density of the lattice. */
const float GRID = 7.0;
const float HALFPI = 1.5707963;
/* Signed distance to a rounded box, zero on the edge, negative inside. */
float sdRoundBox(vec2 p, vec2 b, float r) {
vec2 d = abs(p) - b + r;
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0)) - r;
}
/* A flat plateau (most of the tile) ringed by a thin rounded bevel that
curves smoothly down to zero exactly at the shared edge -- the panel's
real proportions, measured off the reference: the transition band is a
little under a tenth of the tile's half-width, not a wide facet covering
the tile. heightAt() is finite-differenced below rather than
hand-derived, since the SDF's gradient isn't worth carrying by hand. */
const float BEVEL_W = 0.075;
const float CORNER_R = 0.06;
float heightAt(vec2 gc) {
vec2 fp = fract(gc) - 0.5;
float d = sdRoundBox(fp, vec2(0.5 - BEVEL_W), CORNER_R);
float t = clamp(d / BEVEL_W, 0.0, 1.0);
return cos(t * HALFPI);
}
/* The shadow's own axis: a diagonal running down-right, matching the
reference. v0() is the band's perpendicular position along that axis —
a rest wobble, pulled toward wherever the pointer sits nearby. */
const vec2 FOLD_DIR = vec2(0.83205, -0.55470);
const vec2 FOLD_PERP = vec2(0.55470, 0.83205);
float bandV0(float u, float pu, float pv, float pull, float t) {
float restV = -0.02 + 0.06 * sin(t * 0.06);
float near = exp(-pow(u - pu, 2.0) * 2.0);
float v0 = mix(restV, pv, pull * near);
/* a gentle organic wobble so the band isn't a ruler-straight line */
v0 += 0.05 * sin(u * 3.4 + t * 0.07) + 0.02 * sin(u * 8.0 - t * 0.05);
return v0;
}
void main() {
vec2 res = u_res / u_scale;
vec2 uv = gl_FragCoord.xy / u_scale / res; /* 0..1, y up */
float aspect = res.x / res.y;
vec2 P = (uv - 0.5) * vec2(aspect, 1.0); /* centred, square units */
vec2 ptrP = (u_pointer.xy - 0.5) * vec2(aspect, 1.0);
float pull = u_pointer.z;
/* ---- the shadow: a broad, soft diagonal band, dragged by the pointer -- */
float pu = dot(ptrP, FOLD_DIR);
float pv = dot(ptrP, FOLD_PERP);
float u = dot(P, FOLD_DIR);
float v = dot(P, FOLD_PERP);
float v0 = bandV0(u, pu, pv, pull, u_time);
float bandW = 0.125;
float shadow = exp(-pow((v - v0) / bandW, 2.0)) * smoothstep(-1.05, -0.25, u);
shadow = clamp(shadow, 0.0, 1.0);
/* ---- the tile height field and its normal, via a one-pixel finite
difference -------------------------------------------------------- */
vec2 gc = P * GRID;
float h0 = heightAt(gc);
float eps = 1.4 / res.y * GRID;
float hx = heightAt(gc + vec2(eps, 0.0));
float hy = heightAt(gc + vec2(0.0, eps));
float dhx = (hx - h0) / eps;
float dhy = (hy - h0) / eps;
float bump = 1.35;
vec3 N = normalize(vec3(-dhx * bump, -dhy * bump, 1.0));
vec3 L = normalize(vec3(-0.42, 0.58, 0.62));
vec3 V = vec3(0.0, 0.0, 1.0);
vec3 Hn = normalize(L + V);
float diff = dot(N, L) * 0.5 + 0.5; /* wrapped diffuse -- never fully black on its own */
float spec = pow(max(dot(N, Hn), 0.0), 45.0); /* the seam's bright catch-ridge */
float spec2 = pow(max(dot(N, Hn), 0.0), 6.0) * 0.15;
vec3 col = mix(u_groove, u_top, clamp(diff * 1.05, 0.0, 1.0));
col += u_ice * spec * 1.1;
col += u_ice * spec2;
float hover = u_pointer.z * exp(-dot(P - ptrP, P - ptrP) * 9.0);
col += col * hover * 0.12;
/* the shadow darkens multiplicatively, not toward a flat target colour --
that keeps the ratio between the already-bright tops and the
already-dark groove intact, so the groove crushes to near-black while
the tops merely dim, the way the reference's tile centres still show
through even at the shadow's core. */
col *= mix(1.0, 0.16, shadow);
col += u_valley * shadow * 0.10;
/* ---- the reading guard ------------------------------------------------
See TileField for the full rationale: where the copy sits, pull the
field's luminance under a ceiling so text holds WCAG AA. holdUnder()
clamps brightness in linear light and leaves hue and saturation alone —
the field dims itself, there is no overlay layer. */
vec2 rd = abs(uv - u_readA.xy) / max(u_readA.zw, vec2(0.02));
float m = mix(max(rd.x, rd.y), length(rd), 0.4);
float band = 1.0 - smoothstep(0.72, 2.1, m);
band = max(band, smoothstep(0.8, 1.0, uv.y) * 0.85);
col = mix(col, holdUnder(col, 0.09), band * u_guard);
col += (bayer8(gl_FragCoord.xy) - 0.5) * (2.0 / 255.0);
gl_FragColor = vec4(max(col, 0.0), 1.0);
}
`;
export interface CreaseFieldProps {
className?: string;
/**
* CSS selector for the block the reading guard should keep readable,
* resolved within the nearest `.iris-hero` (falling back to the
* document). `null` (the default) turns the guard off.
*/
guardSelector?: string | null;
}
export function CreaseField({ className, guardSelector = null }: CreaseFieldProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const guardOn = guardSelector != null;
/* The block the reading guard protects. Looked up once, lazily. */
let guardEl: Element | null | undefined;
const readGuardEl = () => {
if (guardEl === undefined) {
guardEl = guardOn
? (canvas.closest(".iris-hero") ?? document).querySelector(
guardSelector as string
)
: null;
}
return guardEl;
};
return mountShaderSurface(canvas, {
fragment: FRAG,
uniforms: [...Object.keys(PALETTE), "u_readA", "u_guard"],
onInit: (gl, u) => {
for (const name of Object.keys(PALETTE)) {
if (u[name]) gl.uniform3fv(u[name], PALETTE[name]);
}
if (u.u_readA) gl.uniform4f(u.u_readA, 0.5, 0.5, 0.44, 0.32);
if (u.u_guard) gl.uniform1f(u.u_guard, guardOn ? 1 : 0);
},
onFrame: (gl, u, s) => {
if (!guardOn || !u.u_readA) return;
let cx = 0.5, cy = 0.5, hw = 0.44, hh = 0.32;
const el = readGuardEl();
const { rect } = s;
if (el && rect.width > 0 && rect.height > 0) {
const r = el.getBoundingClientRect();
const padX = rect.width * 0.09;
const padY = rect.height * 0.11;
cx = (r.left + r.width / 2 - rect.left) / rect.width;
cy = 1 - (r.top + r.height / 2 - rect.top) / rect.height;
hw = (r.width / 2 + padX) / rect.width;
hh = (r.height / 2 + padY) / rect.height;
}
gl.uniform4f(u.u_readA, cx, cy, hw, hh);
},
onPainted: () => canvas.setAttribute("data-shader", "on"),
onLost: () => canvas.removeAttribute("data-shader"),
maxPixels: 1_900_000,
dprCap: 1.5,
});
}, [guardSelector]);
return (
<div
className={`absolute inset-0 overflow-hidden${className ? ` ${className}` : ""}`}
aria-hidden="true"
>
<div className="absolute inset-0 [background:radial-gradient(_120%_100%_at_10%_8%,oklch(0.78_0.05_270)_0%,transparent_55%_),radial-gradient(_120%_110%_at_92%_88%,oklch(0.30_0.05_270)_0%,transparent_58%_),radial-gradient(_70%_60%_at_50%_55%,oklch(0.20_0.03_270)_0%,transparent_74%_),oklch(0.60_0.045_270)] after:content-[''] after:absolute after:inset-0 after:[background:repeating-linear-gradient(_0deg,transparent_0_92px,oklch(0.10_0.04_270_/_0.6)_92px_98px_),repeating-linear-gradient(_90deg,transparent_0_92px,oklch(0.10_0.04_270_/_0.6)_92px_98px_)]" />
<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