IrisBackgroundsNight Road
Night Road
Red-to-amber trails racing a real perspective toward a vanishing point; the pointer steers and throttles.
Night Road
Four warm red-to-amber streaks and one branching steel-grey outlier race up a genuine perspective projection toward a vanishing point inside the frame, a dashed lane divider foreshortening correctly as it recedes. Above the vanishing point is open black sky.
The pointer's x pans the vanishing point, so the whole road swings under you like turning a wheel; its y doubles as an accelerator, running the dashes and flicker faster the closer it sits to the bottom edge. The cursor itself drops a warm headlight bloom.
Install
No installation needed — self-contained, paste-in code.
Usage
Drop it straight into a page.
import { RoadField } from "./RoadField";
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">
<RoadField />
</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 field of long-exposure light trails converging on a road —
* four warm red-to-amber streaks and one branching steel-grey outlier,
* racing up a real perspective toward a vanishing point inside the frame, a
* dashed lane divider foreshortening correctly as it recedes. Above the
* vanishing point is open black sky, exactly like the reference photograph
* this was built from.
*
* The perspective is a genuine projection, not a texture: every lane centre
* and every line's width is derived from one `invZ(depth)` term that goes 1
* at the camera (the bottom edge) to 0 exactly at the vanishing point, so
* the bundle narrows to a single point rather than fading out early. World
* depth (`wz`, the inverse of that same term) drives the dash spacing and
* the flicker along each trail, which is why the dashes compress correctly
* as they approach the horizon instead of staying evenly spaced in screen
* space.
*
* Two pointer interactions, both literal driving metaphors rather than a
* generic hover glow:
* - steering — the pointer's x pans the vanishing point, so the whole road
* swings under you the way it does turning a wheel.
* - throttle — the pointer's y doubles as an accelerator: the nearer it
* sits to the bottom edge (closer to the camera), the faster the dashes
* and the trail flicker run.
* A third, smaller touch: the cursor itself drops a warm headlight bloom
* with a near-white core, the same "your touch joined the light" move
* `ArcLightsField` and `RakeField` make with their own pointer lights.
*
* One of the reusable background fields (`TileField`, `SilkField`,
* `RakeField`, `FilamentField`, `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-roadfield__floor` underneath visible (a still
* composed frame in the same palette, never a blank box).
*
* Like `RakeField` / `FilamentField` (and unlike `SilkField`), the palette is
* a fixed set passed as uniforms rather than read from the token ramp — the
* near-white flare core and the steel outlier live nowhere in the accent
* ramp, so the CSS floor carries the colour rather than a live token read.
*
* 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), so copy laid
* over the field holds WCAG AA with no overlay layer. `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_black: [0.014, 0.011, 0.011], // the near-black ground between the trails
u_ember: [0.28, 0.045, 0.03], // the dim inner-lane red and the road haze
u_crimson: [0.78, 0.1, 0.06], // the mid-warm trail colour
u_amber: [0.98, 0.42, 0.11], // the hotter outer-lane trail and pointer bloom
u_flare: [1.0, 0.9, 0.76], // the near-white core riding every trail's centre
u_steel: [0.58, 0.63, 0.7], // the branching outlier lane
u_line: [0.82, 0.78, 0.7], // the dashed lane divider
};
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_black;
uniform vec3 u_ember;
uniform vec3 u_crimson;
uniform vec3 u_amber;
uniform vec3 u_flare;
uniform vec3 u_steel;
uniform vec3 u_line;
/* Reading region for the contrast guard: xy = centre (uv, y up),
zw = half-extent. Measured from the live copy block every frame.
u_guard is 0 when the guard is off. */
uniform vec4 u_readA;
uniform float u_guard;
const float HORIZON = 0.70; /* uv.y of the vanishing point */
const float VP_X = 0.085; /* the point's x offset from centre, P-space */
const float ROAD_W = 0.62; /* road half-width at the camera, P-space */
const float MIN_BW = 0.004; /* width floor so the bundle never divides by zero */
/* One light trail. 'dx' is the pixel's screen-space offset from the lane
centre, 'sigma' its half-width. 'wz' (world depth) drives a slow flicker
along its length, so the trail reads as still travelling rather than a
painted-on streak — 'spd' lets the throttle speed that travel up. */
float trail(float dx, float sigma, float wz, float seed, float spd, out float core) {
float s2 = sigma * sigma + 1e-7;
float body = exp(-dx * dx / s2);
core = exp(-dx * dx / (s2 * 0.08));
float flicker = 0.76 + 0.24 * sin(wz * 0.5 - u_time * spd + seed * 7.0);
return body * flicker;
}
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);
/* steering: the pointer's x pans the vanishing point, the way turning a
wheel swings the road under you. u_pointer is already eased upstream
(shader-surface's shared pointer), so this needs no smoothing of its
own. throttle: the pointer's y doubles as an accelerator — nearer the
bottom edge (closer to the camera) reads as pressing it down. */
float steer = (u_pointer.x - 0.5) * u_pointer.z * 0.5;
float speed = 1.0 + u_pointer.z * (1.0 - u_pointer.y) * 2.4;
float vpX = VP_X + steer;
/* depth: 0 at the camera, 1 exactly at the vanishing point. A power curve
so the convergence accelerates the way a real lens does, not a ramp. */
float d = uv.y / HORIZON;
float invZ = pow(clamp(1.0 - d, 0.0, 1.0), 1.35); /* 1 near .. 0 far */
float aboveHorizon = 1.0 - smoothstep(0.9, 1.02, d); /* fades to black sky */
/* world depth: the inverse of invZ, capped rather than left to explode —
grows quickly near the horizon, which is what makes the dashes and the
flicker compress correctly as they recede. */
float wz = (1.0 - invZ) / max(invZ, 0.02);
float roadCenterX = vpX * (1.0 - invZ);
float bw = max(ROAD_W * invZ, MIN_BW);
/* five lane centres in absolute screen space, as offsets of the bundle's
own half-width — so they converge to the same point together. The
outlier gets an extra term that grows with distance instead: it drifts
off the bundle rather than joining it, the branching grey streak that
peels away in the reference photograph. */
float lane1 = roadCenterX - bw * 0.86;
float lane2 = roadCenterX - bw * 0.30;
float lane3 = roadCenterX + bw * 0.34;
float lane4 = roadCenterX + bw * 0.80;
float laneDash = roadCenterX - bw * 0.06;
float laneOutlier = roadCenterX + bw * 0.95 + 0.16 * (1.0 - invZ) * (1.0 - invZ);
float c1, c2, c3, c4, cO;
float e1 = trail(P.x - lane1, bw * 0.100, wz, 1.3, 0.9 * speed, c1);
float e2 = trail(P.x - lane2, bw * 0.085, wz, 2.7, 0.8 * speed, c2);
float e3 = trail(P.x - lane3, bw * 0.115, wz, 4.1, 1.0 * speed, c3);
float e4 = trail(P.x - lane4, bw * 0.090, wz, 5.5, 0.85 * speed, c4);
float eO = trail(P.x - laneOutlier, bw * 0.05 + 0.0015, wz, 6.9, 0.7 * speed, cO) * 0.8;
vec3 col = u_black;
/* a faint warm haze under the bundle near the camera — nothing here is
pure black, the same discipline RakeField and FilamentField use */
float haze = smoothstep(0.55, 0.0, d) * exp(-abs(P.x - roadCenterX) * 1.2);
col += u_ember * haze * 0.10;
col += mix(u_ember, u_crimson, 0.5) * e1 * 1.5 * aboveHorizon;
col += u_crimson * e2 * 1.6 * aboveHorizon;
col += mix(u_crimson, u_amber, 0.5) * e3 * 1.6 * aboveHorizon;
col += u_amber * e4 * 1.5 * aboveHorizon;
col += u_steel * eO * 1.1 * aboveHorizon;
float hotCore = (c1 + c2 + c3 + c4) * aboveHorizon;
col += u_flare * hotCore * 0.7;
col += u_flare * cO * aboveHorizon * 0.3;
/* the dashed lane divider — spaced and animated in world depth, so it
compresses toward the horizon exactly the way the trails converge. */
float dashSigma = bw * 0.045 + 0.0012;
float dxD = P.x - laneDash;
float lineBody = exp(-dxD * dxD / (dashSigma * dashSigma + 1e-7));
float dp = fract(wz * 0.55 - u_time * 0.9 * speed);
float dashOn = clamp(smoothstep(0.0, 0.05, dp) - smoothstep(0.55, 0.60, dp), 0.0, 1.0);
col += u_line * lineBody * dashOn * aboveHorizon * 1.3;
/* the touch: a warm headlight bloom with a near-white core, the same
"your touch joined the light" move ArcLightsField and RakeField make */
vec2 ptr = (u_pointer.xy - 0.5) * vec2(aspect, 1.0);
vec2 toPtr = P - ptr;
float dPtr = dot(toPtr, toPtr);
col += u_amber * u_pointer.z * exp(-dPtr * 9.0) * 0.5;
col += u_flare * u_pointer.z * exp(-dPtr * 40.0) * 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 RoadFieldProps {
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 RoadField({ className, guardSelector = null }: RoadFieldProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const guardOn = guardSelector != null;
/* The block the reading guard protects. Looked up once, lazily. */
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 / RakeField: 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:radial-gradient(_38%_55%_at_58%_70%,oklch(0.9_0.05_70)_0%,transparent_10%_),radial-gradient(_46%_70%_at_58%_70%,oklch(0.68_0.19_40)_0%,transparent_30%_),radial-gradient(_70%_95%_at_58%_70%,oklch(0.42_0.19_30)_0%,transparent_55%_),radial-gradient(_100%_100%_at_58%_70%,oklch(0.16_0.06_30)_0%,transparent_75%_),oklch(0.03_0.01_30)] after:content-[''] after:absolute after:inset-0 after:[background:repeating-linear-gradient(90deg,transparent_0_5%,oklch(0.08_0.02_30_/_0.6)_5%_8%)] after:[mask-image:radial-gradient(60%_85%_at_58%_70%,transparent_0_12%,#000_40%,transparent_78%)] after:[-webkit-mask-image:radial-gradient(60%_85%_at_58%_70%,transparent_0_12%,#000_40%,transparent_78%)]" />
<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