IrisBackgroundsSilk Pool
Silk Pool
Silk Drape that reaches the floor and gathers — the folds tighten and gather into a heap, and the pointer stirs ripples through it instead of parting it.
Silk Pool
A variation on `silk-drape`: the same backlit amber drapery up top, but it doesn't hang straight to the floor — the fold frequency tightens the lower it reaches, and near the base the vertical bands give way to a radial fan of folds converging on a cinch point, the way real fabric heaps rather than hangs once it reaches the ground. The heap's troughs carry their own contact shadow, darker than anything the hung portion above ever gets.
Two pointer zones rather than `silk-drape`'s one: above the pool, the cursor parts the hung folds exactly as it always has; inside the pool, that same touch instead sends a ring rippling out across the heap, the way pressing a finger into piled fabric disturbs it outward rather than bending it toward you.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { SilkPoolField } from "./SilkPoolField";
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">
<SilkPoolField />
</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 variation on `silk-drape`'s drape family: the same backlit
* amber drapery up top, but it doesn't hang straight to the floor — the
* lower reach gathers, the fold frequency tightening as `uv.y` falls, and
* near the very base the vertical bands give way to a radial fan of folds
* converging on a cinch point, the way real fabric heaps rather than hangs
* once it reaches the ground. The heap's troughs carry their own contact
* shadow, darker than anything the hung portion above ever gets.
*
* Two pointer zones rather than `silk-drape`'s one: above the pool, the
* cursor parts the hung folds exactly as `SilkField` does; inside the pool,
* that same touch instead sends a ring rippling out across the heap, the
* way pressing a finger into piled fabric disturbs it outward rather than
* bending it toward you.
*
* One of the reusable background fields, a sibling of `SilkField`,
* `AuroraVeil` and `QuiltField` in the same fabric family. Drop it into any
* `position: relative`/`isolate` parent — it fills the box. Built on
* `lib/shader-surface.ts`, so degradation is already handled: no WebGL, a
* blocked or lost context, a hidden tab, or `prefers-reduced-motion` all
* leave the CSS `.iris-silkpoolfield__floor` underneath visible.
*
* Same reasoning as `SilkField` for the palette: it IS the site's amber
* accent ramp, read live via `colors`, not a fixed departure from it.
*
* Reading guard: identical mechanism to `SilkField` — off by default
* (`null`), on when `guardSelector` resolves to an element.
*/
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_deep; /* --color-accent-950, the top of the field */
uniform vec3 u_umber; /* --color-accent-800, upper-mid */
uniform vec3 u_gold; /* --color-accent-600, lower-mid */
uniform vec3 u_pale; /* --color-accent-300, the base */
uniform vec4 u_readA;
uniform float u_guard;
const float FOLDS = 6.0;
const float THREADS = 22.0;
const float POOL_Y = 0.34; /* uv.y below which the heap dominates */
const float GATHER_FOLDS = 10.0;
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);
/* how far into the heap this fragment sits — 0 up in the hung drapery,
1 down in the pooled fabric at the floor */
float poolWeight = 1.0 - smoothstep(0.0, POOL_Y, uv.y);
vec2 ptr = (u_pointer.xy - 0.5) * vec2(aspect, 1.0);
vec2 toP = P - ptr;
float dP = length(toP);
/* the parting pull only belongs to the hung portion — it fades out as
the fragment sinks into the pool, where the touch works differently */
float pull = u_pointer.z * exp(-dP * dP * 50.0) * (1.0 - poolWeight);
vec2 wp = P - toP * pull * 1.1;
/* fold density tightens toward the floor — the same length of fabric
reads as compressed into less height, a heap rather than a curtain */
float densify = mix(1.0, 2.2, poolWeight);
float warp = fbm(vec2(wp.x * 1.4, wp.y * 0.8 + u_time * 0.02)) * 0.30;
float phase = wp.x * FOLDS * densify + warp + u_time * 0.035;
float coarse = sin(phase * 3.14159265);
float fine = sin((wp.x * THREADS * densify + warp * 1.6) * 3.14159265);
float drape = clamp(coarse * 0.72 + fine * 0.28, -1.0, 1.0);
drape = sign(drape) * pow(abs(drape), 0.82);
/* the gather: a radial fan of folds converging on a cinch point at the
floor's centre, blended in only where the pool actually is */
vec2 toCinch = vec2(P.x, uv.y);
float rCinch = length(toCinch);
float aCinch = atan(toCinch.y, toCinch.x);
float gatherPhase = aCinch * GATHER_FOLDS - rCinch * 10.0 + u_time * 0.06;
float gather = sin(gatherPhase * 3.14159265);
float gatherMix = poolWeight * (1.0 - smoothstep(0.0, 0.55, rCinch));
drape = mix(drape, gather, gatherMix * 0.7);
vec3 base = u_pale;
base = mix(base, u_gold, smoothstep(0.10, 0.52, uv.y));
base = mix(base, u_umber, smoothstep(0.42, 0.80, uv.y));
base = mix(base, u_deep, smoothstep(0.70, 1.05, uv.y));
vec3 col = base * (0.58 + 0.62 * (0.5 + 0.5 * drape));
/* crevice shadow — the heap's troughs read darker than the hung
drapery's ever do */
float crevice = pow(clamp(-drape, 0.0, 1.0), 1.4) * poolWeight;
col *= (1.0 - crevice * 0.5);
col += u_pale * pull * 0.42;
col += base * pull * (0.5 + 0.5 * drape) * 0.5;
/* the pool's own touch: an expanding ripple rather than a directional
bend, the way pressing into piled fabric disturbs it outward */
float distPtr = length(P - ptr);
float ripple = sin(distPtr * 26.0 - u_time * 3.4) * exp(-distPtr * 5.0);
col += u_pale * u_pointer.z * poolWeight * max(ripple, 0.0) * 0.4;
col = max(col, 0.0);
/* ---- the reading guard (see SilkField 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 band = 1.0 - smoothstep(0.72, 2.1, m);
col = mix(col, holdUnder(col, 0.09), band * u_guard);
col += (bayer8(gl_FragCoord.xy) - 0.5) * (2.2 / 255.0);
gl_FragColor = vec4(col, 1.0);
}
`;
export interface SilkPoolFieldProps {
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 SilkPoolField({ className, guardSelector = null }: SilkPoolFieldProps) {
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,
colors: {
u_deep: "--color-accent-950",
u_umber: "--color-accent-800",
u_gold: "--color-accent-600",
u_pale: "--color-accent-300",
},
uniforms: ["u_readA", "u_guard"],
onInit: (gl, u) => {
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: once painted, the last
frame stays on screen while the surface is parked off-view. */
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:radial-gradient(_60%_38%_at_50%_6%,oklch(0_0_0_/_0.4)_0%,transparent_100%_),linear-gradient(_180deg,var(--color-accent-950)_0%,var(--color-accent-800)_42%,var(--color-accent-600)_72%,var(--color-accent-300)_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