IrisBackgroundsLight Current
Light Current
Curved long-exposure light trails sweeping down the frame, warm on one bank, cool on the other.
Light Current
A bundle of tall, curved streaks sweep down the frame in one shared bend — warm amber and coral on the left bank, cooling through violet into blue-cyan on the right, over a ground that darkens the same way. The look of a slow-shutter photograph of traffic on a curving road.
Nearby streaks bow toward the cursor and their local flow speeds up — a bolder, fewer, curved take on the same pull the finer trail fields use.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { CurrentField } from "./CurrentField";
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">
<CurrentField />
</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 current of long-exposure light trails — a bundle of tall,
* curved streaks sweeping down the frame in one shared bend, warm amber and
* coral on the left bank, cooling through violet into blue-cyan on the
* right, over a ground that darkens the same way. The look of a slow-shutter
* photograph of traffic on a curving road.
*
* The pointer is a real distortion of the current, not an overlay: nearby
* streaks bow toward the cursor and their local flow speeds up, the same
* pull `FilamentField` and `SilkField` use, just applied to a bolder, fewer,
* curved set of trails instead of a fine woven one.
*
* One of the reusable background fields (`TileField`, `SilkField`,
* `AuroraVeil`, `SpineField`, `SlabField`, `RakeField`, `OpalField`,
* `FilamentField`, `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-currentfield__floor` underneath visible (a
* still composed frame in the same palette, never a blank box).
*
* Like `FilamentField` / `RakeField` (and unlike `SilkField`), the palette is
* a fixed set passed as uniforms rather than read from the token ramp — the
* warm-to-cool split lives nowhere in the site's own accent ramp.
*
* 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_groundWarm: [0.028, 0.014, 0.012], // near-black, the warm (left) bank
u_groundCool: [0.008, 0.014, 0.03], // near-black, the cool (right) bank
u_amber: [0.98, 0.52, 0.16],
u_coral: [0.98, 0.3, 0.42],
u_violet: [0.56, 0.34, 0.96],
u_cyan: [0.28, 0.72, 0.98],
};
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_groundWarm;
uniform vec3 u_groundCool;
uniform vec3 u_amber;
uniform vec3 u_coral;
uniform vec3 u_violet;
uniform vec3 u_cyan;
uniform vec4 u_readA;
uniform float u_guard;
const int LAYERS = 5;
/* Warm-to-cool ramp, sampled by actual screen x — so a streak's colour
drifts along its own length as the shared bend below carries it from one
bank to the other, the way a single light trail in the reference shifts
hue as the road curves. */
vec3 bankColor(float x) {
vec3 c = mix(u_amber, u_coral, smoothstep(0.05, 0.42, x));
c = mix(c, u_violet, smoothstep(0.38, 0.68, x));
c = mix(c, u_cyan, smoothstep(0.62, 0.98, x));
return c;
}
/* One layer of near-vertical streaks riding the shared bend. Returns the
soft body; writes the near-white-hot core into 'core'. */
float streaks(vec2 p, float seed, float freq, float speed, float t, float kick, out float core) {
/* 'kick' is a bounded local phase nudge (not a per-pixel speed
multiplier) — multiplying speed itself by a spatially-varying amount
would compound against the absolute run time and fracture the lanes
near the cursor once the surface has been running for more than a few
seconds, since neighbouring pixels drift out of phase faster than the
eye can read as "sped up". Nudging phase directly stays bounded
regardless of how long the surface has been animating. */
float flow = p.y - t * speed - kick;
float lane = p.x * freq + seed * 7.0;
float laneId = floor(lane);
/* each lane wobbles a little of its own, so the bundle reads as separate
ribbons rather than one ruled grid */
float wob = fbm(vec2(flow * 0.45, laneId * 1.3 + seed)) * 0.32;
float dx = (fract(lane) - 0.5) + wob;
float body = exp(-dx * dx * 170.0);
core = exp(-dx * dx * 1300.0);
/* long bright runs broken by gaps, per lane */
float m = fbm(vec2(flow * 0.7 + laneId * 4.0, laneId * 2.0 + seed * 3.0));
float bright = smoothstep(0.0, 0.58, m);
bright *= step(0.2, dotHash(vec2(laneId, seed * 13.0)));
core *= bright;
return body * bright;
}
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 * 24.0);
/* the cursor nudges the current, not the whole coordinate frame — a radial
warp here (as SilkField/FilamentField use on their fine, high-frequency
content) opens a visible empty ring on this field's few, thick, bold
streaks, so the offset is kept small and is a lean rather than a fold */
vec2 Pw = P - toP * pull * 0.12;
/* the local phase nudge the cursor gives nearby streaks — see the note on
'kick' in streaks() for why this is additive rather than a speed
multiplier */
float kick = pull * 0.8;
/* the one shared curve every streak rides — a slow two-frequency sweep,
so the whole bundle bends together like a single stretch of road, not a
field of independent wiggles */
float bend = sin(Pw.y * 1.55 + u_time * 0.05) * 0.5
+ sin(Pw.y * 0.5 - u_time * 0.018) * 0.34;
vec2 Pb = vec2(Pw.x - bend, Pw.y);
vec3 col = mix(u_groundWarm, u_groundCool, smoothstep(0.0, 1.0, uv.x));
float hot = 0.0;
for (int i = 0; i < LAYERS; i++) {
float fi = float(i);
float k = fi / float(LAYERS - 1); /* 0..1 across layers */
float seed = fi * 1.618;
float freq = mix(1.6, 5.4, k);
float speed = mix(0.05, 0.14, fract(seed * 2.0));
float core;
float s = streaks(Pb, seed, freq, speed, u_time, kick, core);
float w = mix(0.4, 1.2, k);
vec3 tint = bankColor(uv.x);
col += tint * s * w * 1.6;
col += tint * s * s * w * 0.6; /* a little bloom off the body */
hot += core * w;
}
col += vec3(1.0, 0.98, 0.95) * hot * 0.9; /* near-white core */
/* the touch: a bloom in the local bank colour, a tight core exactly at the
cursor, and the nearest streak cores flare a little harder */
vec3 touchTint = bankColor(u_pointer.x);
col += touchTint * pull * 0.65;
float touchCore = exp(-dP * dP * 40.0) * u_pointer.z;
col += vec3(1.0, 0.98, 0.95) * touchCore * 0.4;
col += vec3(1.0, 0.98, 0.95) * pull * hot * 0.6;
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 md = mix(max(rd.x, rd.y), length(rd), 0.4);
float band = 1.0 - smoothstep(0.72, 2.1, md);
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 CurrentFieldProps {
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 CurrentField({ className, guardSelector = null }: CurrentFieldProps) {
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 / FilamentField: 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"
data-print="hide"
>
<div className="absolute inset-0 [background:linear-gradient(_100deg,oklch(0.05_0.02_40)_0%,oklch(0.62_0.17_55)_18%,oklch(0.6_0.19_15)_38%,oklch(0.55_0.19_300)_60%,oklch(0.62_0.14_230)_80%,oklch(0.035_0.03_250)_100%_)] after:content-[''] after:absolute after:inset-0 after:[background:repeating-linear-gradient(_12deg,transparent_0_10px,oklch(0.98_0.01_90_/_0.14)_10px_13px_)] after:[-webkit-mask-image:linear-gradient(0deg,#000_0_60%,transparent_100%)] after:[mask-image:linear-gradient(0deg,#000_0_60%,transparent_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>
);
}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