IrisBackgroundsGauze Veil
Gauze Veil
Backlit gauze in soft umber and amber, blurred rather than banded, with one prismatic sliver caught in its right-hand edge.
Gauze Veil
Wide fold bands, flattened into soft plateaus rather than sharp bars, stand in for a curtain photographed out of focus — nothing here gets a hard edge. Two paler columns wander down the field as the gaps true light gets through, and one more saturated gold streak tapers wide at the bottom of the frame and narrow toward the top, the way a single fold catches more light than the rest.
The right edge falls into a dark maroon border, and inside it sits one thin prismatic line — the fabric's edge acting, for a hairline's width, like a lens. The pointer parts the gauze itself, bowing the fold field away from the cursor and leaving a soft warm bloom where it touched, and brightens the fringe as it passes near — the one place the field answers the light source moving past it.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { GauzeField } from "./GauzeField";
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">
<GauzeField />
</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 field of backlit gauze — soft vertical fold bands in warm
* umber and amber, heavily softened rather than sharply banded, the way a
* curtain photographed out of focus loses every hard edge. Two paler
* columns stand in for the gaps true light gets through, and one more
* saturated gold streak tapers wide at the bottom of the frame and narrow
* toward the top, the way a single fold catches more light than the rest.
*
* The right edge falls into a dark maroon border, and inside it sits one
* thin prismatic sliver — the sheer fabric's edge acting, for one hairline's
* width, like a lens. It brightens on its own slow drift and again when the
* pointer comes near, the way a real fringe catches more colour as the
* light source (here, the cursor) moves past it.
*
* Interactive: the pointer parts the gauze — the fold field nearest the
* cursor bows away from it as if brushed aside by hand — and leaves a soft
* warm bloom where it touched. No pointer, and the folds settle back into
* their own slow drift.
*
* One of the reusable background fields (`SilkField`, `ShaftField`,
* `MeshFluidField`, …). 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-gauzefield__floor` underneath visible.
*
* Like `EmberField` and `ShaftField`, the palette is a fixed set of
* uniforms rather than read from the accent ramp — this particular warm-to-
* maroon mood, with its rainbow fringe, is its own thing, not the
* portfolio's amber.
*
* 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). `null` (the
* default) turns the guard off — for decorative use where nothing sits on
* top.
*/
/* Palette, sRGB 0–1. Uniforms, not tokens — see the note above. */
const PALETTE: Record<string, [number, number, number]> = {
u_ground: [0.2, 0.12, 0.06], // the shadow between folds
u_shade: [0.46, 0.28, 0.13], // the body of a fold
u_gold: [0.93, 0.63, 0.2], // the one saturated streak
u_cream: [0.98, 0.9, 0.74], // the backlit gaps, and the pointer's bloom
u_edge: [0.3, 0.07, 0.05], // the right-hand border, falling into shadow
};
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_ground;
uniform vec3 u_shade;
uniform vec3 u_gold;
uniform vec3 u_cream;
uniform vec3 u_edge;
uniform vec4 u_readA;
uniform float u_guard;
const float FOLDS = 4.2;
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);
vec2 ptr = (u_pointer.xy - 0.5) * vec2(aspect, 1.0);
vec2 toP = P - ptr;
float dP = length(toP);
float pull = u_pointer.z * exp(-dP * dP * 8.0);
/* the touch parts the gauze: the field nearest the cursor bows away from
it, as if a hand had brushed the sheer fabric aside */
vec2 wp = P - toP * pull * 0.8;
/* a heavy, low-frequency warp stands in for the photograph's blur —
everything here stays soft, nothing gets a hard edge */
float warp = fbm(vec2(wp.x * 0.85, wp.y * 0.5 + u_time * 0.015)) * 0.4;
wp.x += warp;
/* the fold bands: a wide sine flattened by a low power into broad
plateaus, so the seams between folds stay soft rather than banding */
float phase = wp.x * FOLDS + u_time * 0.02;
float band = sin(phase * 3.14159265);
float soft = sign(band) * pow(abs(band), 0.32);
vec3 col = mix(u_ground, u_shade, 0.5 + 0.5 * soft);
/* two backlit columns — the gaps a curtain lets true light through, each
wandering a little on its own so neither reads as a ruled line, spaced
well apart so their glow never stacks into one blown-out mass */
float c1 = wp.x - (-0.36 + 0.05 * sin(wp.y * 1.1 + 0.6));
float c2 = wp.x - ( 0.30 + 0.04 * sin(wp.y * 0.9 + 2.4));
col += u_cream * exp(-c1 * c1 * 230.0) * 0.6;
col += u_cream * exp(-c2 * c2 * 260.0) * 0.5;
/* the one gold streak, tapering wide at the bottom of the frame and
narrow toward the top, sitting between the two columns without
touching either */
float gx = wp.x - (-0.03 - wp.y * 0.12);
float gWidth = mix(70.0, 260.0, smoothstep(-0.5, 0.5, wp.y));
col += u_gold * exp(-gx * gx * gWidth) * 0.6;
/* the pointer's own soft bloom, where the fabric is being touched */
col += u_cream * pull * 0.35;
/* the right-hand border: the frame falls into shadow, with one thin
prismatic sliver caught in it, brightening on its own slow drift and
again as the pointer passes near it */
float edgeMask = smoothstep(0.80, 1.0, uv.x);
col = mix(col, u_edge, edgeMask * 0.8);
float lineX = 0.93 + 0.006 * sin(u_time * 0.12);
float dxP = (uv.x - lineX) * aspect;
float halfWidth = 0.004;
float glintCore = exp(-dxP * dxP / (halfWidth * halfWidth));
float envelope = exp(-pow((uv.y - 0.62) / 0.18, 2.0));
float glintStrength = envelope * (0.55 + 0.55 * pull + 0.3 * u_pointer.z);
vec3 rainbow = 0.5 + 0.5 * cos(6.2831853 * (vec3(0.0, 0.33, 0.67) + dxP * 60.0 + u_time * 0.05));
col = mix(col, rainbow, clamp(glintCore * glintStrength, 0.0, 1.0));
col = max(col, 0.0);
/* ---- the reading guard (see TileField for the full rationale) ---- */
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 band2 = 1.0 - smoothstep(0.72, 2.1, m);
col = mix(col, holdUnder(col, 0.09), band2 * u_guard);
col += (bayer8(gl_FragCoord.xy) - 0.5) * (2.2 / 255.0);
gl_FragColor = vec4(col, 1.0);
}
`;
export interface GauzeFieldProps {
className?: string;
/**
* CSS selector for the block the reading guard should keep readable,
* resolved against `document`. `null` (the default) turns the guard off.
*/
guardSelector?: string | null;
}
export function GauzeField({ className, guardSelector = null }: GauzeFieldProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const guardOn = guardSelector != null;
let guardEl: Element | null | undefined;
const readGuardEl = () => {
if (guardEl === undefined) {
guardEl = guardOn ? 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"),
/* No onIdle — same contract as SilkField / EmberField: once painted,
the last frame stays on screen while the surface is parked
off-view, so scrolling away and back does not crossfade to the
static floor. A lost context still clears it below. */
onLost: () => canvas.removeAttribute("data-shader"),
maxPixels: 2_200_000,
dprCap: 1.5,
});
}, [guardSelector]);
return (
<div
className={`absolute inset-0 overflow-hidden${className ? ` ${className}` : ""}`}
aria-hidden="true"
>
<div className="absolute inset-0 [background:linear-gradient(_100deg,transparent_0%,transparent_28%,oklch(0.93_0.05_85_/_0.55)_33%,transparent_40%_),linear-gradient(_100deg,transparent_0%,transparent_50%,oklch(0.88_0.09_75_/_0.45)_55%,transparent_62%_),linear-gradient(_100deg,transparent_0%,transparent_76%,oklch(0.72_0.16_55_/_0.55)_79%,transparent_85%_),linear-gradient(_96deg,transparent_0%,transparent_91%,oklch(0.85_0.18_95_/_0.5)_93.5%,transparent_95.5%_),linear-gradient(_90deg,transparent_0%,transparent_88%,oklch(0.26_0.08_25_/_0.92)_95%,oklch(0.22_0.07_20)_100%_),linear-gradient(_180deg,oklch(0.5_0.09_55)_0%,oklch(0.3_0.07_40)_100%_)]" />
<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