docs / atmospheres / routa
Routa
ground frost. water freezes below the surface, lifts the clay into low cells, then breaks it along the walls between them. it is relief first and almost still: fissures propagate across the first half-minute, settle, and never loop or breathe.
intensity · roomrsc · client
provenance
authored in usvalayer
atmospheresintensity
room · the whole environment. nothing competes with itcomposition
wraps a whole region; content sits above the ground in normal flowsavi is its home: the fissures hold pigment there. dark grounds get the raking key insteadone atmosphere per page. it is the room, nothing layers over itnot an ambient pulse. it does not float above the page or breathe like foga11y
the canvas layer isaria-hidden and pointer-transparent · reduced motion paints the mature frame at twenty-seven seconds and stopsjest-axedependencies
ogl · atmospheres-core from the same packagelive demo · try it out
cold from underneath
the fissures hold the dark at every frozen seam
customize
modeauto reads the ground, or force stain / key
speeddomain drift, barely perceptible
opacityoverall strength, 0 to 1
advanced18
cellScalefrost cells across the short side
heavehow high the cell interiors lift
crackWidthwidth of the fissure at each wall
crackDepthhow far the walls fall below the surface
unevenScalefrequency of the broad ground swell
unevenhow differently neighbours lift
driftdomain travel per second, near still
growthRateone-shot fissure propagation per second
slopeheight-gradient gain before the normal
roughOren-Nayar roughness, 0 to 1
ambientfill under the raking key
keystrength of the raking key
reliefdamp held in the lee of a heave, light grounds only
ditherdither amplitude for dark-ground emission
light · xlateral rake of the key across the ground
light · yvertical rake of the key
light · zkeep low so the key stays grazing
light · coloroptional cold key for the dark ground
usage
import { Routa } from "usva/atmospheres/routa";
<Routa>
<Article />
</Routa>props
| prop | type | default | notes |
|---|---|---|---|
| children | ReactNode | — | rendered above the frozen ground, in normal flow. |
| speed | number | 1 | domain drift multiplier. barely perceptible either way. |
| mode | "emissive" | "absorptive" | — | forces the material. by default a light ground stains its fissures and a dark ground catches the raking key. |
| light | { direction?; color? } | [-0.68, 0.46, 0.38] | the low raking key. keep z low, the heave only reads when the key rakes across it. color only affects the dark-ground reading. |
| opacity | number | 1 | overall opacity, 0 to 1. |
| params | Partial<RoutaParams> | — | the frost field: cellScale, heave, crackWidth, crackDepth, unevenScale, uneven, drift, growthRate, slope, rough, ambient, key, relief, and dither. relief is the damp a light ground holds in the lee of a heave, and does nothing on a dark one. |
get it
npx shadcn add https://usva.build/r/routa.jsonsource · components/ui/routa-field.tsexactly what this command copies
import {
type BlendMode,
pigmentFor,
type Rgb,
} from "./atmospheres-color";
export interface RoutaParams {
/** Frost cells across one short-side unit. */
cellScale: number;
/** Height of the heaved cell interiors. */
heave: number;
/** Width of the fissure at each cell wall. */
crackWidth: number;
/** How far the cell walls fall below the heaved surface. */
crackDepth: number;
/** Spatial frequency of the broad unevenness under the frost. */
unevenScale: number;
/** How differently neighbouring cells lift. */
uneven: number;
/** Domain travel per second. Deliberately close to still. */
drift: number;
/** One-shot fissure propagation per second. */
growthRate: number;
/** Height-gradient gain before the normal is built. */
slope: number;
/** Oren-Nayar roughness. */
rough: number;
/** Ambient fill under the raking key. */
ambient: number;
/** Strength of the raking key. */
key: number;
/** How much damp the clay holds in the lee of a heave. Light grounds only. */
relief: number;
/** Dither amplitude for dark-ground emission. */
dither: number;
}
export const ROUTA_DEFAULTS: RoutaParams = {
cellScale: 3.8,
heave: 0.075,
crackWidth: 0.055,
crackDepth: 0.045,
unevenScale: 0.72,
uneven: 0.34,
drift: 0.0025,
growthRate: 0.028,
slope: 1.35,
rough: 0.92,
ambient: 0.32,
key: 0.88,
relief: 0.18,
dither: 0.005,
};
export function resolveParams(
overrides: Partial<RoutaParams> = {},
): RoutaParams {
const merged = { ...ROUTA_DEFAULTS, ...overrides };
return {
...merged,
cellScale: Math.max(merged.cellScale, 0.5),
heave: Math.max(merged.heave, 0),
crackWidth: Math.min(Math.max(merged.crackWidth, 0.005), 0.2),
crackDepth: Math.max(merged.crackDepth, 0),
uneven: Math.min(Math.max(merged.uneven, 0), 0.8),
// Past about a third the damp stops reading as a lit surface and starts
// competing with the fissures, which are the subject on clay.
relief: Math.min(Math.max(merged.relief, 0), 0.35),
drift: Math.max(merged.drift, 0),
growthRate: Math.max(merged.growthRate, 0),
rough: Math.min(Math.max(merged.rough, 0), 1),
};
}
export const ROUTA_ROLES = ["bg", "ink", "accent", "accent-alt"] as const;
export type RoutaRole = (typeof ROUTA_ROLES)[number];
export interface RoutaColors {
/** The dark pigment held in the fissures on clay. */
pigment: Rgb;
/** The cold raking key caught by heaved crests on a dark ground. */
emission: Rgb;
/** The frozen surface itself on a dark ground. */
body: Rgb;
/** Under the surface: the fissures, and ground the key never reaches. */
fissure: Rgb;
}
function mix(a: Rgb, b: Rgb, t: number): Rgb {
return [
a[0] + (b[0] - a[0]) * t,
a[1] + (b[1] - a[1]) * t,
a[2] + (b[2] - a[2]) * t,
];
}
/**
* The theme swap changes the material, not the shader, the way kynnös already
* does it. On clay the fissures hold pigment and the frost is what is left of
* the ground. On a dark ground it is black ice: a surface that is always there,
* a grazing key that only the heaved crests catch, and fissures cut below it.
*
* The body is the whole point. Without one the shader painted crests and left
* everything between them transparent, so the relief had nothing to sit on and
* read as lumps of glow rather than frozen earth.
*/
export function buildColors(
roles: Record<RoutaRole, Rgb>,
mode: BlendMode,
keyColor?: Rgb,
): RoutaColors {
const hue = mode === "absorptive" ? roles.accent : roles["accent-alt"];
return {
pigment: pigmentFor(hue, roles.ink),
emission: keyColor ?? mix(roles.accent, roles.ink, 0.55),
body: mix(roles.bg, roles.ink, 0.09),
fissure: mix(roles.bg, [0, 0, 0], 0.55),
};
}
/** A low raking key, cold and long across the broken ground. */
export const DEFAULT_LIGHT: [number, number, number] = [-0.68, 0.46, 0.38];
source · components/ui/routa-shader.tsexactly what this command copies
import { glsl } from "./atmospheres-glsl";
export const routaVertexShader = /* glsl */ `#version 300 es
in vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`;
export const routaFragmentShader = /* glsl */ `#version 300 es
precision highp float;
uniform float uTime;
uniform vec2 uResolution;
uniform float uCellScale;
uniform float uHeave;
uniform float uCrackWidth;
uniform float uCrackDepth;
uniform float uUnevenScale;
uniform float uUneven;
uniform float uDrift;
uniform float uGrowthRate;
uniform float uSlope;
uniform float uRough;
uniform float uAmbient;
uniform float uKey;
uniform float uRelief;
uniform float uDither;
uniform float uAlpha;
uniform float uAbsorb;
uniform vec3 uLightDir;
uniform vec3 uPigment;
uniform vec3 uEmission;
uniform vec3 uBody;
uniform vec3 uFissure;
uniform float uStainFloor;
out vec4 fragColor;
${glsl("fbm", "worley", "dither", "stain", "composite")}
const vec3 VIEW = vec3(0.0, 0.0, 1.0);
const float SIGMA = 1.12;
const float SOAK = 0.52;
/** How far in from a fissure the plate has finished rising, in wall units. */
const float PLATE_EDGE = 0.2;
vec2 frostAt(vec2 p) {
vec2 travel = vec2(uTime * uDrift, -uTime * uDrift * 0.37);
vec2 q = p + travel;
float wall = craquelure(q * uCellScale);
float fissure = 1.0 - smoothstep(0.0, uCrackWidth, wall);
float arrival = fbm2(
q * uCellScale * 0.55 + vec2(7.3, 1.9),
2
) / 0.75;
float progress = clamp(0.1 + uTime * uGrowthRate, 0.0, 1.0);
fissure *= smoothstep(arrival - 0.08, arrival + 0.08, progress);
/* A heaved plate is flat, and flatness is also what makes it drawable.
craquelure is F2 - F1, whose slope kinks along the medial axis of every
cell, where the second-nearest cell changes. The normal is built by
differencing the height, so a slope kink becomes a hard seam straight
across each plate. Doming the interior put a gradient there for the kink
to bend; a flat interior gives it nothing, and the shape lands where it
belongs, on the shoulder falling into the seam. */
float dome = smoothstep(uCrackWidth, uCrackWidth + PLATE_EDGE, wall);
dome = dome * dome * (3.0 - 2.0 * dome);
float broad = fbm2(q * uUnevenScale + vec2(13.1, -7.4), 3) / 0.875;
float lift = dome * mix(1.0 - uUneven, 1.0 + uUneven, broad);
return vec2(uHeave * lift - uCrackDepth * fissure, fissure);
}
float heightAt(vec2 p) {
return frostAt(p).x;
}
float orenNayar(vec3 n, vec3 l, float rough) {
float s2 = rough * rough;
float a = 1.0 - 0.5 * s2 / (s2 + 0.33);
float b = 0.45 * s2 / (s2 + 0.09);
float ndl = dot(n, l);
float ndv = dot(n, VIEW);
float thetaL = acos(clamp(ndl, -1.0, 1.0));
float thetaV = acos(clamp(ndv, -1.0, 1.0));
float alpha = max(thetaL, thetaV);
float beta = min(thetaL, thetaV);
vec3 lp = l - n * ndl;
vec3 vp = VIEW - n * ndv;
float cosPhi = 0.0;
if (length(lp) > 1e-4 && length(vp) > 1e-4) {
cosPhi = max(dot(normalize(lp), normalize(vp)), 0.0);
}
return max(ndl, 0.0) * (a + b * cosPhi * sin(alpha) * tan(min(beta, 1.5)));
}
void main() {
float unit = max(min(uResolution.x, uResolution.y), 1.0);
vec2 p = (gl_FragCoord.xy - 0.5 * uResolution) / unit;
vec2 field = frostAt(p);
float height = field.x;
float eps = 1.5 / unit;
float heightX = heightAt(p + vec2(eps, 0.0));
float heightY = heightAt(p + vec2(0.0, eps));
vec3 normal = normalize(vec3(
(height - heightX) * uSlope,
(height - heightY) * uSlope,
eps * 3.0
));
vec3 light = normalize(uLightDir);
float diffuse = orenNayar(normal, light, uRough);
float lit = clamp(uAmbient + uKey * diffuse, 0.0, 1.2);
// Black ice. The ground is opaque and always painted, and the key only picks
// out the crests: that surface is what separates frozen earth from a handful
// of lit lumps floating on whatever happens to be behind the canvas.
if (uAbsorb < 0.5) {
float crest = smoothstep(0.0, uHeave, max(height, 0.0));
float rake = smoothstep(0.36, 1.0, lit);
vec3 ground = mix(uFissure, uBody, smoothstep(0.18, 0.72, lit));
vec3 col = mix(ground, uEmission, crest * rake * 0.85);
col = mix(col, uFissure, field.y * 0.9);
col = dither(col, gl_FragCoord.xy, uDither);
fragColor = composite(col, uAlpha);
return;
}
/* Clay carries the heave as damp, never as a darker multiply: the multiply
composite is inert under isolate, so a light ground can only show relief by
holding more pigment. Wet in the lee of a plate, dry where the key lands.
Kept out of the fissures so it never doubles with the pigment already
there, and an order under them so the seams stay the subject. */
float relief = smoothstep(0.62, 0.18, lit) * (1.0 - field.y) * uRelief;
float fissure = field.y * mix(1.0, 0.68, diffuse);
vec3 absorbed = hold(uPigment, uStainFloor);
float alpha = clamp(soak(fissure * SOAK + relief, SIGMA) * uAlpha, 0.0, 1.0);
fragColor = vec4(absorbed * alpha, alpha);
}
`;
source · components/ui/routa-uniforms.tsexactly what this command copies
import { MAX_STAIN } from "./atmospheres-color";
import {
setUniform,
type Uniforms,
} from "./atmospheres-gl";
import type { RoutaColors, RoutaParams } from "./routa-field";
export interface RoutaFrame {
time: number;
alpha: number;
absorb: number;
light: [number, number, number];
}
export function routaUniforms(
colors: RoutaColors,
params: RoutaParams,
light: [number, number, number],
): Uniforms {
return {
uTime: { value: 0 },
uResolution: { value: [1, 1] },
uCellScale: { value: params.cellScale },
uHeave: { value: params.heave },
uCrackWidth: { value: params.crackWidth },
uCrackDepth: { value: params.crackDepth },
uUnevenScale: { value: params.unevenScale },
uUneven: { value: params.uneven },
uDrift: { value: params.drift },
uGrowthRate: { value: params.growthRate },
uSlope: { value: params.slope },
uRough: { value: params.rough },
uAmbient: { value: params.ambient },
uKey: { value: params.key },
uRelief: { value: params.relief },
uDither: { value: params.dither },
uAlpha: { value: 1 },
uAbsorb: { value: 1 },
uLightDir: { value: [...light] },
uPigment: { value: [...colors.pigment] },
uEmission: { value: [...colors.emission] },
uBody: { value: [...colors.body] },
uFissure: { value: [...colors.fissure] },
uStainFloor: { value: MAX_STAIN },
};
}
export function setRoutaColors(u: Uniforms, colors: RoutaColors): void {
setUniform(u, "uPigment", [...colors.pigment]);
setUniform(u, "uEmission", [...colors.emission]);
setUniform(u, "uBody", [...colors.body]);
setUniform(u, "uFissure", [...colors.fissure]);
}
export function setRoutaParams(u: Uniforms, params: RoutaParams): void {
setUniform(u, "uCellScale", params.cellScale);
setUniform(u, "uHeave", params.heave);
setUniform(u, "uCrackWidth", params.crackWidth);
setUniform(u, "uCrackDepth", params.crackDepth);
setUniform(u, "uUnevenScale", params.unevenScale);
setUniform(u, "uUneven", params.uneven);
setUniform(u, "uDrift", params.drift);
setUniform(u, "uGrowthRate", params.growthRate);
setUniform(u, "uSlope", params.slope);
setUniform(u, "uRough", params.rough);
setUniform(u, "uAmbient", params.ambient);
setUniform(u, "uKey", params.key);
setUniform(u, "uRelief", params.relief);
setUniform(u, "uDither", params.dither);
}
export function setRoutaFrame(u: Uniforms, frame: RoutaFrame): void {
setUniform(u, "uTime", frame.time);
setUniform(u, "uAlpha", frame.alpha);
setUniform(u, "uAbsorb", frame.absorb);
setUniform(u, "uLightDir", [...frame.light]);
}
source · components/ui/routa.tsxexactly what this command copies
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import {
type BlendMode,
blendStyleFor,
blendUniform,
resolveBlendMode,
resolveColor,
} from "./atmospheres-color";
import { useGlCanvas } from "./use-gl-canvas";
import { useTokenColors } from "./use-token-colors";
import {
buildColors,
DEFAULT_LIGHT,
ROUTA_ROLES,
type RoutaParams,
resolveParams,
} from "./routa-field";
import { routaFragmentShader } from "./routa-shader";
import {
routaUniforms,
setRoutaColors,
setRoutaFrame,
setRoutaParams,
} from "./routa-uniforms";
export interface RoutaLight {
/** Keep z low: the heave only reads when the key rakes across it. */
direction?: [number, number, number];
/** Optional CSS colour for the dark-ground key. */
color?: string;
}
export interface RoutaProps extends React.HTMLAttributes<HTMLDivElement> {
/** Domain drift multiplier. Defaults to 1 and remains barely perceptible. */
speed?: number;
/** Force the material. By default the ground chooses frost-light or stain. */
mode?: BlendMode;
/** Raking light direction and optional dark-ground colour. */
light?: RoutaLight;
/** Overall opacity, 0..1. Defaults to 1. */
opacity?: number;
/** Escape hatch for frost cell, fissure, and relief parameters. */
params?: Partial<RoutaParams>;
children?: React.ReactNode;
}
const STILL_TIME = 27;
export const Routa = React.forwardRef<HTMLDivElement, RoutaProps>(
(
{
speed = 1,
mode,
light,
opacity = 1,
params,
className,
children,
...props
},
forwardedRef,
) => {
const lightColor = light?.color;
const direction = light?.direction ?? DEFAULT_LIGHT;
const speedRef = React.useRef(speed);
speedRef.current = speed;
const opacityRef = React.useRef(opacity);
opacityRef.current = opacity;
const lightRef = React.useRef(direction);
lightRef.current = direction;
const scopeRef = React.useRef<HTMLDivElement | null>(null);
const tokens = useTokenColors(ROUTA_ROLES, { scopeRef });
const blend = resolveBlendMode(mode, tokens.bg);
const colors = React.useMemo(
() =>
buildColors(
tokens.colors,
blend,
lightColor ? resolveColor(lightColor) : undefined,
),
[tokens.colors, blend, lightColor],
);
const colorsRef = React.useRef(colors);
colorsRef.current = colors;
const resolved = resolveParams(params);
const paramsRef = React.useRef(resolved);
paramsRef.current = resolved;
const blendRef = React.useRef(blend);
blendRef.current = blend;
const canvas = useGlCanvas({
fragment: routaFragmentShader,
stillTime: STILL_TIME,
maxDpr: 1.5,
uniforms: () =>
routaUniforms(colorsRef.current, paramsRef.current, lightRef.current),
onFrame: (u, frame) => {
setRoutaColors(u, colorsRef.current);
setRoutaParams(u, paramsRef.current);
setRoutaFrame(u, {
time: frame.time * speedRef.current,
alpha: opacityRef.current,
absorb: blendUniform(blendRef.current),
light: lightRef.current,
});
},
});
const { redraw } = canvas;
// biome-ignore lint/correctness/useExhaustiveDependencies: the still frame must repaint when the material, key, or ground changes.
React.useEffect(() => {
redraw();
}, [redraw, colors, direction, 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>
);
},
);
Routa.displayName = "Routa";