Skip to content

IrisBackgroundsGargantua

Gargantua

A small black horizon embedded in a disk far larger than itself: an actual ray-marched lens, not a drawn ring — icy blue-white near the hole cooling outward on a blackbody curve, a wide near-edge-on band spanning almost the full frame, its far side bent into a broad arc over the top.

gravity

Gargantua

Unlike `event-horizon`'s tilted saucer or `singularity`'s hourglass, this one is an actual (heavily simplified) ray marcher rather than a shape drawn in screen space: every pixel casts a 3D ray from a camera sitting almost edge-on to a thin world-space disk, the ray bends toward the hole step by step under a stabilised pull, and each step is tested for an exact crossing of the disk's own plane — a sign change in local height, resolved to the precise crossing point rather than guessed from a density field. The near side shows up because a nearly-straight ray crosses it directly, in front of the horizon, spanning almost the full width; the far side shows up bent above the silhouette because a ray aimed just over the hole curves enough to cross the plane a second time, behind it, before reaching the camera — one loop, alpha-composited front to back over each crossing, not two pasted shapes. A ray that dips inside the horizon is simply captured, flat black; a ray that survives carries its own closest approach, which alone drives the photon ring. Colour runs on an actual temperature curve — hot blue-white near the hole cooling toward orange on a power law, converted through a blackbody approximation, with one sector of the same disk cooling further into the rust-brown patch low on one side — brightened or dimmed by a Doppler-style term reading the local orbital velocity's own alignment with the ray reaching the camera, and textured by turbulence sampled on a seamless cylinder, rotating on two time-shifted phases so the loop never pops.

Every motion here is autonomous — the turbulence's own rotation, the ring's shimmer, the stars' twinkle — the camera and the hole itself never move, and there is no pointer interaction, same reasoning as `event-horizon` and `singularity`.

  • cool
  • no-pointer
  • gravity
  • halo
Family
gravity
Status
Available
Licence
Free

Install

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

Usage

