IrisBackgroundsSilk Coil
Silk Coil
Silk Drape wound rather than hung — the same folds ride a helical phase around a column, with a fake-round rim shading to sell it.
Silk Coil
A variation on `silk-drape` that swaps hung fabric for wound fabric: the same fold math rides a helical phase instead of a vertical one, so the bands wind around a column as height increases rather than hanging straight down — a twisted silk rope filling the frame rather than a curtain. A fake-cylindrical rim shading, brighter at the centre and darker toward the left/right edges, sells the roundness the helix alone doesn't.
The pointer's contract is grip, not parting: its x leans the whole column sideways and speeds the twist's travel, the way pulling a held rope skews its axis and spins it faster; its y drops a bright cinch ring at that height, as if a hand had closed around the coil there.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { SilkCoilField } from "./SilkCoilField";
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">
<SilkCoilField />
</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 that swaps hung
* fabric for wound fabric: the same fold math rides a helical phase instead
* of a purely vertical one, so the bands wind around a column as height
* increases rather than hanging straight down — a twisted silk rope filling
* the frame rather than a curtain. A fake-cylindrical rim shading (brighter
* at the centre, darker toward the left/right edges) sells the roundness the
* helix alone doesn't.
*
* The pointer's contract is grip, not parting: its x leans the whole column
* sideways and speeds the twist's travel, the way pulling a held rope skews
* its axis and spins it faster; its y drops a bright cinch ring at that
* height, as if a hand were gripping the coil there.
*
* 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-silkcoilfield__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 = 5.0;
const float THREADS = 18.0;
const float TWIST = 2.6; /* how sharply the bands wind per unit height */
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);
/* grip: the pointer's x leans the column's axis, its presence speeds the
twist's travel — the two things gripping and pulling a wound rope
actually does, as distinct from silk-drape's parting pull. */
float lean = (u_pointer.x - 0.5) * u_pointer.z * 0.35;
float speed = 1.0 + u_pointer.z * 0.9;
vec2 wp = vec2(P.x - lean, P.y);
float warp = fbm(vec2(wp.x * 1.3 + wp.y * 0.6, wp.y * 0.7 + u_time * 0.02)) * 0.26;
float twist = wp.y * TWIST + u_time * 0.05 * speed;
float phase = wp.x * FOLDS + twist + warp;
float coarse = sin(phase * 3.14159265);
float fine = sin((wp.x * THREADS + twist * 1.6 + warp * 1.6) * 3.14159265);
float drape = clamp(coarse * 0.7 + fine * 0.3, -1.0, 1.0);
drape = sign(drape) * pow(abs(drape), 0.82);
/* fake cylindrical shading: brighter at the column's own centre, darker
toward its left/right edges — what reads as "round" rather than "flat"
under the fold pattern riding on top of it */
float rim = 1.0 - pow(clamp(abs(uv.x - 0.5) * 2.0, 0.0, 1.0), 1.6);
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.5 + 0.55 * (0.5 + 0.5 * drape)) * mix(0.55, 1.08, rim);
/* the cinch: a bright ring at the pointer's own height, as if a grip had
closed around the coil there */
float cinch = u_pointer.z * exp(-pow((uv.y - u_pointer.y) * 3.4, 2.0));
col += u_pale * cinch * mix(0.15, 0.5, rim) * (0.4 + 0.6 * (0.5 + 0.5 * drape));
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 SilkCoilFieldProps {
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 SilkCoilField({ className, guardSelector = null }: SilkCoilFieldProps) {
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(_70%_100%_at_50%_50%,transparent_40%,oklch(0_0_0_/_0.45)_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