IrisBackgroundsQuasar
Quasar
A black horizon sitting left-of-frame, its warm turbulent ring broken open by a single jet running white-hot to ember, a cool nebula in one corner against a warm one gathered behind the jet's own path — the jet leans gently toward the pointer.
Quasar
Where the rest of the gravity family sits its hole dead centre, this one is framed the way the reference photo frames it: off to the left, wrapped in a warm, tan accretion ring thick with turbulent bands rather than a clean saucer. The ring breaks open on its upper-right side, and through that gap a single bright jet punches out — white-hot right at the horizon, cooling to ember orange as it travels, its own flicker riding along its length. A thin lensed arc echoes the ring's far side over the top of the horizon. The frame's two corners carry the same warm/cool duality as the ring itself: a cool blue nebula gathers in the corner behind the hole, a warm one behind the jet's own path, both drifting through a faint twinkling starfield.
Unlike `event-horizon`, `singularity` and `gargantua`, this one keeps the interactions — small, deliberately: the jet leans a few degrees toward wherever the pointer sits vertically, the two nebula layers drift a few percent of the frame for parallax, and a soft warm glow trails the cursor itself, clipped by the horizon like any other light. Everything else — the ring's swirl, the jet's flicker, the star twinkle — runs on its own.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { QuasarField } from "./QuasarField";
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">
<QuasarField />
</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 quasar — built to one reference photo's own off-centre
* composition rather than the catalogue's usual face-on hole: a black
* horizon sitting left-of-frame, wrapped in a warm, turbulent accretion ring
* that breaks open on its upper-right side, a single bright jet punching out
* through that break — white-hot at the horizon, cooling to ember orange as
* it travels — and a cool blue nebula filling the frame's opposite corner
* against a warm one gathered behind the jet's own path. A thin lensed arc
* echoes the ring's far side over the top of the horizon, and a faint
* starfield twinkles through both nebulae.
*
* Unlike `EventHorizonField` / `SingularityField`, this one *does* answer to
* the cursor, kept deliberately small: the jet leans a few degrees toward
* wherever the pointer sits vertically, the two nebula layers drift a few
* percent of the frame for parallax rather than staying pinned to it, and a
* soft warm glow trails the pointer itself. Every other motion — the ring's
* swirl, the jet's flicker, the star twinkle — runs on its own regardless of
* whether anyone is pointing at it.
*
* 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-quasarfield__floor` underneath visible — a still
* frame in the same palette, never a blank box.
*
* Fixed palette, not the token ramp — like `EventHorizonField` and
* `SingularityField`, this is a deep-space duality the site's own accent
* ramp doesn't carry.
*
* 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 — see the note above. */
const PALETTE: Record<string, [number, number, number]> = {
u_ground: [0.005, 0.006, 0.014], // near-black, cool deep space
u_void: [0.0, 0.0, 0.0], // the flat black horizon
u_diskLit: [0.94, 0.87, 0.74], // the ring's hot, sunlit tan
u_diskShadow: [0.24, 0.19, 0.16], // the ring's own turbulent shadow
u_ring: [0.97, 0.95, 0.9], // the photon ring and lensed arc
u_jetHot: [1.0, 0.98, 0.92], // the jet's white-hot core, near the horizon
u_jetWarm: [0.98, 0.42, 0.14], // the jet cooling to ember orange
u_nebulaWarm: [0.5, 0.16, 0.05], // the warm cloud gathered behind the jet
u_nebulaCool: [0.06, 0.1, 0.24], // the cool cloud in the opposite corner
u_star: [0.92, 0.95, 1.0], // the background starfield
};
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_ground;
uniform vec3 u_void;
uniform vec3 u_diskLit;
uniform vec3 u_diskShadow;
uniform vec3 u_ring;
uniform vec3 u_jetHot;
uniform vec3 u_jetWarm;
uniform vec3 u_nebulaWarm;
uniform vec3 u_nebulaCool;
uniform vec3 u_star;
uniform vec4 u_readA;
uniform float u_guard;
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;
float pep = u_pointer.z;
vec2 ptr = u_pointer.xy * sc;
/* small pointer interactions, all scaled to a few percent of the frame so
the composition itself never chases the cursor — the far nebula drifts
for parallax, the near one drifts a little less */
vec2 driftFar = (u_pointer.xy - 0.5) * 0.05 * pep;
vec2 driftNear = (u_pointer.xy - 0.5) * 0.02 * pep;
vec3 col = u_ground;
/* a faint starfield, twinkling on its own, drifting a touch with the far
nebula layer */
vec2 sp = uv * vec2(150.0, 95.0) - driftFar * 40.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.4 + 0.6 * sin(u_time * (1.3 + sh * 2.0) + sh * 40.0), 0.0);
float star = exp(-d * d * 700.0) * twinkle;
col += u_star * star * (sh - 0.965) * 28.0;
}
/* the cool nebula, filling the corner opposite the jet's own path */
vec2 coolQ = q + driftFar;
float coolN = fbm(vec2(coolQ.x * 1.1, coolQ.y * 1.1 - u_time * 0.015)) * 0.5 + 0.5;
float coolMask = smoothstep(0.7, -0.15, uv.x) * smoothstep(0.02, 0.85, uv.y);
col += mix(u_nebulaCool * 0.55, u_nebulaCool * 1.5, coolN) * coolMask * 0.9;
/* the warm nebula, gathered behind the jet's own line of travel */
vec2 warmQ = q + driftFar * 0.7;
float warmN = fbm(vec2(warmQ.x * 1.3 - 3.0, warmQ.y * 1.3 + u_time * 0.02)) * 0.5 + 0.5;
float warmN2 = fbm(vec2(warmQ.x * 2.6 + 7.0, warmQ.y * 2.6 - u_time * 0.03)) * 0.5 + 0.5;
float warmMask = smoothstep(0.3, 1.05, uv.x);
col += mix(u_nebulaWarm * 0.5, u_nebulaWarm * 1.5, warmN * 0.6 + warmN2 * 0.4) * warmMask * 1.1;
/* the hole itself sits left-of-frame, the way the reference photo frames
it, not dead centre */
vec2 centre = vec2(0.36 * aspect, 0.5) + driftNear * 0.3;
vec2 p = q - centre;
float r = max(length(p), 1e-4);
float ang = atan(p.y, p.x);
float holeR = 0.11;
/* the jet: one beam, not the usual symmetric pair, leaning a few degrees
toward wherever the pointer sits vertically — a small interaction, not
a redirect */
float baseTilt = 0.32;
float jetBend = (u_pointer.y - 0.5) * 0.22 * pep;
vec2 jdir = normalize(vec2(1.0, baseTilt + jetBend));
vec2 jperp = vec2(-jdir.y, jdir.x);
float jetAngle = atan(jdir.y, jdir.x);
float along = dot(p, jdir);
float off = dot(p, jperp);
/* the accretion ring: tilted, turbulent, warm — broken open on the side
the jet punches through */
float tilt = 0.5;
vec2 pd = vec2(p.x, p.y / tilt);
float rd = max(length(pd), 1e-4);
float adisk = atan(pd.y, pd.x);
float diskInner = holeR * 1.15;
float diskOuter = holeR * 2.8;
float diskMask = smoothstep(diskInner - 0.01, diskInner + 0.015, rd)
* (1.0 - smoothstep(diskOuter - 0.2, diskOuter, rd));
float angFromJet = abs(atan(sin(ang - jetAngle), cos(ang - jetAngle)));
float gapGate = smoothstep(0.0, 0.55, angFromJet);
diskMask *= gapGate;
float swirl = fbm(vec2(rd * 3.0, adisk * 1.3 - u_time * 0.05));
float bands = 0.5 + 0.5 * sin(adisk * 2.0 + rd * 10.0 - u_time * 0.4 + swirl * 1.3);
float heat = clamp((rd - diskInner) / max(diskOuter - diskInner, 1e-4), 0.0, 1.0);
vec3 diskCol = mix(u_diskLit, u_diskShadow, heat * 0.7 + (1.0 - bands) * 0.3);
col += diskCol * diskMask * (0.5 + 0.6 * bands) * 1.05;
/* the lensed arc: the ring's far side, echoed thin and bright over the
top of the horizon */
float haloR = holeR * 1.35;
float dHalo = r - haloR;
float haloBand = exp(-dHalo * dHalo * 900.0);
float topGate = smoothstep(-0.15, 0.35, p.y / r);
col += mix(u_ring, u_diskLit, 0.4) * haloBand * topGate * 0.9;
/* the jet proper: widening as it travels, white-hot near the horizon and
cooling to ember orange further out, its own flicker riding on top */
float coreWidth = mix(0.014, 0.1, smoothstep(0.0, 1.3, along));
float jetShape = exp(-(off * off) / (coreWidth * coreWidth));
float jetGate = smoothstep(holeR * 0.55, holeR * 1.05, along);
float jetFlicker = 0.6 + 0.4 * fbm(vec2(along * 3.0 - u_time * 0.6, ang * 2.0));
vec3 jetCol = mix(u_jetWarm, u_jetHot, exp(-along * along * 3.0));
col += jetCol * jetShape * jetFlicker * jetGate * 1.3;
/* the photon ring, warm on the jet's side and cool on the far side —
the same duality the two nebulae carry */
float ringR = holeR * 1.05;
float dRing = r - ringR;
float photonRing = exp(-dRing * dRing * 2600.0);
float ringWarmth = 0.5 + 0.5 * cos(ang - jetAngle);
vec3 ringCol = mix(mix(u_ring, u_nebulaCool, 0.25), mix(u_ring, u_jetWarm, 0.35), ringWarmth);
col += ringCol * photonRing * 1.4;
/* a soft warm glow trailing the pointer itself — the smallest of the
interactions, and clipped by the horizon below like any other light */
float dPtr = distance(q, ptr);
float bloom = exp(-dPtr * dPtr * 3.0) * pep;
col += mix(u_jetHot, u_star, 0.3) * bloom * 0.06;
/* the event horizon: flat black with a little organic turbulence on its
edge, occluding everything behind it */
float rimTurb = fbm(vec2(cos(ang) * 2.5, sin(ang) * 2.5) + u_time * 0.02);
float rEdge = r * (1.0 + rimTurb * 0.02);
float horizon = 1.0 - smoothstep(holeR - 0.006, holeR + 0.006, rEdge);
col = mix(col, u_void, horizon);
/* corner vignette */
vec2 corner = uv - 0.5;
float cvig = length(corner * vec2(1.0, 1.15));
col *= mix(1.0, 0.62, smoothstep(0.55, 1.1, cvig));
/* a soft filmic roll-off so the jet and rim bloom rather than clip flat */
col = max(col, 0.0);
col = col / (1.0 + col * 0.5);
/* ---- 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 QuasarFieldProps {
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 QuasarField({ className, guardSelector = null }: QuasarFieldProps) {
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 / 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_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(_7%_10%_at_36%_50%,oklch(0.02_0_0)_0%,oklch(0.02_0_0)_60%,transparent_78%_),radial-gradient(_11%_14%_at_36%_50%,transparent_55%,oklch(0.94_0.03_75_/_0.85)_68%,transparent_80%_),radial-gradient(_55%_42%_at_70%_26%,oklch(0.72_0.14_45_/_0.5)_0%,transparent_68%_),radial-gradient(_60%_48%_at_80%_58%,oklch(0.42_0.15_40_/_0.55)_0%,transparent_70%_),radial-gradient(_52%_58%_at_12%_20%,oklch(0.3_0.09_250_/_0.4)_0%,transparent_65%_),oklch(0.03_0.015_260)] after:content-[''] after:absolute after:inset-0 after:[background:repeating-conic-gradient(_from_18deg_at_36%_50%,transparent_0deg_6deg,oklch(0.85_0.06_70_/_0.14)_6deg_6.6deg,transparent_6.6deg_12deg_)] 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