Drop it straight into a page.

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

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">
      <GargantuaField />
    </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 black hole: an actual (heavily simplified) ray marcher, not
 * a shape drawn in screen space. Every pixel casts a 3D ray from a camera
 * sitting almost edge-on to a thin world-space disk; the ray's own
 * direction bends toward the hole step by step under a stabilised
 * inverse-square pull; and each step is tested for an exact crossing of
 * the disk's own plane (a sign change in local height, resolved by linear
 * interpolation to the precise crossing point, not a continuous volumetric
 * guess). The near side of the disk is visible because a nearly-straight
 * ray crosses it directly, in front of the hole; the far side appears bent
 * up and over the silhouette because a ray aimed just above the hole
 * curves enough to cross the plane a second time, behind it, before ever
 * reaching the camera. One accumulation loop, front-to-back alpha
 * composited over each crossing — nothing marks "near band" or "lensed
 * arc" as separate shapes.
 *
 * The shading model at each crossing is adapted from a reference
 * implementation built on an actual physical disk model rather than an art
 * -directed gradient: temperature runs hot (blue-white) near the inner
 * edge and cools (toward orange) outward on a power-law falloff, converted
 * to colour through a blackbody approximation; brightness carries a
 * relativistic-style Doppler term — the local orbital velocity's own
 * alignment with the ray direction reaching the camera, not a flat
 * per-side multiplier; and the turbulent texture is a 3D fbm sampled on a
 * cylinder (radius, plus the angle's own cosine/sine) so it wraps with no
 * seam, blended between two time-shifted phases a full rotation cycle
 * apart so the loop is seamless rather than popping.
 *
 * A ray that dips inside the horizon is captured — flat black, nothing
 * further. A ray that survives carries `minR`, its closest approach along
 * the whole bent path; that alone drives the photon ring (this shader has
 * no separate bloom pass to lean on, unlike the reference), so the ring is
 * a consequence of the same integration rather than a second,
 * independently drawn circle.
 *
 * Every motion here is autonomous — the turbulence's own rotation, the
 * ring's shimmer, the stars' twinkle — the camera and the hole itself
 * never move, and there is no pointer interaction: `u_pointer` isn't read
 * by this shader, matching `EventHorizonField` / `SingularityField`.
 *
 * The composition is exposed as uniforms rather than baked constants —
 * `u_blackHoleRadius`, `u_diskTemperature`, `u_lensStrength` and the rest
 * below, all in the ray marcher's own world-space units — because this one
 * is tuned to a specific photo rather than an open silhouette, and the
 * shape most likely to need a nudge later is exactly the shape this file
 * can't fully see from code alone.
 *
 * One of the reusable background fields. 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-gargantuafield__floor` underneath visible — a
 * still frame in the same palette, never a blank box.
 *
 * 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 — the disk's own colour comes
   mostly from its blackbody temperature (see PARAMS below), these are
   tints and accents layered on top: a mild overall colour-grade on the
   disk, the rust patch's own hue, the photon ring / bloom colour, and
   deep space. */
const PALETTE: Record<string, [number, number, number]> = {
  u_ground: [0.004, 0.005, 0.012], // near-black, cool deep space
  u_ring: [0.85, 0.93, 1.0], // the photon ring / bloom, brightest tier
  u_disk: [0.92, 0.96, 1.0], // a mild overall tint multiplied onto the blackbody colour
  u_warm: [0.55, 0.28, 0.16], // the rust-brown cooling riding one side of the disk
  u_star: [0.85, 0.92, 1.0], // the background starfield's cool tier
};

/* Composition, exposed as uniforms rather than baked in — see the doc
   comment above for why this one field carries knobs the rest of the
   catalogue doesn't. Radii/thickness are world-space units inside the ray
   marcher; temperature is in thousands of kelvin. */
const PARAMS: Record<string, number> = {
  u_blackHoleRadius: 0.85,
  u_diskInclination: 0.12, // camera elevation above the disk plane, radians — small = near edge-on
  u_diskThickness: 0.05,
  u_lensStrength: 1.4, // gravitational pull driving the ray-bend integration
  u_lensFalloff: 0.35, // stabiliser floor in the pull's 1/r^2 denominator
  u_diskBrightness: 1.6,
  u_diskWidth: 26.0, // the disk's own outer radius
  u_diskTemperature: 58.0, // peak (inner-edge) blackbody temperature, thousands of K
  u_temperatureFalloff: 0.62, // how fast temperature (and colour) cools outward
  u_diskRotationSpeed: -6.0, // sign sets spin direction; magnitude drives Doppler + turbulence phase
  u_dopplerStrength: 1.0,
  u_diskEdgeSoftnessInner: 0.14,
  u_diskEdgeSoftnessOuter: 0.4,
  u_diskTurbulence: 1.0,
  u_diskStreakScale: 0.34, // radial frequency of the cylinder-mapped turbulence
  u_turbulenceStretch: 0.85, // how tight the tangential streaks wrap
  u_turbulenceSharpness: 5.5, // how thin/filamentary the turbulence reads
  u_turbulenceCycleTime: 6.0, // seconds per seamless turbulence-rotation loop
  u_turbulenceLacunarity: 2.4,
  u_turbulencePersistence: 0.75,
  u_photonRingWidth: 0.045,
  u_photonRingBrightness: 1.7,
  u_bloomStrength: 0.2,
  u_blueIntensity: 1.0,
  u_warmDustIntensity: 0.5,
  u_starIntensity: 1.0,
  u_animationSpeed: 1.0,
};

/* Where the hole sits on screen, as a fraction of the frame — slightly
   above vertical centre, per the reference. */
const BH_CENTER: [number, number] = [0.5, 0.54];

const FRAG = `
uniform vec2  u_res;
uniform float u_time;
uniform float u_scale;

uniform vec3 u_ground;
uniform vec3 u_ring;
uniform vec3 u_disk;
uniform vec3 u_warm;
uniform vec3 u_star;

uniform vec2  u_bhCenter;
uniform float u_blackHoleRadius;
uniform float u_diskInclination;
uniform float u_diskThickness;
uniform float u_lensStrength;
uniform float u_lensFalloff;
uniform float u_diskBrightness;
uniform float u_diskWidth;
uniform float u_diskTemperature;
uniform float u_temperatureFalloff;
uniform float u_diskRotationSpeed;
uniform float u_dopplerStrength;
uniform float u_diskEdgeSoftnessInner;
uniform float u_diskEdgeSoftnessOuter;
uniform float u_diskTurbulence;
uniform float u_diskStreakScale;
uniform float u_turbulenceStretch;
uniform float u_turbulenceSharpness;
uniform float u_turbulenceCycleTime;
uniform float u_turbulenceLacunarity;
uniform float u_turbulencePersistence;
uniform float u_photonRingWidth;
uniform float u_photonRingBrightness;
uniform float u_bloomStrength;
uniform float u_blueIntensity;
uniform float u_warmDustIntensity;
uniform float u_starIntensity;
uniform float u_animationSpeed;

uniform vec4  u_readA;
uniform float u_guard;

float wrapAngle(float a) {
  return a - 6.2831853 * floor(a / 6.2831853 + 0.5);
}

/* A second, independent hash/noise family in 3D — the shared prelude's
   fbm() is 2D, and the disk's turbulence needs to sample a genuine 3D
   cylinder (radius, cos angle, sin angle) so it wraps with no seam at
   angle = +-pi, which a 2D (angle, radius) flow can't do without a warp. */
float hash3(vec3 p) {
  return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453);
}

