IrisBackgroundsLiquid Shaft
Liquid Shaft
A hot-cored beam of light bending once and pouring wherever the cursor stands.
Liquid Shaft
A shaft of light falls from an entry point above the top edge, bends once, and pours on down at an angle over a cool near-black ground — a hard red-orange body around a near-white core, bleeding softly into the dark around it. It is walked as a short chain of segments rather than one straight line, each swaying a little on a slow drift, and a brightness pulse runs down its length over time: light moving through the beam, not just sitting in it.
The point it pours onto is the cursor itself whenever one is present — move it and the whole lower run bends to keep landing there, elbow included, with a small warm pool gathering right where it lands. With no pointer it settles back to a slow drift near the lower-left.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { ShaftField } from "./ShaftField";
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">
<ShaftField />
</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 shaft of light falling through a near-black, cool-dark field —
* a hard, hot-cored beam that drops from an entry point above the top edge,
* bends once, and pours on down at an angle, the way a single doorway of
* light bends across a floor. Warm red-orange body, a near-white core, a
* soft ember bleed into the dark around it.
*
* The "liquid" is literal: the beam is walked as a short chain of
* sub-segments rather than one straight line, each vertex swaying a little
* on a slow low-frequency drift so the shaft ripples gently instead of
* reading as a ruled line, and a brightness pulse runs down its length over
* time — light moving through the beam, not just sitting in it.
*
* Interactive: the point the shaft pours onto IS the visitor's cursor,
* whenever one is present — move the pointer and the whole lower run bends
* to keep landing on it, elbow included, with a small warm pool gathering
* right where it lands. No pointer, and it settles back to a slow
* independent drift near the lower-left, so the field is never static.
*
* One of the reusable background fields (`SilkField`, `EmberField`,
* `ArcLightsField`, …). 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-shaftfield__floor` underneath visible.
*
* Like `EmberField`, the palette is a fixed set passed as uniforms rather
* than read from the accent ramp — this beam's red is its own mood, not the
* portfolio's amber.
*
* 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.014, 0.021, 0.026], // cool near-black behind everything
u_deep: [0.16, 0.035, 0.02], // the beam's soft outer bleed into the dark
u_beam: [0.95, 0.22, 0.11], // the body of the shaft
u_core: [1.0, 0.86, 0.66], // the hot near-white line riding its centre
};
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_deep;
uniform vec3 u_beam;
uniform vec3 u_core;
uniform vec4 u_readA;
uniform float u_guard;
/* One bend, roughly 42% of the way down from the entry to the landing
point — a short vertical fall, then the longer pour. Walked as SEG
sub-segments per leg so the sway below has somewhere to ripple. */
const int SEG = 7;
const int NPTS = SEG * 2 + 1;
const float ELBOW_FRAC = 0.42;
/* Distance from p to segment ab; h is the clamped projection (0 at a .. 1 at
b), carried out so the caller can tell how far along the whole path the
nearest point sits. */
float distSegT(vec2 p, vec2 a, vec2 b, out float h) {
vec2 pa = p - a, ba = b - a;
h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-6), 0.0, 1.0);
return length(pa - ba * h);
}
void main() {
vec2 res = u_res / u_scale;
vec2 uv = gl_FragCoord.xy / u_scale / res; /* 0..1, y up */
vec2 sc = vec2(res.x / max(res.y, 1.0), 1.0); /* aspect, in height units */
vec2 q = uv * sc;
/* The shaft's three anchors. The entry sways slowly on its own above the
top edge; the landing point is the cursor whenever one is present, and
a slow independent drift near the lower-left when it is not; the elbow
falls straight down from the entry to a height between the two. */
float topX = 0.60 + 0.05 * sin(u_time * 0.05);
vec2 top = vec2(topX, 1.10) * sc;
vec2 landIdle = vec2(
0.20 + 0.05 * sin(u_time * 0.037 + 1.7),
-0.04 + 0.02 * sin(u_time * 0.061)
);
vec2 land = mix(landIdle, u_pointer.xy, u_pointer.z) * sc;
vec2 elbow = vec2(top.x, mix(top.y, land.y, ELBOW_FRAC));
/* Walk the two legs as a short chain, swaying each vertex a little in x
on a slow low-frequency fbm — the wobble that reads as light in motion
rather than a ruled line. The sway tapers to zero at both ends so the
entry and the landing point stay exactly put. */
vec2 prev = top;
float d = 1e5;
float bestT = 0.0;
for (int i = 1; i < NPTS; i++) {
float tt = float(i) / float(NPTS - 1);
vec2 base;
if (i <= SEG) {
base = mix(top, elbow, float(i) / float(SEG));
} else {
base = mix(elbow, land, float(i - SEG) / float(SEG));
}
float taper = smoothstep(0.0, 0.1, tt) * smoothstep(1.0, 0.9, tt);
base.x += fbm(vec2(tt * 3.1 + 4.7, u_time * 0.10)) * 0.05 * taper;
float h;
float dd = distSegT(q, prev, base, h);
if (dd < d) {
d = dd;
bestT = (float(i - 1) + h) / float(NPTS - 1);
}
prev = base;
}
/* Fuller at both ends of the fall — the source, and where it pours in —
the way a poured liquid gathers at either end and runs thin between. */
float widthK = 1.0
+ 0.9 * smoothstep(0.85, 1.0, bestT)
+ 0.35 * smoothstep(0.12, 0.0, bestT);
float dK = d / widthK;
/* A pulse travels down the shaft's length over time — the "liquid" in
liquid light: it runs through the beam, not just sits in it. */
float drip = pow(0.5 + 0.5 * sin(bestT * 5.0 * 6.2831853 - u_time * 0.6), 5.0);
float core = exp(-dK * dK / (0.0035 * 0.0035));
float body = exp(-dK * dK / (0.014 * 0.014));
float glow = exp(-dK / 0.08);
vec3 col = u_ground;
col += u_deep * glow * 0.9;
col += u_beam * body * (1.0 + drip * 0.5) * 1.5;
col += u_core * core * (1.0 + drip * 0.4) * 1.3;
/* Where it lands: a small pool of the same light, brightest with the
cursor actually sitting in it — the one place the interaction reads
unambiguously as "this beam is answering me". */
vec2 toLand = q - land;
float pool = exp(-dot(toLand, toLand) / (0.05 * 0.05));
col += u_beam * pool * 0.5;
col += u_core * pool * pool * 0.4;
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 ShaftFieldProps {
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 ShaftField({ className, guardSelector = null }: ShaftFieldProps) {
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 SilkField / EmberField: once painted,
the last frame stays on screen while the surface is parked
off-view, so scrolling away and back does not crossfade to the
static floor. 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(_115deg,transparent_0%,transparent_45%,oklch(0.62_0.19_30_/_0.6)_48%,transparent_52%_),linear-gradient(_200deg,transparent_0%,transparent_56%,oklch(0.62_0.19_30_/_0.55)_59%,transparent_64%_),radial-gradient(_30%_20%_at_20%_88%,oklch(0.68_0.2_40_/_0.5)_0%,transparent_70%_),oklch(0.1_0.02_210)]" />
<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