docs / atmospheres / vare
Väre
a few broad wavefronts cross the frame from off to the right and interfere. on a dark ground the crests glow; on savi the same ripples press into clay, troughs holding a warm damp pigment. it needs its irregularity: tune that out and the bands snap into a striped diagram.
intensity · roomrsc · client
provenance
authored in usvalayer
atmospheresintensity
room · the whole environment. nothing competes with itcomposition
a hero or full-page room, its content as children in normal flowdark and light themes both work; the blend resolves itself per groundnever behind dense reading surfaces or another atmospherenot a card texture. it is a room, one per pagea11y
the canvas layer isaria-hidden and pointer-events-none · reduced motion paints one still frame · without WebGL2 the canvas never mountsjest-axedependencies
ogl · atmospheres-core from the same packagelive demo · try it out
bands, not waves
move the pointer and the fronts warp as the lens passes
customize
speedwave rate multiplier
opacityband strength, 0 to 1
modeauto reads the ground, or force emit / stain
interactivea phase-space lens bends the bands toward the pointer
bodythe front of the ramp, reads accent
deepthe back of the ramp
edgethe bright nodes where fronts cross
advanced17
angleshared travel direction of the fronts, radians
spreadhalf-width of the k-vector fan, radians
wavenumberbase band frequency, each front scatters around it
field speedintrinsic travel rate of the fronts
warpamplitude of the domain warp that bends the field
warpScalespatial scale of that warp
jitterper-front phase drift, keeps fronts from lining up
softridge exponent, higher is a narrower boundary
detailfrequency of the band-thickness noise
nodeextra light where two fronts cross
gainoverall brightness of the ramp
source · xoff-frame origin x, half-height units by aspect
source · yoff-frame origin y, half-height units
falloffinverse-square decay, higher voids sooner
spandistance over which the hue ramps front to back
lensstrength of the pointer lens in phase space
lensSigmareach of that lens
usage
import { Vare } from "usva/atmospheres/vare";
<Vare
speed={1}
>
<Hero />
</Vare>props
| prop | type | default | notes |
|---|---|---|---|
| children | ReactNode | — | rendered above the bands, in normal flow. |
| speed | number | 1 | wave rate multiplier. |
| interactive | boolean | true | a lens in phase space follows the eased cursor and bends the bands as it passes. |
| opacity | number | 1 | overall opacity of the bands, 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? } | — | on dark ground, the front-to-back ramp. on savi, body combines with theme ink to make the trough pigment. omitted stops read their token. |
| params | Partial<VareParams> | — | the field: angle, spread, wavenumber, speed, warp, warpScale, jitter, soft, detail, node, gain, source, falloff, span, lens, lensSigma. |
get it
npx shadcn add https://usva.build/r/vare.jsonsource · components/ui/vare-field.tsexactly what this command copies
import type { Rgb } from "./atmospheres-color";
/**
* vare (väre, a ripple) emits from the iso-phase lines of a few broad travelling
* wavefronts, never from the wave bodies. What you see is a set of drifting
* luminous boundaries: the folded edges of a curtain, seen from very far away.
*
* The failure mode is moire. Every defence against it is a parameter here: the
* domain warp, the per-front phase jitter, the wavenumber scatter and the
* thickness noise. Turn them to zero and the interference snaps into a diagram.
*/
export interface VareParams {
/** Shared travel direction of the front, in radians. */
angle: number;
/** Half-width of the k-vector fan around that direction, in radians. */
spread: number;
/** Base wavenumber. Each front scatters around it, so no two bands match. */
wavenumber: number;
speed: number;
/** Amplitude of the FBM domain warp that bends the whole field. */
warp: number;
warpScale: number;
/** Per-front phase drift, in band periods. This is what stops two fronts from
* lining up, since the shared warp bends them all the same way. */
jitter: number;
/** Exponent of the cosine ridge. Higher is a narrower boundary; low melts
* the bands into one another. */
soft: number;
/** Frequency of the noise that varies band thickness along a front. */
detail: number;
/** Extra light where two fronts cross. */
node: number;
gain: number;
/** Off-frame origin of the fronts, in half-height units, x scaled by aspect. */
source: [number, number];
/** Inverse-square arrival from the source. Higher decays to void sooner. */
falloff: number;
/** Distance over which the hue completes its front-to-back ramp. */
span: number;
/** Strength of the pointer lens in phase space. */
lens: number;
lensSigma: number;
}
export const VARE_DEFAULTS: VareParams = {
angle: 3.64,
spread: 0.62,
wavenumber: 3.8,
speed: 0.9,
warp: 0.7,
warpScale: 0.55,
jitter: 0.7,
soft: 6.5,
detail: 1.6,
node: 0.9,
gain: 1.15,
source: [1.15, 0.8],
falloff: 0.09,
span: 4,
lens: 2.2,
lensSigma: 0.9,
};
export interface VareColors {
/** The body of the front. */
body: Rgb;
/** The oldest, furthest bands. */
deep: Rgb;
/** The freshest bands, closest to the source. */
edge: Rgb;
/** kosteus: the hue the clay takes where the trough holds water. */
pigment: Rgb;
}
export const POINTER_EASE = 0.06;
export function approach(
current: number,
target: number,
ease: number,
): number {
return current + (target - current) * ease;
}
export function resolveParams(overrides?: Partial<VareParams>): VareParams {
return { ...VARE_DEFAULTS, ...overrides };
}
source · components/ui/vare-shader.tsexactly what this command copies
import { glsl } from "./atmospheres-glsl";
const WAVES = 4;
export const vareFragmentShader = /* glsl */ `#version 300 es
precision highp float;
uniform float uTime;
uniform vec2 uResolution;
uniform vec2 uMouse;
uniform float uPointer;
uniform float uAngle;
uniform float uSpread;
uniform float uWavenumber;
uniform float uSpeed;
uniform float uWarp;
uniform float uWarpScale;
uniform float uJitter;
uniform float uSoft;
uniform float uDetail;
uniform float uNode;
uniform float uGain;
uniform vec2 uSource;
uniform float uFalloff;
uniform float uSpan;
uniform float uLens;
uniform float uLensSigma;
uniform float uAlpha;
uniform float uAbsorb;
uniform vec3 uBody;
uniform vec3 uDeep;
uniform vec3 uEdgeColor;
uniform vec3 uPigment;
uniform float uStainFloor;
out vec4 fragColor;
${glsl("fbm", "dither", "stain", "composite")}
const float TAU = 6.28318530718;
/** The raking key. Low and from the left, so a ridge one pixel high still casts. */
const vec3 KEY = vec3(-0.62, -0.44, 0.36);
/** How hard the clay drinks. Tuned so a node is damp, not a bruise. */
const float SIGMA = 1.15;
/** The wettest a ripple ever gets. Above this the ground stops being ground. */
const float SOAK = 0.45;
float phaseLens(vec2 p) {
vec2 toMouse = p - uMouse;
return uPointer * uLens
* exp(-dot(toMouse, toMouse) / (uLensSigma * uLensSigma));
}
vec2 waves(vec2 p, vec2 source) {
float lens = phaseLens(p);
float bands = 0.0;
float squares = 0.0;
float basins = 0.0;
float basinSquares = 0.0;
for (int i = 0; i < ${WAVES}; i++) {
float fi = float(i);
float angle = uAngle + (hash11(fi * 3.17 + 0.5) - 0.5) * uSpread;
float k = uWavenumber * (0.7 + 1.1 * hash11(fi * 7.31 + 1.7));
float w = uSpeed * (0.6 + 0.8 * hash11(fi * 11.7 + 4.3));
vec2 dir = vec2(cos(angle), sin(angle));
// The shared domain warp bends every front identically, so on its own it
// cannot stop two fronts beating against each other. This one is per front.
float drift = fbm2(
p * uWarpScale * 1.7 + vec2(fi * 21.7, uTime * 0.018), 2
) - 0.5;
vec2 bendAxis = normalize(vec2(
cos(angle + 1.5708 + fi * 0.31),
sin(angle + 1.5708 + fi * 0.31)
));
float bend = (fbm2(
p * (uWarpScale * (0.7 + 0.18 * fi)) + vec2(fi * 7.1),
2
) - 0.375) * (0.7 + 0.22 * fi);
float phase = dot(p - source + bendAxis * bend, dir) * k - w * uTime
+ hash11(fi * 5.13 + 2.9) * TAU + drift * uJitter * TAU + lens;
float thickness = 0.5 + 0.5
* fbm2(p * uDetail + vec2(fi * 13.0, uTime * 0.02), 2);
float weight = fi < 2.0 ? 1.0 : mix(0.4, 0.6, hash11(fi + 8.4));
// A soft cosine ridge, not a hairline: the boundary glows and falls off,
// and a faint square-law fill keeps the space between from reading as a
// diagram on black.
float wave = 0.5 + 0.5 * cos(phase);
float exponent = uSoft * (0.8 + 0.4 * hash11(fi * 2.3 + 0.7));
float crest = pow(wave, exponent);
float trough = pow(1.0 - wave, exponent);
float band = (crest + 0.1 * wave * wave) * thickness * weight;
float basinBand = trough * thickness * weight;
bands += band;
squares += band * band;
basins += basinBand;
basinSquares += basinBand * basinBand;
}
float overlap = max(0.5 * (bands * bands - squares), 0.0);
float nodes = smoothstep(0.3, 1.0, overlap);
nodes *= nodes;
float emission = bands + nodes * uNode;
float basinOverlap = max(0.5 * (basins * basins - basinSquares), 0.0);
float basinNodes = smoothstep(0.3, 1.0, basinOverlap);
basinNodes *= basinNodes;
float basin = basins + basinNodes * uNode;
return vec2(emission, basin);
}
void main() {
float aspect = uResolution.x / max(uResolution.y, 1.0);
vec2 ndc = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
vec2 warp = vec2(
fbm2(ndc * uWarpScale + vec2(uTime * 0.03, 0.0), 2),
fbm2(ndc * uWarpScale + vec2(9.7, uTime * 0.024), 2)
) - 0.5;
vec2 p = ndc + warp * uWarp;
vec2 source = vec2(uSource.x * aspect, uSource.y);
vec2 travel = vec2(cos(uAngle), sin(uAngle));
vec2 field = waves(p, source);
float emission = field.x;
float depth = clamp(dot(p - source, travel) / uSpan, 0.0, 1.0);
vec3 hue = mix(uBody, uDeep, depth);
hue = mix(uEdgeColor, hue, smoothstep(0.0, 0.3, depth));
float reach = length(p - source);
float arrival = 1.0 / (1.0 + uFalloff * reach * reach);
vec3 col = hue * emission * arrival * uGain;
// Tonemapped on luminance, not per channel, so a bright node 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 emissive = dither(tint, gl_FragCoord.xy, 0.006);
float emissiveAlpha = peak * uAlpha;
if (uAbsorb < 0.5) {
float alpha = ditherAlpha(emissiveAlpha, gl_FragCoord.xy, 0.004);
fragColor = composite(emissive, alpha);
return;
}
float eps = 2.0 / max(uResolution.y, 1.0);
float heightX = waves(p + vec2(eps, 0.0), source).x;
float heightY = waves(p + vec2(0.0, eps), source).x;
vec3 normal = normalize(vec3(
emission - heightX,
emission - heightY,
eps * 5.0
));
float key = clamp(dot(normal, normalize(KEY)), 0.0, 1.0);
float amount = clamp(field.y * arrival * uGain, 0.0, 1.0);
float wet = amount * SOAK * (1.0 - 0.62 * key);
vec3 absorbed = hold(uPigment, uStainFloor);
float alpha = clamp(soak(wet, SIGMA) * uAlpha, 0.0, 1.0);
fragColor = vec4(absorbed * alpha, alpha);
}
`;
source · components/ui/vare-uniforms.tsexactly what this command copies
import { MAX_STAIN } from "./atmospheres-color";
import {
setUniform,
type Uniforms,
} from "./atmospheres-gl";
import type { VareColors, VareParams } from "./vare-field";
export interface VareFrame {
time: number;
/** Eased pointer in half-height screen units, y up. */
mouse: [number, number];
/** Fades the phase lens in and out, 0..1. */
pointer: number;
alpha: number;
/** The house law: 1 stains the ground, 0 emits into it. */
absorb: number;
}
export function vareUniforms(colors: VareColors, params: VareParams): Uniforms {
return {
uTime: { value: 0 },
uResolution: { value: [1, 1] },
uMouse: { value: [0, 0] },
uPointer: { value: 0 },
uAngle: { value: params.angle },
uSpread: { value: params.spread },
uWavenumber: { value: params.wavenumber },
uSpeed: { value: params.speed },
uWarp: { value: params.warp },
uWarpScale: { value: params.warpScale },
uJitter: { value: params.jitter },
uSoft: { value: params.soft },
uDetail: { value: params.detail },
uNode: { value: params.node },
uGain: { value: params.gain },
uSource: { value: [...params.source] },
uFalloff: { value: params.falloff },
uSpan: { value: params.span },
uLens: { value: params.lens },
uLensSigma: { value: params.lensSigma },
uAlpha: { value: 1 },
uAbsorb: { value: 0 },
uBody: { value: [...colors.body] },
uDeep: { value: [...colors.deep] },
uEdgeColor: { value: [...colors.edge] },
uPigment: { value: [...colors.pigment] },
uStainFloor: { value: MAX_STAIN },
};
}
export function setVareColors(u: Uniforms, colors: VareColors): void {
setUniform(u, "uBody", [...colors.body]);
setUniform(u, "uDeep", [...colors.deep]);
setUniform(u, "uEdgeColor", [...colors.edge]);
setUniform(u, "uPigment", [...colors.pigment]);
}
export function setVareParams(u: Uniforms, params: VareParams): void {
setUniform(u, "uAngle", params.angle);
setUniform(u, "uSpread", params.spread);
setUniform(u, "uWavenumber", params.wavenumber);
setUniform(u, "uSpeed", params.speed);
setUniform(u, "uWarp", params.warp);
setUniform(u, "uWarpScale", params.warpScale);
setUniform(u, "uJitter", params.jitter);
setUniform(u, "uSoft", params.soft);
setUniform(u, "uDetail", params.detail);
setUniform(u, "uNode", params.node);
setUniform(u, "uGain", params.gain);
setUniform(u, "uSource", [...params.source]);
setUniform(u, "uFalloff", params.falloff);
setUniform(u, "uSpan", params.span);
setUniform(u, "uLens", params.lens);
setUniform(u, "uLensSigma", params.lensSigma);
}
export function setVareFrame(u: Uniforms, frame: VareFrame): 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/vare.tsxexactly what this command copies
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import {
type BlendMode,
blendStyleFor,
blendUniform,
pigmentFor,
type Rgb,
resolveBlendMode,
resolveColor,
} from "./atmospheres-color";
import { useGlCanvas } from "./use-gl-canvas";
import {
useThemeVersion,
useTokenColors,
} from "./use-token-colors";
import {
approach,
POINTER_EASE,
resolveParams,
type VareColors,
type VareParams,
} from "./vare-field";
import { vareFragmentShader } from "./vare-shader";
import {
setVareColors,
setVareFrame,
setVareParams,
vareUniforms,
} from "./vare-uniforms";
const ROLES = ["accent", "accent-2", "accent-alt", "ink"] as const;
/** Far enough in that the fronts have separated in the still frame. */
const STILL_TIME = 9;
export interface VareProps extends React.HTMLAttributes<HTMLDivElement> {
/** Wave rate multiplier. Defaults to 1. */
speed?: number;
/** When on, a phase lens follows the eased cursor and bends the bands.
* Defaults to true. */
interactive?: boolean;
/** Overall opacity of the bands, 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<VareParams>;
children?: React.ReactNode;
}
export const Vare = React.forwardRef<HTMLDivElement, VareProps>(
(
{
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<VareParams>(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<VareColors>(() => {
const stop = (value: string | undefined, fallback: Rgb): Rgb =>
value ? resolveColor(value) : fallback;
const body = stop(cBody, tokens.colors.accent);
return {
body,
deep: stop(cDeep, tokens.colors["accent-2"]),
edge: stop(cEdge, tokens.colors["accent-alt"]),
pigment: pigmentFor(body, tokens.colors.ink),
};
}, [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: vareFragmentShader,
uniforms: () => vareUniforms(rampRef.current, paramsRef.current),
pointer: true,
pointerEase: POINTER_EASE,
stillTime: STILL_TIME,
maxDpr: 1.5,
renderScale: 0.8,
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,
);
}
setVareColors(u, rampRef.current);
setVareParams(u, paramsRef.current);
setVareFrame(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>
);
},
);
Vare.displayName = "Vare";