float noise3(vec3 p) {
  vec3 i = floor(p);
  vec3 f = fract(p);
  vec3 u = f * f * (3.0 - 2.0 * f);
  float a = hash3(i);
  float b = hash3(i + vec3(1.0, 0.0, 0.0));
  float c = hash3(i + vec3(0.0, 1.0, 0.0));
  float d = hash3(i + vec3(1.0, 1.0, 0.0));
  float e = hash3(i + vec3(0.0, 0.0, 1.0));
  float f2 = hash3(i + vec3(1.0, 0.0, 1.0));
  float g = hash3(i + vec3(0.0, 1.0, 1.0));
  float h = hash3(i + vec3(1.0, 1.0, 1.0));
  return mix(mix(mix(a, b, u.x), mix(c, d, u.x), u.y),
             mix(mix(e, f2, u.x), mix(g, h, u.x), u.y), u.z);
}

float fbm3(vec3 p, float lacunarity, float persistence) {
  float value = 0.0;
  float amplitude = 0.5;
  for (int i = 0; i < 4; i++) {
    value += noise3(p) * amplitude;
    p *= lacunarity;
    amplitude *= persistence;
  }
  return value;
}

/* A crude but cheap blackbody approximation — plausible enough to carry a
   hot blue-white inner edge cooling to orange at the rim, without a real
   spectral integral. */
vec3 blackbody(float tempK) {
  float t = clamp(tempK / 40000.0, 0.0, 1.0);
  vec3 cool = vec3(1.0, 0.36, 0.12);    /* ~1500K, deep orange */
  vec3 warm = vec3(1.0, 0.86, 0.62);    /* ~4500K, warm white */
  vec3 neutral = vec3(1.0, 1.0, 1.0);   /* ~6500K, white */
  vec3 hot = vec3(0.66, 0.8, 1.0);      /* ~25000K+, blue-white */
  vec3 col = mix(cool, warm, smoothstep(0.0, 0.15, t));
  col = mix(col, neutral, smoothstep(0.1, 0.3, t));
  col = mix(col, hot, smoothstep(0.15, 0.55, t));
  return col;
}

/* A distant starfield indexed by ray direction rather than screen space —
   fixed at infinity regardless of how much a given ray bent — with a
   per-star size and colour-temperature jitter, plus a slow twinkle for
   the same autonomous-motion reasoning the rest of the catalogue holds to. */
vec3 starfield(vec3 dir, float time) {
  float theta = atan(dir.z, dir.x);
  float phi = asin(clamp(dir.y, -1.0, 1.0));
  vec2 sp = vec2(theta, phi) * 60.0;
  vec2 cell = floor(sp);
  vec2 cellUV = fract(sp);
  float ch = dotHash(cell);
  vec3 out_ = vec3(0.0);
  if (ch > 0.975) {
    vec2 starPos = vec2(dotHash(cell + 1.7), dotHash(cell + 5.3)) * 0.8 + 0.1;
    float distToStar = length(cellUV - starPos);
    float sizeVar = dotHash(cell + 9.1) * 0.12 + 0.03;
    float core = smoothstep(sizeVar, 0.0, distToStar);
    float glow = smoothstep(sizeVar * 3.0, 0.0, distToStar) * 0.3;
    float twinkle = 0.6 + 0.4 * sin(time * (1.0 + ch * 2.0) + ch * 40.0);
    float starI = (core + glow) * twinkle * (ch - 0.975) * 40.0;
    vec3 starCol = mix(u_star, vec3(1.0, 0.95, 0.85), dotHash(cell + 21.0));
    out_ = starCol * starI;
  }
  return out_ * u_starIntensity;
}

