Skip to content

IrisBackgroundsSilk Drape

Silk Drape

Backlit silk in one warm amber gradient, its folds parting toward the pointer.

drape

Silk Drape

Soft vertical drapery lit from behind, in one amber gradient — deep umber at the top thinning to pale gold at the base. The palette is the site's own accent ramp, read live, so a theme change or context restore picks it up automatically.

The pointer parts the folds: the drapery nearest the cursor bends toward it and catches a soft bloom, as if the fabric were touched from behind.

  • warm
  • fabric
  • token-palette
Family
drape
Status
Available
Licence
Free

Install

No installation needed — self-contained, paste-in code.

Usage

Drop it straight into a page.

example.tsx
import { SilkField } from "./SilkField";

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">
      <SilkField />
    </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 soft vertical drapery — backlit silk in one warm
 * amber gradient, deep umber at the top thinning to pale gold at the base.
 * The pointer parts the folds: the drapery nearest the cursor bends toward
 * it and catches a soft bloom, as if the fabric were being touched from
 * behind.
 *
 * 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-silkfield__floor` underneath visible.
 *
 * Unlike `TileField`, the palette here IS the site's amber accent ramp
 * (`--color-accent-950` … `--color-accent-300`, read live via `colors` so a
 * theme change or context restore picks it up automatically) — the "same
 * colour" the reference asked for is the portfolio's own amber, not a new
 * fixed one.
 *
 * Reading guard: same mechanism as `TileField` — when `guardSelector`
 * resolves to an element, the field measures it every frame and clamps its
 * own luminance under a ceiling in that region (hue/saturation untouched).
 * Off by default (`null`) because this field, unlike the hero's, is meant to
 * sit behind anything — pass a selector where copy overlaps it.
 */
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;

/* Folds across the width, at two scales — a slow, wide sway and a finer
   thread count riding on top of it. What reads as silk rather than
   stripes is the second scale plus the domain warp below. */
const float FOLDS   = 6.0;
const float THREADS = 22.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);

  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 * 50.0);

  /* the touch parts the drapery: folds near the cursor bend toward it,
     as if the fabric were being pulled from behind the cursor */
  vec2 wp = P - toP * pull * 1.1;

  /* a slow, low-amplitude warp gives the folds their waviness without
     smearing the banding away — its own spatial frequency stays well
     below the fold frequency it perturbs */
  float warp = fbm(vec2(wp.x * 1.4, wp.y * 0.8 + u_time * 0.02)) * 0.30;
  float phase = wp.x * FOLDS + warp + u_time * 0.035;

  float coarse = sin(phase * 3.14159265);
  float fine   = sin((wp.x * THREADS + 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);         /* widen the bright peaks a touch */

  /* ---- the vertical gradient, warm at the top, pale at the base ---- */
  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));

  /* the touch: a soft warm bloom, and the nearest folds catch extra light */
  col += u_pale * pull * 0.42;
  col += base   * pull * (0.5 + 0.5 * drape) * 0.5;

  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 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 SilkFieldProps {
  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 SilkField({ className, guardSelector = null }: SilkFieldProps) {
  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: once the field has painted once, its last frame stays on
         screen while the surface is parked off-view. Removing data-shader
         here (as TileField once did) crossfades the canvas back to the
         static __floor gradient on every scroll-away and then fades the live
         field back in over 600ms on return — the "static then shader" flash.
         The floor is a fallback for no-WebGL / lost-context / reduced-motion
         only; 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(_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>
  );
}

More backgrounds

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

Fardin Omor Afnan

Reads and answers every request himself

Or start with a section.

Whole page sections, composed and ready to drop in — take one and build the rest of the page around it.

Browse sections