IrisBackgroundsSingularity
Singularity
A black oval horizon with a crisp rim, a fountain of thin rays fanning up off it, and — below — a woven grid funnelling into a narrow throat before it blooms back out into a starlit nebula. Built to one reference photo's own hourglass shape.
Singularity
The horizon sits upper-centre as a flat black oval with a crisp bright rim traced tight around its edge, rather than the catalogue's usual face-on accretion disk. Above it, a wide fan of thin white-blue rays fountains up and outward across the whole top of the frame — some short, some running near to the edge, each its own length and its own flicker, the way real light escaping the rim would never come in one uniform length. Below it, a woven grid of meridian lines and drifting latitude rings funnels down from the horizon's own width into a narrow throat, then opens back out into a bloom of nebula gas and scattered stars at the bottom edge — one continuous silhouette, mouth to throat to bloom.
Every motion here is self-sustaining — the rays' own flicker, the throat's own inward drift, the stars' own twinkle — there is no pointer interaction, unlike the rest of the catalogue's cursor-driven fields.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { SingularityField } from "./SingularityField";
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">
<SingularityField />
</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 built to the shape of one specific reference
* photo: a black oval horizon with a layered, bloomed rim sitting
* upper-centre, a fan of soft volumetric light shafts off that rim across
* the whole top of the frame, and — below the horizon — a woven, turbulent
* grid that funnels into a narrow throat and blooms back out into a
* starlit nebula at the bottom edge. One continuous silhouette,
* mouth-to-throat-to-bloom, rendered for HDR-style depth rather than flat
* vector lines: every glow is three stacked falloffs (a tight hot core, a
* soft mid halo, a wide ambient bloom), every edge carries a little organic
* turbulence so nothing reads as a perfect drafted curve, motion is slow
* fbm drift rather than a sine-wave twinkle, and the whole frame runs
* through a soft tonemap so highlights roll off instead of clipping flat.
*
* Every motion is autonomous and unhurried — the rays drift, the throat's
* latitude bands crawl slowly inward, the whole horizon breathes by about
* one percent. `u_pointer` isn't read by this shader — no pointer
* interaction.
*
* 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-singularityfield__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 reference photo is a strict
monochrome blue/white/black, nothing in the accent ramp carries that.
Three tiers of brightness (deep, line, hot) rather than two, so the glow
has tonal depth instead of reading as a single flat hue. */
const PALETTE: Record<string, [number, number, number]> = {
u_ground: [0.004, 0.005, 0.011], // near-black, cool space
u_void: [0.0, 0.0, 0.0], // the flat black horizon
u_hot: [0.94, 0.97, 1.0], // the brightest tier — rim core, ray cores
u_line: [0.42, 0.66, 0.98], // the mid tier — ray bodies, near grid
u_deep: [0.06, 0.17, 0.46], // the deep tier — ambient haze, far grid
u_star: [0.9, 0.95, 1.0], // the background starfield
u_nebula: [0.04, 0.11, 0.34], // the bloom where the throat opens out at the bottom
};
const FRAG = `
uniform vec2 u_res;
uniform float u_time;
uniform float u_scale;
uniform vec3 u_ground;
uniform vec3 u_void;
uniform vec3 u_hot;
uniform vec3 u_line;
uniform vec3 u_deep;
uniform vec3 u_star;
uniform vec3 u_nebula;
uniform vec4 u_readA;
uniform float u_guard;
/* soft periodic line at every integer of x — a gaussian well, not a hard
edge, so the grid reads as glowing energy rather than drafted wireframe */
float sline(float x, float w) {
float f = fract(x);
float d = min(f, 1.0 - f);
return exp(-(d * d) / (w * w));
}
const int RAYS = 14;
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 sc = vec2(aspect, 1.0);
vec2 q = uv * sc;
/* a one-percent breathing scale — the whole object is very slightly
alive rather than a frozen render */
float breathe = 1.0 + 0.01 * sin(u_time * 0.05);
vec2 C = vec2(0.5 * aspect, 0.64);
vec2 p = (q - C) / breathe;
vec3 col = u_ground;
/* a faint starfield, drifting and twinkling slowly */
vec2 sp = uv * vec2(150.0, 95.0) + vec2(u_time * 0.0025, 0.0);
vec2 sid = floor(sp);
vec2 sf = fract(sp);
float sh = dotHash(sid);
if (sh > 0.965) {
vec2 jitter = vec2(dotHash(sid + 1.7), dotHash(sid + 5.3));
float d = length(sf - jitter);
float twinkle = max(0.5 + 0.5 * sin(u_time * (0.3 + sh * 0.5) + sh * 40.0), 0.0);
float star = exp(-d * d * 700.0) * twinkle;
col += u_star * star * (sh - 0.965) * 28.0;
}
float holeRX = 0.3;
float holeRY = 0.115;
float angRim = atan(p.y, p.x);
float rimTurb = fbm(vec2(cos(angRim) * 2.2, sin(angRim) * 2.2) + u_time * 0.025);
/* ---- the upper fountain: soft volumetric shafts, a few structured
hero rays over a continuous, noise-driven ambient fan ------------- */
if (p.y > -holeRY * 1.1) {
vec2 up = vec2(p.x, p.y + holeRY * 0.15);
float rr = length(up);
float fanAng = atan(up.x, up.y); /* 0 = straight up */
/* the ambient haze: a continuous fan, denser toward the centre,
textured by slow fbm rather than N discrete identical rays */
float fanMask = smoothstep(1.55, 0.0, abs(fanAng));
float density = fbm(vec2(fanAng * 2.6, rr * 1.2 - u_time * 0.02)) * 0.5 + 0.5;
float ambient = fanMask * density * exp(-rr * 0.85);
col += mix(u_deep, u_line, 0.5) * ambient * 0.4;
for (int i = 0; i < RAYS; i++) {
float fi = float(i);
float u01 = (fi + 0.5) / float(RAYS);
float jitterA = (dotHash(vec2(fi, 1.3)) - 0.5) * 0.06;
float angle = (u01 - 0.5) * 2.9 + jitterA; /* ~166°, centred up */
vec2 dir = vec2(sin(angle), cos(angle));
vec2 perp = vec2(dir.y, -dir.x);
float along = dot(up, dir);
float off = dot(up, perp);
float lenHash = dotHash(vec2(fi, 4.7));
float rayLen = mix(0.5, 1.4, lenHash);
float thickHash = dotHash(vec2(fi, 9.1));
/* two stacked gaussians per ray — a tight hot core plus a soft wide
halo — the cheap single-pass version of a bloom pass */
float thickness = mix(1400.0, 3600.0, thickHash);
float core = exp(-off * off * thickness);
float halo = exp(-off * off * thickness * 0.12) * 0.4;
float presence = smoothstep(0.0, 0.08, along)
* (1.0 - smoothstep(rayLen * 0.55, rayLen, along));
/* slow organic drift instead of a sine-wave twinkle */
float drift = fbm(vec2(along * 1.4 - u_time * 0.05, fi * 3.7)) * 0.5 + 0.5;
float bright = mix(0.35, 1.0, dotHash(vec2(fi, 6.6)));
vec3 rayCol = mix(u_line, u_hot, core * 0.55);
col += rayCol * (core + halo) * presence * (0.55 + 0.45 * drift) * bright * 0.62;
}
}
/* a single soft anamorphic streak through the horizon, and a faint
vertical companion — the lens-flare bar a real long-exposure capture
of something this bright would actually throw */
float hStreak = exp(-p.y * p.y * 5200.0) * exp(-abs(p.x) * 1.1);
float vStreak = exp(-p.x * p.x * 9000.0) * exp(-abs(p.y) * 2.4);
col += u_hot * hStreak * 0.22 + u_line * vStreak * 0.1;
/* ---- the lower throat: a woven, turbulent grid funnelling to a neck,
then a haze-filled bloom back out toward the bottom edge ----------- */
if (p.y < holeRY * 0.4) {
float belowStart = -holeRY * 0.4;
float denom = max(belowStart + C.y, 1e-3);
float t = clamp((belowStart - p.y) / denom, 0.0, 1.0);
float neckT = 0.45;
float neckW = 0.018;
float wTop = holeRX * 0.92;
float bloomW = 0.85;
float width = t < neckT
? mix(wTop, neckW, smoothstep(0.0, neckT, t))
: mix(neckW, bloomW, smoothstep(neckT, 1.0, t));
float s = p.x / max(width, 1e-4);
/* gentle turbulence so the funnel wavers like woven fabric, not a
drafted cone */
float warp = fbm(vec2(s * 1.6, t * 2.4 - u_time * 0.06));
float sw = s + warp * 0.12;
float tw = t + fbm(vec2(t * 3.5, s * 1.8)) * 0.015;
float meridian = sline(sw * 3.2, 0.11) * step(abs(s), 1.2);
float latitude = sline(tw * 7.0 - u_time * 0.1, 0.16) * smoothstep(1.3, 0.85, abs(s));
float gridLine = meridian * 0.75 + latitude * 0.55;
/* a soft haze filling the throat's cross-section, independent of the
line pattern — volume, not just wireframe */
float haze = exp(-abs(s) * 2.0) * mix(0.06, 0.3, 1.0 - t);
float neckGlow = exp(-pow(t - neckT, 2.0) * 40.0);
float fall = mix(1.0, 0.45, smoothstep(0.0, 1.0, t));
vec3 throatCol = mix(u_deep, mix(u_line, u_hot, neckGlow * 0.6), neckGlow);
col += throatCol * gridLine * fall * 0.55;
col += u_deep * haze * fall * 0.7;
}
/* the nebula bloom where the throat opens back out at the bottom edge */
float nebNoise = fbm(vec2(q.x * 1.4, q.y * 1.4 - u_time * 0.03)) * 0.5 + 0.5;
float nebNoise2 = fbm(vec2(q.x * 3.1 - 4.0, q.y * 3.1 + u_time * 0.015)) * 0.5 + 0.5;
float nebMask = smoothstep(0.48, -0.05, uv.y);
col += u_nebula * nebMask * (0.4 + 0.5 * nebNoise + 0.25 * nebNoise2) * 1.05;
/* the horizon itself: a black oval with a little organic turbulence on
its edge, occluding, with a layered bloomed rim rather than one hard
ring — a tight hot core, a soft mid halo, a wide ambient glow */
vec2 pe = p / vec2(holeRX, holeRY);
float e = length(pe) * (1.0 + rimTurb * 0.035);
float angMod = 0.8 + 0.2 * fbm(vec2(cos(angRim) * 3.1, sin(angRim) * 3.1) + u_time * 0.015);
float coreRim = exp(-pow(e - 1.0, 2.0) * 320.0);
float midRim = exp(-pow(e - 1.0, 2.0) * 60.0);
float softRim = exp(-pow(e - 1.0, 2.0) * 10.0);
col += u_deep * softRim * angMod * 0.55;
col += u_line * midRim * angMod * 0.85;
col += mix(u_line, u_hot, 0.7) * coreRim * angMod * 1.5;
/* a whisper of chromatic split right at the hottest edge — the lens
artefact a real bright rim would actually carry */
float eR = length(pe + vec2(0.0018, 0.0)) * (1.0 + rimTurb * 0.035);
float eB = length(pe - vec2(0.0018, 0.0)) * (1.0 + rimTurb * 0.035);
col.r += exp(-pow(eR - 1.0, 2.0) * 320.0) * 0.12;
col.b += exp(-pow(eB - 1.0, 2.0) * 320.0) * 0.12;
/* inside the horizon: not flat #000 but a whisper of deep-space gradient,
so it reads as a void with depth rather than a pasted cutout */
vec3 insideCol = mix(u_void, u_ground * 0.4, smoothstep(0.0, 1.0, e));
float inside = 1.0 - smoothstep(0.965, 1.02, e);
col = mix(col, insideCol, inside);
/* corner vignette */
vec2 corner = uv - 0.5;
float cvig = length(corner * vec2(1.0, 1.15));
col *= mix(1.0, 0.6, smoothstep(0.55, 1.1, cvig));
/* crush the black point a hair, then a soft filmic roll-off so the
brightest cores bloom rather than clip flat */
col = max(col - vec3(0.0015), 0.0);
col = col / (1.0 + col * 0.55);
/* ---- 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 SingularityFieldProps {
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 SingularityField({ className, guardSelector = null }: SingularityFieldProps) {
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 EventHorizonField: 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_800_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(_26%_11%_at_50%_36%,oklch(0.02_0_0)_0%,oklch(0.02_0_0)_60%,transparent_74%_),radial-gradient(_30%_13%_at_50%_36%,transparent_62%,oklch(0.95_0.02_235_/_0.9)_70%,transparent_80%_),radial-gradient(_62%_68%_at_50%_-6%,oklch(0.72_0.11_235_/_0.4)_0%,transparent_62%_),radial-gradient(_20%_42%_at_50%_56%,oklch(0.5_0.14_240_/_0.35)_0%,transparent_72%_),radial-gradient(_78%_52%_at_50%_97%,oklch(0.32_0.15_250_/_0.55)_0%,transparent_70%_),oklch(0.03_0.015_260)] after:content-[''] after:absolute after:inset-0 after:[background:repeating-conic-gradient(_from_191deg_at_50%_36%,transparent_0deg_4deg,oklch(0.85_0.05_235_/_0.2)_4deg_4.6deg,transparent_4.6deg_8deg_)] after:[-webkit-mask-image:radial-gradient(_65%_65%_at_50%_6%,#000_0%,#000_55%,transparent_85%_)] after:[mask-image:radial-gradient(_65%_65%_at_50%_6%,#000_0%,#000_55%,transparent_85%_)] 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>
);
}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