/* The disk's own shading at one exact plane crossing: a physically
   -inspired temperature gradient converted through blackbody(), a
   Doppler-style brightness split from the local orbital velocity's
   alignment with the ray reaching the camera, and turbulence sampled on a
   cylinder so it wraps with no seam, cross-faded between two phases one
   full rotation apart so the loop never pops. Returns premultiplied
   colour plus opacity, for front-to-back compositing in the caller. */
vec4 diskShade(float hitR, float hitAngle, vec3 rayDir, float innerR, float outerR, float time) {
  float normR = clamp((hitR - innerR) / max(outerR - innerR, 1e-4), 0.0, 1.0);

  float peakTempK = u_diskTemperature * 1000.0;
  float outerTempK = 1400.0;
  float tempFalloff = pow(innerR / max(hitR, 1e-3), u_temperatureFalloff);
  float tempK = mix(outerTempK, peakTempK, tempFalloff);

  /* a mild angular cooling bias riding the disk itself, rather than a
     patch pinned to the screen — where the reference's rust-brown region
     comes from: one sector of the same disk, just cooler */
  float dustBias = exp(-pow(wrapAngle(hitAngle + 0.95) / 0.5, 2.0)) * u_warmDustIntensity;
  tempK = mix(tempK, tempK * 0.14, dustBias);

  vec3 diskCol = blackbody(tempK);
  diskCol = mix(diskCol, u_warm, dustBias * 0.75);
  diskCol *= mix(vec3(1.0), u_disk, 0.4);

  float rotationSign = sign(u_diskRotationSpeed);
  vec3 velocityDir = vec3(-sin(hitAngle) * rotationSign, 0.0, cos(hitAngle) * rotationSign);
  float velocityMagnitude = 1.0 / sqrt(max(hitR / innerR, 1e-4));
  float beta = velocityMagnitude * 0.3;
  float cosTheta = dot(velocityDir, rayDir);
  float dopplerFactor = 1.0 / max(1.0 - beta * cosTheta, 1e-3);
  float dopplerBoost = clamp(pow(dopplerFactor, 3.0 * u_dopplerStrength), 0.1, 5.0);
  diskCol *= dopplerBoost;

  float edgeFalloff = smoothstep(0.0, u_diskEdgeSoftnessInner, normR)
                     * smoothstep(1.0, 1.0 - u_diskEdgeSoftnessOuter, normR);

  /* two time-shifted turbulence samples, one full rotation cycle apart,
     cross-faded — a seamlessly looping spin instead of one that pops at
     the cycle boundary */
  float cycleLength = max(u_turbulenceCycleTime, 0.5);
  float cyclicTime = mod(time, cycleLength);
  float blendT = cyclicTime / cycleLength;
  float rPow = pow(max(hitR, 1e-3), 1.5);
  float phase1 = cyclicTime * u_diskRotationSpeed / rPow;
  float phase2 = (cyclicTime + cycleLength) * u_diskRotationSpeed / rPow;
  float stretch = max(u_turbulenceStretch, 0.1);
  vec3 nc1 = vec3(hitR * u_diskStreakScale, cos(hitAngle + phase1) / stretch, sin(hitAngle + phase1) / stretch);
  vec3 nc2 = vec3(hitR * u_diskStreakScale, cos(hitAngle + phase2) / stretch, sin(hitAngle + phase2) / stretch);
  float turb1 = fbm3(nc1, u_turbulenceLacunarity, u_turbulencePersistence);
  float turb2 = fbm3(nc2, u_turbulenceLacunarity, u_turbulencePersistence);
  float turb = mix(turb2, turb1, blendT);
  float ringOpacity = pow(clamp(turb, 0.0, 1.0), u_turbulenceSharpness);
  ringOpacity = mix(1.0, ringOpacity, clamp(u_diskTurbulence, 0.0, 1.0));

  return vec4(diskCol * u_diskBrightness, ringOpacity * edgeFalloff);
}

