docs / atmospheres / loimu
Loimu
light arrives from something enormous and off-frame, drawn into long parallel streamers that decay to nothing before the edge. it expects a scrim over it, keeping one diagonal wedge and cutting the rest: the scrim does that work, never the atmosphere.
intensity · roomrsc · client
provenance
authored in usvalayer
atmospheresintensity
room · the whole environment. nothing competes with itcomposition
the ground of a hero or a full page, with the page's own scrim on topcontent goes in as children, above the sheet in normal flownever unmasked full-frame: without the scrim it lies about where the light livesnot inside a Card or a panel. it is the room, one per pagea11y
the canvas isaria-hidden · reduced motion paints one static developed frame · without WebGL2 the canvas never mountsjest-axedependencies
ogl · atmospheres-core from the same packagelive demo · try it out
light from off-frame
the scrim keeps one diagonal wedge and destroys the rest
customize
speedflow and fold rate
opacitysheet strength, 0 to 1
interactivestreamers bend toward the pointer
modeauto reads the ground, or force emit / stain
scrimthe three-layer CSS mask on top
bodythe sheet, reads accent
deepwhere the light is oldest
edgethe leading lines
advanced27
focalcamera focal length, larger flattens
sheetDisteye to sheet plane along its normal
sheetSpanhalf depth of the marched neighbourhood
sigmagaussian thickness of the sheet
foldhow far the low fold bends it out of plane
foldScalespatial frequency of that fold
normal · xsheet plane normal, x
normal · ysheet plane normal, y
normal · zsheet plane normal, z
flow · xflow axis projected onto the plane, x
flow · yflow axis projected onto the plane, y
flow · zflow axis projected onto the plane, z
source · xemission source, half-height units, x
source · yemission source, half-height units, y
noiseFreqbase frequency of the streamer noise
stretchdomain stretch along flow, below 4 loses aurora
curlScalespatial scale of the curl field
curlAmtstrength of the curl advection
flowSpeedbase advection speed along the flow
omegastrength of the pointer vortex
thresholdcutoff below which the sheet is void
sharpencontrast on the streamer edges
falloffinverse-square arrival, higher decays sooner
gainoverall emission gain
edgeweight of the thin leading line
edgeBandshow many leading lines per streamer
flowLengthdistance over which the hue completes
usage
import { Loimu } from "usva/atmospheres/loimu";
<Loimu
speed={1}
opacity={1}
interactive={true}
>
<div aria-hidden className="pointer-events-none absolute inset-0" style={{ backgroundImage: SCRIM }} />
<Hero />
</Loimu>props
| prop | type | default | notes |
|---|---|---|---|
| children | ReactNode | — | rendered above the sheet, in normal flow. |
| speed | number | 1 | flow and fold rate multiplier. |
| interactive | boolean | true | the streamers bend toward your cursor from a distance, following it with a lag. |
| opacity | number | 1 | overall opacity of the sheet, 0 to 1. |
| mode | "emissive" | "absorptive" | — | forces the blend. by default a dark ground emits and a light ground stains, which is the only way this survives a light theme. |
| colors | { body?; deep?; edge? } | — | the hue along the flow: body reads accent, deep reads accent-2 where the light is oldest, edge reads accent-alt on the leading lines. omitted stops read their token. |
| params | Partial<LoimuParams> | — | escape hatch for the field: source, sigma, fold, curlAmt, omega, edge and the rest. the defaults are the tuned sheet. |
get it
npx shadcn add https://usva.build/r/loimu.jsonsource · components/ui/loimu-field.tsexactly what this command copies
import type { Rgb } from "./atmospheres-color";
/**
* loimu is a blaze: a light sheet hanging in space, lit from a source the size
* of a weather system sitting just past the top right corner. Nothing here is a
* vignette. The source is a real point in front of the sheet, the streamers are
* anisotropic noise advected by a divergence-free curl field, and the hue rides
* the distance already travelled along the streamline.
*/
export interface LoimuParams {
/** Camera focal length. Larger flattens the perspective. */
focal: number;
/** Distance from the eye to the sheet plane along its normal. */
sheetDist: number;
/** Half depth of the marched neighbourhood around the sheet. */
sheetSpan: number;
/** Gaussian thickness of the sheet. */
sigma: number;
/** How far the low-frequency fold bends the sheet out of plane. */
fold: number;
foldScale: number;
/** Sheet plane normal, normalised in the shader. */
normal: [number, number, number];
/** Flow axis. Projected onto the sheet plane in the shader. */
flow: [number, number, number];
/** Emission source, in half-height units of screen space, x scaled by aspect. */
source: [number, number];
noiseFreq: number;
/** Domain stretch along the flow axis. Below ~4 this stops reading as aurora. */
stretch: number;
curlScale: number;
curlAmt: number;
flowSpeed: number;
/** Strength of the pointer vortex added to the curl field. */
omega: number;
threshold: number;
sharpen: number;
/** Inverse-square arrival from the source. Higher decays to void sooner. */
falloff: number;
gain: number;
/** Weight of the thin leading line where one streamer edge slides over another. */
edge: number;
edgeBands: number;
/** Distance along the flow over which the hue completes its ramp. */
flowLength: number;
}
export const LOIMU_DEFAULTS: LoimuParams = {
focal: 1.5,
sheetDist: 3,
sheetSpan: 1.6,
sigma: 0.8,
fold: 0.55,
foldScale: 0.22,
normal: [0.3, 0.4, 0.87],
flow: [-0.86, -0.5, 0],
source: [1.28, 0.92],
noiseFreq: 1.5,
stretch: 12,
curlScale: 0.35,
curlAmt: 0.7,
flowSpeed: 0.28,
omega: 1.6,
threshold: 0.33,
sharpen: 2.2,
falloff: 0.045,
gain: 14,
edge: 0.3,
edgeBands: 2,
flowLength: 7,
};
export interface LoimuColors {
/** The body of the sheet. */
body: Rgb;
/** Where the light is oldest and thinnest. */
deep: Rgb;
/** The thin line on a streamer's leading edge. */
edge: Rgb;
}
export const POINTER_EASE = 0.05;
export function approach(
current: number,
target: number,
ease: number,
): number {
return current + (target - current) * ease;
}
export function resolveParams(overrides?: Partial<LoimuParams>): LoimuParams {
return { ...LOIMU_DEFAULTS, ...overrides };
}
source · components/ui/loimu-shader.tsexactly what this command copies
import { glsl } from "./atmospheres-glsl";
/** Eight taps. The sheet is thin, so a long march would only oversample void. */
const TAPS = 8;
export const loimuFragmentShader = /* glsl */ `#version 300 es
precision highp float;
uniform float uTime;
uniform vec2 uResolution;
uniform vec2 uMouse;
uniform float uPointer;
uniform float uFocal;
uniform float uSheetDist;
uniform float uSheetSpan;
uniform float uSigma;
uniform float uFold;
uniform float uFoldScale;
uniform vec3 uNormal;
uniform vec3 uFlow;
uniform vec2 uSource;
uniform float uNoiseFreq;
uniform float uStretch;
uniform float uCurlScale;
uniform float uCurlAmt;
uniform float uFlowSpeed;
uniform float uOmega;
uniform float uThreshold;
uniform float uSharpen;
uniform float uFalloff;
uniform float uGain;
uniform float uEdge;
uniform float uEdgeBands;
uniform float uFlowLength;
uniform float uAlpha;
uniform float uAbsorb;
uniform vec3 uBody;
uniform vec3 uDeep;
uniform vec3 uEdgeColor;
out vec4 fragColor;
${glsl("fbm", "curl", "isoband", "dither", "tonemap", "composite")}
/** Where a ray leaving the eye crosses the sheet plane. Grazing rays would run
* to infinity, so the denominator is floored rather than discarded: the arrival
* falloff has already taken those pixels to zero anyway. */
float sheetHit(vec3 dir, vec3 nrm) {
return uSheetDist / max(dot(dir, nrm), 0.08);
}
void main() {
float aspect = uResolution.x / max(uResolution.y, 1.0);
vec2 ndc = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
vec3 dir = normalize(vec3(ndc, uFocal));
vec3 nrm = normalize(uNormal);
vec3 flowAxis = normalize(uFlow - nrm * dot(uFlow, nrm));
vec3 crossAxis = cross(nrm, flowAxis);
vec3 srcDir = normalize(vec3(uSource.x * aspect, uSource.y, uFocal));
vec3 source = srcDir * sheetHit(srcDir, nrm);
vec3 mouseDir = normalize(vec3(uMouse, uFocal));
vec3 mouse = mouseDir * sheetHit(mouseDir, nrm);
float hit = sheetHit(dir, nrm);
float stepLen = (2.0 * uSheetSpan) / float(${TAPS});
float jitter = ign(gl_FragCoord.xy);
vec3 col = vec3(0.0);
float acc = 0.0;
vec3 sheetPoint = dir * hit;
vec3 vel = curl(sheetPoint * uCurlScale + vec3(0.0, 0.0, uTime * 0.05), 0.35);
vec2 baseVel = vec2(dot(vel, flowAxis), dot(vel, crossAxis));
for (int i = 0; i < ${TAPS}; i++) {
float t = hit - uSheetSpan + (float(i) + jitter) * stepLen;
vec3 p = dir * t;
float bend = fbm(p * uFoldScale + vec3(0.0, 0.0, uTime * 0.02), 2) - 0.5;
float s = dot(p, nrm) - uSheetDist - bend * uFold;
float shell = exp(-(s * s) / (uSigma * uSigma));
if (shell < 0.002) continue;
vec3 toMouse = p - mouse;
vec2 mAB = vec2(dot(toMouse, flowAxis), dot(toMouse, crossAxis));
vec2 sheetVel = baseVel + clamp(
uPointer * uOmega * vec2(-mAB.y, mAB.x) / (1.0 + dot(mAB, mAB)),
vec2(-0.65),
vec2(0.65)
);
vec2 disp = sheetVel * uCurlAmt;
vec3 q = p + disp.x * flowAxis + disp.y * crossAxis
- flowAxis * (uFlowSpeed * uTime);
vec3 domain = vec3(
dot(q, flowAxis) / uStretch,
dot(q, crossAxis),
dot(q, nrm) * 0.6
) * uNoiseFreq;
float fb = fbm(domain, 3);
float along = domain.x;
float across = domain.y;
float tear = (fbm(vec3(along * 0.32, across * 1.8, domain.z), 3) - 0.5) * 0.6;
float d0 = across + 0.62 + tear;
float d1 = across - 0.05 + tear * 0.7;
float d2 = across - 0.72 - tear * 0.5;
float lanes = exp(-5.0 * d0 * d0)
+ 0.85 * exp(-7.0 * d1 * d1)
+ 0.65 * exp(-6.0 * d2 * d2);
float body = pow(smoothstep(uThreshold, 0.9, fb), uSharpen);
float rayWave = 0.5 + 0.5 * sin(along * 18.0 - uTime * 1.4);
float rays = 0.72 + 0.28 * rayWave * rayWave * rayWave;
float density = shell * (0.15 + 0.85 * lanes) * (0.08 + 0.92 * body) * rays;
float flow = clamp(dot(q - source, flowAxis) / uFlowLength, 0.0, 1.0);
vec3 hue = mix(uBody, uDeep, flow);
hue += uEdgeColor * isobands(fb, uEdgeBands) * uEdge;
float reach = length(p - source);
float alongReach = dot(p - source, flowAxis);
float launched = smoothstep(-1.2, 0.8, alongReach);
float arrival = launched / (1.0 + uFalloff * reach * reach);
col += hue * density * arrival * stepLen;
acc += density * arrival * stepLen;
}
col *= uGain;
float amount = clamp(acc * uGain, 0.0, 1.0);
// Tonemapped on luminance, not per channel, so a bright pass saturates
// toward the accent instead of washing to white. sisu must not bloom.
float lum = max(col.r, max(col.g, col.b));
float peak = 1.0 - exp(-lum);
vec3 tint = col / max(lum, 1e-4);
vec3 rgb = dither(mix(tint, tint * 0.78, uAbsorb), gl_FragCoord.xy, 0.006);
float alpha = mix(peak, amount * 0.85, uAbsorb) * uAlpha;
alpha = ditherAlpha(alpha, gl_FragCoord.xy, 0.004);
fragColor = composite(rgb, alpha);
}
`;
source · components/ui/loimu-uniforms.tsexactly what this command copies
import {
setUniform,
type Uniforms,
} from "./atmospheres-gl";
import type { LoimuColors, LoimuParams } from "./loimu-field";
export interface LoimuFrame {
time: number;
/** Eased pointer in half-height screen units, y up. */
mouse: [number, number];
/** Fades the vortex in and out, 0..1. */
pointer: number;
alpha: number;
/** The house law: 1 stains the ground, 0 emits into it. */
absorb: number;
}
export function loimuUniforms(
colors: LoimuColors,
params: LoimuParams,
): Uniforms {
return {
uTime: { value: 0 },
uResolution: { value: [1, 1] },
uMouse: { value: [0, 0] },
uPointer: { value: 0 },
uFocal: { value: params.focal },
uSheetDist: { value: params.sheetDist },
uSheetSpan: { value: params.sheetSpan },
uSigma: { value: params.sigma },
uFold: { value: params.fold },
uFoldScale: { value: params.foldScale },
uNormal: { value: [...params.normal] },
uFlow: { value: [...params.flow] },
uSource: { value: [...params.source] },
uNoiseFreq: { value: params.noiseFreq },
uStretch: { value: params.stretch },
uCurlScale: { value: params.curlScale },
uCurlAmt: { value: params.curlAmt },
uFlowSpeed: { value: params.flowSpeed },
uOmega: { value: params.omega },
uThreshold: { value: params.threshold },
uSharpen: { value: params.sharpen },
uFalloff: { value: params.falloff },
uGain: { value: params.gain },
uEdge: { value: params.edge },
uEdgeBands: { value: params.edgeBands },
uFlowLength: { value: params.flowLength },
uAlpha: { value: 1 },
uAbsorb: { value: 0 },
uBody: { value: [...colors.body] },
uDeep: { value: [...colors.deep] },
uEdgeColor: { value: [...colors.edge] },
};
}
export function setLoimuColors(u: Uniforms, colors: LoimuColors): void {
setUniform(u, "uBody", [...colors.body]);
setUniform(u, "uDeep", [...colors.deep]);
setUniform(u, "uEdgeColor", [...colors.edge]);
}
export function setLoimuParams(u: Uniforms, params: LoimuParams): void {
setUniform(u, "uFocal", params.focal);
setUniform(u, "uSheetDist", params.sheetDist);
setUniform(u, "uSheetSpan", params.sheetSpan);
setUniform(u, "uSigma", params.sigma);
setUniform(u, "uFold", params.fold);
setUniform(u, "uFoldScale", params.foldScale);
setUniform(u, "uNormal", [...params.normal]);
setUniform(u, "uFlow", [...params.flow]);
setUniform(u, "uSource", [...params.source]);
setUniform(u, "uNoiseFreq", params.noiseFreq);
setUniform(u, "uStretch", params.stretch);
setUniform(u, "uCurlScale", params.curlScale);
setUniform(u, "uCurlAmt", params.curlAmt);
setUniform(u, "uFlowSpeed", params.flowSpeed);
setUniform(u, "uOmega", params.omega);
setUniform(u, "uThreshold", params.threshold);
setUniform(u, "uSharpen", params.sharpen);
setUniform(u, "uFalloff", params.falloff);
setUniform(u, "uGain", params.gain);
setUniform(u, "uEdge", params.edge);
setUniform(u, "uEdgeBands", params.edgeBands);
setUniform(u, "uFlowLength", params.flowLength);
}
export function setLoimuFrame(u: Uniforms, frame: LoimuFrame): void {
setUniform(u, "uTime", frame.time);
setUniform(u, "uMouse", [...frame.mouse]);
setUniform(u, "uPointer", frame.pointer);
setUniform(u, "uAlpha", frame.alpha);
setUniform(u, "uAbsorb", frame.absorb);
}
source · components/ui/loimu.tsxexactly what this command copies
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import {
type BlendMode,
blendStyleFor,
blendUniform,
type Rgb,
resolveBlendMode,
resolveColor,
} from "./atmospheres-color";
import { hiddenOnGround } from "./atmospheres-ground";
import { useGlCanvas } from "./use-gl-canvas";
import {
useThemeVersion,
useTokenColors,
} from "./use-token-colors";
import {
approach,
type LoimuColors,
type LoimuParams,
POINTER_EASE,
resolveParams,
} from "./loimu-field";
import { loimuFragmentShader } from "./loimu-shader";
import {
loimuUniforms,
setLoimuColors,
setLoimuFrame,
setLoimuParams,
} from "./loimu-uniforms";
const ROLES = ["accent", "accent-2", "accent-alt"] as const;
/** Far enough in that the still frame shows a developed sheet, not a seed. */
const STILL_TIME = 14;
export interface LoimuProps extends React.HTMLAttributes<HTMLDivElement> {
/** Flow and fold rate multiplier. Defaults to 1. */
speed?: number;
/** When on, the field swirls toward the eased cursor. Defaults to true. */
interactive?: boolean;
/** Overall opacity of the sheet, 0..1. Defaults to 1. */
opacity?: number;
/** Force the blend. Defaults to emissive on a dark ground, absorptive on a
* light one, which is the only way this survives a light theme. */
mode?: BlendMode;
/** Override any hue stop with a CSS colour. Omitted stops read their token. */
colors?: { body?: string; deep?: string; edge?: string };
/** Escape hatch for the field parameters, for tuning demos. */
params?: Partial<LoimuParams>;
children?: React.ReactNode;
}
export const Loimu = React.forwardRef<HTMLDivElement, LoimuProps>(
(
{
speed = 1,
interactive = true,
opacity = 1,
mode,
colors,
params,
className,
children,
...props
},
forwardedRef,
) => {
const cBody = colors?.body;
const cDeep = colors?.deep;
const cEdge = colors?.edge;
const speedRef = React.useRef(speed);
speedRef.current = speed;
const interactiveRef = React.useRef(interactive);
interactiveRef.current = interactive;
const opacityRef = React.useRef(opacity);
opacityRef.current = opacity;
const paramsRef = React.useRef<LoimuParams>(resolveParams(params));
paramsRef.current = resolveParams(params);
const themeVersion = useThemeVersion();
const scopeRef = React.useRef<HTMLDivElement | null>(null);
const tokens = useTokenColors(ROLES, { scopeRef });
const blend = resolveBlendMode(mode, tokens.bg);
// biome-ignore lint/correctness/useExhaustiveDependencies: a theme swap re-resolves the same colour strings to new channels.
const ramp = React.useMemo<LoimuColors>(() => {
const stop = (value: string | undefined, fallback: Rgb): Rgb =>
value ? resolveColor(value) : fallback;
return {
body: stop(cBody, tokens.colors.accent),
deep: stop(cDeep, tokens.colors["accent-2"]),
edge: stop(cEdge, tokens.colors["accent-alt"]),
};
}, [cBody, cDeep, cEdge, tokens, themeVersion]);
const rampRef = React.useRef(ramp);
rampRef.current = ramp;
const blendRef = React.useRef(blend);
blendRef.current = blend;
const mouse = React.useRef<[number, number]>([0, 0]);
const canvas = useGlCanvas({
fragment: loimuFragmentShader,
uniforms: () => loimuUniforms(rampRef.current, paramsRef.current),
enabled: !hiddenOnGround("loimu", blend),
pointer: true,
pointerEase: POINTER_EASE,
stillTime: STILL_TIME,
maxDpr: 1.5,
renderScale: 0.7,
onFrame: (u, frame) => {
const half = Math.max(frame.height, 1) / 2;
if (interactiveRef.current) {
mouse.current[0] = approach(
mouse.current[0],
frame.pointer.x / half,
POINTER_EASE,
);
mouse.current[1] = approach(
mouse.current[1],
frame.pointer.y / half,
POINTER_EASE,
);
}
setLoimuColors(u, rampRef.current);
setLoimuParams(u, paramsRef.current);
setLoimuFrame(u, {
time: frame.time * speedRef.current,
mouse: mouse.current,
pointer: interactiveRef.current ? frame.pointer.amount : 0,
alpha: opacityRef.current,
absorb: blendUniform(blendRef.current),
});
},
});
const { redraw } = canvas;
// biome-ignore lint/correctness/useExhaustiveDependencies: the still frame must repaint when the ramp or the blend changes.
React.useEffect(() => {
redraw();
}, [redraw, ramp, blend]);
const on = canvas.active;
return (
<div
ref={(node) => {
canvas.containerRef.current = node;
scopeRef.current = node;
if (typeof forwardedRef === "function") forwardedRef(node);
else if (forwardedRef) forwardedRef.current = node;
}}
data-blend={blend}
className={cn("relative isolate overflow-hidden", className)}
{...props}
>
{on ? (
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 -z-10"
>
<canvas
ref={canvas.canvasRef}
className="block h-full w-full"
style={blendStyleFor(blend)}
/>
</div>
) : null}
{children}
</div>
);
},
);
Loimu.displayName = "Loimu";