/* Narkowicz's ACES approximation — a cheap, standard filmic roll-off so
   the hottest disk pixels bloom toward white instead of clipping flat. */
vec3 acesFilm(vec3 x) {
  float a = 2.51, b = 0.03, c = 2.43, d = 0.59, e = 0.14;
  return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.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 - u_bhCenter) * vec2(aspect, 1.0);
  float time = u_time * u_animationSpeed;

  float bhR = u_blackHoleRadius;
  float innerR = bhR * 2.1;
  float outerR = max(u_diskWidth, innerR * 1.6);

  /* ---- the camera: sitting almost edge-on, a shallow elevation above the
     disk's own plane, looking back at the hole. Far enough back, and with
     a wide enough field of view, that the disk's own outer radius reaches
     toward the frame's edges while the horizon — even amplified by its own
     photon-capture cross-section, well past the bare event-horizon radius
     — stays a small fraction of that: the ratio between the two is fixed
     by world-space geometry alone, not by how far back the camera sits, so
     camDist/tanFov are tuned to the disk's own outerR here. ------------ */
  float camDist = outerR * 1.55;
  float camHeight = camDist * tan(u_diskInclination);
  vec3 camPos = vec3(0.0, camHeight, -camDist);
  vec3 fwd = normalize(-camPos);
  vec3 worldUp = vec3(0.0, 1.0, 0.0);
  vec3 right = normalize(cross(fwd, worldUp));
  vec3 up = cross(right, fwd);
  float tanFov = 0.8;
  vec3 dir = normalize(fwd + p.x * tanFov * right + p.y * tanFov * up);
  vec3 pos = camPos;
  vec3 prevPos = pos;

  /* ---- the ray march: bend dir toward the hole every step under a
     stabilised inverse-square pull, testing each step for an exact
     crossing of the disk's plane and alpha-compositing front to back.
     Near-side and far-side (lensed) images fall out of this one loop with
     no separate branch for either — see the component doc comment. ---- */
  vec3 accum = vec3(0.0);
  float alpha = 0.0;
  float minR = 1e4;
  bool captured = false;

  const int STEPS = 40;
  for (int i = 0; i < STEPS; i++) {
    float r = length(pos);
    minR = min(minR, r);
    if (r < bhR) { captured = true; break; }
    if (alpha > 0.985) break;

    float stepLen = clamp(r * 0.22, 0.035, 2.4);
    vec3 toCenter = -pos / max(r, 1e-4);
    float accel = u_lensStrength / max(r * r, u_lensFalloff);
    /* the turn each step is allowed to make is capped outright — without
       this, the 1/r^2 pull overshoots badly in the last few steps before
       capture and the numerical instability inflates the shadow's own
       effective radius far past the horizon itself */
    float bend = min(accel * stepLen, 0.3);
    dir = normalize(dir + toCenter * bend);

    prevPos = pos;
    pos += dir * stepLen;

    /* an exact crossing: a sign change in local height, resolved to the
       precise crossing point by linear interpolation rather than guessed
       from a continuous density field */
    if (prevPos.y * pos.y < 0.0) {
      float t = -prevPos.y / (pos.y - prevPos.y);
      vec3 hitPos = mix(prevPos, pos, t);
      float hitR = length(hitPos.xz);
      if (hitR > innerR && hitR < outerR) {
        float hitAngle = atan(hitPos.z, hitPos.x);
        vec4 shaded = diskShade(hitR, hitAngle, dir, innerR, outerR, time);
        float remaining = 1.0 - alpha;
        accum += shaded.rgb * shaded.a * remaining;
        alpha += remaining * shaded.a;
      }
    }

    if (r > outerR * 4.0 && dot(dir, pos) > 0.0) break;
  }

  vec3 col;
  if (captured) {
    col = u_ground * 0.25;
  } else {
    vec3 bg = u_ground + starfield(dir, time);
    col = bg * (1.0 - alpha) + accum;

    /* the photon ring: a consequence of the same integration, not a second
       drawn circle — driven by minR, the closest approach the bent ray
       actually made (this shader has no separate bloom pass to lean on,
       unlike the reference it's adapted from), with a whisper of angular
       irregularity and a boost wherever the disk it rode in on is
       already bright */
    float ringAngNoise = fbm(vec2(cos(atan(p.y, p.x)) * 3.0, sin(atan(p.y, p.x)) * 3.0) + time * 0.02);
    float ringSigma = max(u_photonRingWidth * (1.0 + ringAngNoise * 0.35), 0.008);
    float ringShape = exp(-pow((minR - bhR * 1.06) / ringSigma, 2.0));
    float diskLum = dot(accum, vec3(0.2126, 0.7152, 0.0722));
    float ringBoost = 0.45 + 0.9 * clamp(diskLum * 2.2, 0.0, 1.0);
    col += u_ring * ringShape * ringBoost * u_photonRingBrightness * u_blueIntensity;

    /* a soft bloom, held tight to the hottest pixels only */
    float hot = clamp(ringShape * 0.6 + diskLum * 0.8, 0.0, 1.0);
    col += mix(u_disk, u_ring, 0.5) * hot * hot * u_bloomStrength * u_blueIntensity * 0.6;
  }

  /* corner vignette — the ray marcher already darkens the corners on its
     own (nothing bends far enough out to reach them), this just firms it
     up a little further */
  vec2 corner = uv - 0.5;
  float cvig = length(corner * vec2(1.0, 1.2));
  col *= mix(1.0, 0.55, smoothstep(0.55, 1.15, cvig));

  col = max(col, 0.0);
  col = acesFilm(col);

  /* ---- the reading guard (see TileField for the full rationale) ---- */
  vec2 rg = abs(uv - u_readA.xy) / max(u_readA.zw, vec2(0.02));
  float md = mix(max(rg.x, rg.y), length(rg), 0.4);
  float guardBand = 1.0 - smoothstep(0.72, 2.1, md);
  col = mix(col, holdUnder(col, 0.09), guardBand * u_guard);

  col += (bayer8(gl_FragCoord.xy) - 0.5) * (2.2 / 255.0);

  gl_FragColor = vec4(col, 1.0);
}
`;

export interface GargantuaFieldProps {
  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 GargantuaField({ className, guardSelector = null }: GargantuaFieldProps) {
  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), ...Object.keys(PARAMS), "u_bhCenter", "u_readA", "u_guard"],
      onInit: (gl, u) => {
        for (const name of Object.keys(PALETTE)) {
          if (u[name]) gl.uniform3fv(u[name], PALETTE[name]);
        }
        for (const name of Object.keys(PARAMS)) {
          if (u[name]) gl.uniform1f(u[name], PARAMS[name]);
        }
        if (u.u_bhCenter) gl.uniform2f(u.u_bhCenter, BH_CENTER[0], BH_CENTER[1]);
        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 EventHorizonField / SingularityField:
         once painted, the last frame stays on screen while the surface is
         parked off-view. onLost drops back to the CSS floor. */
      onLost: () => canvas.removeAttribute("data-shader"),
      maxPixels: 1_500_000,
      dprCap: 1.4,
    });
  }, [guardSelector]);

  return (
    <div
      className={`absolute inset-0 overflow-hidden${className ? ` ${className}` : ""}`}
      aria-hidden="true"
    >
      <div className="absolute inset-0 [background:radial-gradient(_4%_6%_at_50%_46%,oklch(0.02_0_0)_0%,oklch(0.02_0_0)_55%,transparent_75%_),radial-gradient(_6%_8%_at_50%_46%,transparent_48%,oklch(0.95_0.02_235_/_0.9)_60%,transparent_78%_),radial-gradient(_46%_9%_at_50%_50%,oklch(0.55_0.14_240_/_0.55)_0%,oklch(0.4_0.13_240_/_0.35)_45%,transparent_82%_),radial-gradient(_38%_20%_at_50%_30%,transparent_40%,oklch(0.62_0.13_240_/_0.5)_60%,transparent_84%_),radial-gradient(_26%_16%_at_24%_53%,oklch(0.32_0.12_40_/_0.4)_0%,transparent_72%_),radial-gradient(_55%_55%_at_50%_46%,oklch(0.06_0.015_255)_0%,transparent_74%_),oklch(0.015_0.008_260)] after:content-[''] after:absolute after:inset-0 after:[background:repeating-radial-gradient(_circle_at_50%_46%,transparent_0_4%,oklch(0.62_0.13_240_/_0.07)_4%_4.5%,transparent_4.5%_9%_)] after:[mix-blend-mode:screen]" />
      <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