docs / atmospheres / kajastus
Kajastus
the glow spreading along the horizon, arrived overhead: a curved roof of folded light arching over the viewport and running off past both edges, bright where a fold turns and you look through more of it. a corridor of low density runs through the middle, so header type keeps its ground.
intensity · roomrsc · client
provenance
authored in usvalayer
atmospheresintensity
room · the whole environment. nothing competes with itcomposition
behind a hero or a full section, children in normal flow above itthe header type sits in the corridor, not on top of the foldsnever on a ground it cannot outshine. on savi it renders nothingone vault per page. it is the room, nothing else competesthe most expensive of the eight to draw. give it a page worth the cost, and do not pair it with a second moving fielda11y
the canvas isaria-hidden and pointer-transparent · reduced motion paints one still frame · with no WebGL2 the canvas never mountsjest-axedependencies
ogl · atmospheres-core from the same packagelive demo · try it out
under the vault
the field thins into a corridor where the type sits, so the words keep their ground
customize
speedray and fold drift rate
opacityvault strength, 0 to 1
modedark grounds emit, light grounds stain
lowgreen low and distant, reads accent-alt
highviolet climbing the folds, reads accent
starthe cold field behind, reads ink
corridordepth of the low-density cut for the type
starscold field behind, 0 for a clean sky
advanced16
pitchhow far the eye tilts up, radians
curvecurvature of the ceiling, arches the edges down
foldhow far the ribbon meanders across the sky
foldScalewavelength of the meander, smaller is longer
warpone-axis domain warp so folds stay folds
offsetwhere the ribbon sits relative to the eye
widththickness of the sheet
detailacross-ribbon detail, along is 8x this
thresholdcuts the noise floor that keeps the frame empty
driftlateral drift of the fine structure
rayFreqvertical wavelength of the streaming rays
raySpeedhow fast the rays run up the ribbon
fardistance extinction and the horizon AA budget
exposurebrightness of the field
corridorYvertical centre of the corridor, -1 to 1
corridorHcorridor height
usage
import { Kajastus } from "usva/atmospheres/kajastus";
<Kajastus
speed={1}
>
<Hero />
</Kajastus>props
| prop | type | default | notes |
|---|---|---|---|
| children | ReactNode | — | rendered above the vault, in normal flow. |
| speed | number | 1 | rate of the streaming rays and the fold drift. |
| opacity | number | 1 | overall strength, 0 to 1. |
| mode | "emissive" | "absorptive" | resolved bg | forces the blend. by default a dark ground emits and a light ground stains. |
| colors | { low?; high?; star? } | accent-alt · accent · ink | the altitude ramp: green low and distant, violet climbing the folds, cold stars behind. omitted stops read their token. |
| params | Partial<KajastusParams> | KAJASTUS_DEFAULTS | the field itself: pitch, curve, fold, foldScale, warp, offset, width, detail, threshold, drift, rayFreq, raySpeed, far, exposure, stars, and the corridor cut (corridor, corridorY, corridorH) that keeps the type on real negative space. |
get it
npx shadcn add https://usva.build/r/kajastus.jsonsource · components/ui/kajastus-uniforms.tsexactly what this command copies
import type { Rgb } from "./atmospheres-color";
import {
setUniform,
type Uniforms,
} from "./atmospheres-gl";
export interface KajastusColors {
/** Low and distant: oxygen green. */
low: Rgb;
/** Climbing the folds: nitrogen violet. */
high: Rgb;
/** The cold field behind. */
star: Rgb;
}
export interface KajastusParams {
/** How far the eye tilts up, radians. */
pitch: number;
/** Curvature of the ceiling. Larger arches it down harder at the edges. */
curve: number;
/** How far the ribbon meanders across the sky. */
fold: number;
/** Wavelength of the meander; smaller is longer. */
foldScale: number;
/** One-axis domain warp, so folds stay folds instead of turning to soup. */
warp: number;
/** Where the ribbon sits relative to the eye. */
offset: number;
/** Thickness of the sheet. */
width: number;
/** Across-ribbon detail. The along-ribbon domain is 8x this. */
detail: number;
/** Cuts the noise floor away, which is most of what keeps the frame empty. */
threshold: number;
/** Lateral drift of the fine structure. */
drift: number;
/** Vertical wavelength of the streaming rays. */
rayFreq: number;
/** How fast the rays run up the ribbon. */
raySpeed: number;
/** Distance extinction. Also the anti-aliasing budget near the horizon. */
far: number;
exposure: number;
/** Star brightness. Zero for a clean sky. */
stars: number;
/** Depth of the corridor cut for the header type, 0..1. */
corridor: number;
/** Vertical centre of the corridor in NDC, -1 bottom to 1 top. */
corridorY: number;
/** Corridor height. */
corridorH: number;
}
export const KAJASTUS_DEFAULTS: KajastusParams = {
pitch: 0.42,
curve: 0.018,
fold: 7,
foldScale: 0.045,
warp: 9,
offset: -6,
width: 1.8,
detail: 0.5,
threshold: 0.28,
drift: 0.06,
rayFreq: 2.4,
raySpeed: 0.9,
far: 0.025,
exposure: 11,
stars: 0.22,
corridor: 0.34,
corridorY: -0.25,
corridorH: 0.6,
};
export function resolveKajastusParams(
overrides: Partial<KajastusParams> = {},
): KajastusParams {
return { ...KAJASTUS_DEFAULTS, ...overrides };
}
export interface KajastusFrame {
time: number;
alpha: number;
/** 1.0 stains a light ground as pigment, 0.0 emits into a dark one. */
blend: number;
}
export function kajastusUniforms(
colors: KajastusColors,
params: KajastusParams,
): Uniforms {
return {
uTime: { value: 0 },
uResolution: { value: [1, 1] },
uLow: { value: colors.low },
uHigh: { value: colors.high },
uStar: { value: colors.star },
uPitch: { value: params.pitch },
uCurve: { value: params.curve },
uFold: { value: params.fold },
uFoldScale: { value: params.foldScale },
uWarp: { value: params.warp },
uOffset: { value: params.offset },
uWidth: { value: params.width },
uDetail: { value: params.detail },
uThreshold: { value: params.threshold },
uDrift: { value: params.drift },
uRayFreq: { value: params.rayFreq },
uRaySpeed: { value: params.raySpeed },
uFar: { value: params.far },
uExposure: { value: params.exposure },
uStars: { value: params.stars },
uCorridor: { value: params.corridor },
uCorridorY: { value: params.corridorY },
uCorridorH: { value: params.corridorH },
uAlpha: { value: 1 },
uBlend: { value: 0 },
};
}
export function setKajastusColors(u: Uniforms, colors: KajastusColors): void {
setUniform(u, "uLow", colors.low);
setUniform(u, "uHigh", colors.high);
setUniform(u, "uStar", colors.star);
}
export function setKajastusParams(u: Uniforms, params: KajastusParams): void {
setUniform(u, "uPitch", params.pitch);
setUniform(u, "uCurve", params.curve);
setUniform(u, "uFold", params.fold);
setUniform(u, "uFoldScale", params.foldScale);
setUniform(u, "uWarp", params.warp);
setUniform(u, "uOffset", params.offset);
setUniform(u, "uWidth", params.width);
setUniform(u, "uDetail", params.detail);
setUniform(u, "uThreshold", params.threshold);
setUniform(u, "uDrift", params.drift);
setUniform(u, "uRayFreq", params.rayFreq);
setUniform(u, "uRaySpeed", params.raySpeed);
setUniform(u, "uFar", params.far);
setUniform(u, "uExposure", params.exposure);
setUniform(u, "uStars", params.stars);
setUniform(u, "uCorridor", params.corridor);
setUniform(u, "uCorridorY", params.corridorY);
setUniform(u, "uCorridorH", params.corridorH);
}
export function setKajastusFrame(u: Uniforms, frame: KajastusFrame): void {
setUniform(u, "uTime", frame.time);
setUniform(u, "uAlpha", frame.alpha);
setUniform(u, "uBlend", frame.blend);
}
source · components/ui/kajastus-shader.tsexactly what this command copies
import { glsl } from "./atmospheres-glsl";
/**
* Kajastus is an aurora seen from underneath: a vault, not a curtain. Three things
* carry it and none of them are negotiable.
*
* 1. The camera sits at the origin, so a point on a ray is just rd * t and the
* radial distance equals t. Altitude is measured in a paraboloid field,
* hh = y + curve * r^2, which is a large-radius sphere near its apex. Its
* level sets arch overhead and drop away toward the horizon, so the geometry
* converges above you with no fake perspective anywhere. hh is quadratic in
* t, so the entry and exit of the emitting layer are solved analytically and
* every one of the 48 steps lands inside the aurora rather than in vacuum.
*
* 2. The noise domain is stretched 8:1 along the field lines, which for an
* aurora run vertically. Long and thin along, sharply detailed across: that
* anisotropy is the whole difference between an aurora and a cloud, and
* isotropic noise here gives violet clouds on near-black.
*
* 3. Aurora is optically thin. There is no Beer-Lambert term: emission
* accumulates additively with zero absorption, and pow(density, 2.2) keeps
* the ribbon edges crisp instead of milky.
*
* Hue follows altitude because that is the physics: oxygen green low, nitrogen
* violet above it. kajo's accent-alt is green and its accent is violet.
*/
/** The march is jittered per pixel per frame, so the dither buys back the bands
* a coarser step would otherwise show. 48 was paying for detail nobody could see. */
const STEPS = 32;
const H_LOW = 1.2;
const H_HIGH = 6.0;
const T_MAX = 90.0;
export const kajastusFragmentShader = /* glsl */ `#version 300 es
precision highp float;
uniform float uTime;
uniform vec2 uResolution;
uniform vec3 uLow;
uniform vec3 uHigh;
uniform vec3 uStar;
uniform float uPitch;
uniform float uCurve;
uniform float uFold;
uniform float uFoldScale;
uniform float uWarp;
uniform float uOffset;
uniform float uWidth;
uniform float uDetail;
uniform float uThreshold;
uniform float uDrift;
uniform float uRayFreq;
uniform float uRaySpeed;
uniform float uFar;
uniform float uExposure;
uniform float uStars;
uniform float uCorridor;
uniform float uCorridorY;
uniform float uCorridorH;
uniform float uAlpha;
uniform float uBlend;
out vec4 fragColor;
${glsl("fbm", "dither", "tonemap", "composite")}
const int STEPS = ${STEPS};
const float H_LOW = ${H_LOW.toFixed(2)};
const float H_HIGH = ${H_HIGH.toFixed(2)};
const float T_MAX = ${T_MAX.toFixed(1)};
/** Where a ray crosses altitude h, given hh(t) = ry*t + a*t^2. Always one
* positive root for h > 0, so rays that dip below the eye still find the vault
* far out, which is how it reaches past the horizon. */
float tAtHeight(float ry, float a, float h) {
if (a < 1e-5) return ry > 1e-4 ? h / ry : -1.0;
return (-ry + sqrt(ry * ry + 4.0 * a * h)) / (2.0 * a);
}
/** The ribbon centreline, meandering in z as a function of x. The warp is
* applied on one axis only: that gives folds in the fabric, not turbulence. */
float centreline(float x, float t) {
float warp = uWarp * (fbm2(vec2(x * uFoldScale * 0.35, t * 0.008), 2) - 0.5) * 2.0;
float u = x + warp;
return uOffset + uFold * (fbm2(vec2(u * uFoldScale, t * 0.011), 3) - 0.5) * 2.0;
}
float ribbon(float dz, float width) {
return exp(-(dz * dz) / max(width * width, 1e-4));
}
void main() {
vec2 uv = (gl_FragCoord.xy * 2.0 - uResolution) / uResolution.y;
float cp = cos(uPitch);
float sp = sin(uPitch);
vec3 rd = normalize(vec3(uv.x, uv.y * cp + 1.25 * sp, uv.y * sp - 1.25 * cp));
float ry = rd.y;
float aq = uCurve * (1.0 - ry * ry);
float t0 = tAtHeight(ry, aq, H_LOW);
float t1 = tAtHeight(ry, aq, H_HIGH);
vec3 acc = vec3(0.0);
float cover = 0.0;
if (t0 > 0.0 && t1 > t0 && t0 < T_MAX) {
t1 = min(t1, T_MAX);
float dt = (t1 - t0) / float(STEPS);
float jitter = ign(gl_FragCoord.xy + vec2(uTime * 61.7, uTime * 37.3));
float span = H_HIGH - H_LOW;
for (int i = 0; i < STEPS; i++) {
float t = t0 + (float(i) + jitter) * dt;
vec3 p = rd * t;
float hh = p.y + uCurve * dot(p.xz, p.xz);
float alt = (hh - H_LOW) / span;
if (alt < 0.0 || alt > 1.0) continue;
float zc = centreline(p.x, uTime);
float mainSheet = ribbon(p.z - zc, uWidth);
float sideA = ribbon(p.z - zc - 3.8, uWidth * 0.78);
float sideB = ribbon(p.z - zc + 4.6, uWidth * 0.66);
float sheet = mainSheet + 0.64 * sideA + 0.46 * sideB;
if (sheet < 0.004) continue;
float n = fbm2(
vec2(p.x * uDetail + uDrift * uTime, hh * uDetail * 0.125),
3
);
float shaped = clamp(n * 1.7 - uThreshold, 0.0, 1.0);
float density = sheet * pow(shaped, 2.2);
if (density < 1e-4) continue;
float low = smoothstep(0.0, 0.08, alt);
float crown = mix(1.0, 0.24, smoothstep(0.2, 1.0, alt));
float phase = 6.2831853 * fbm2(vec2(p.x * 0.3, 4.7), 2);
float rayWave = 0.5 + 0.5 * sin(uRayFreq * hh - uRaySpeed * uTime + phase);
float rays = 0.58 + 0.42 * rayWave * rayWave * rayWave;
float em = density * low * crown * rays * exp(-t * uFar) * dt;
acc += em * mix(uLow, uHigh, smoothstep(0.02, 0.6, alt));
cover += em;
}
}
// pow() is undefined for a negative base in GLSL ES, and uv.y - uCorridorY is
// negative over most of the lower frame. Squaring by hand, never pow(x, 2.0).
float cd = (uv.y - uCorridorY) / max(uCorridorH, 1e-3);
float corridor = 1.0 - min(uCorridor, 0.38) * exp(-0.55 * cd * cd);
acc *= corridor;
cover *= corridor;
vec2 sky = vec2(atan(rd.z, rd.x), asin(clamp(rd.y, -1.0, 1.0))) * 26.0;
vec2 cell = floor(sky);
vec2 off = hash22(cell) - 0.5;
vec2 local = fract(sky) - 0.5 - off * 0.7;
float twinkle = step(0.962, hash12(cell + 3.1));
float star = twinkle * exp(-dot(local, local) * 90.0);
star *= smoothstep(-0.15, 0.06, rd.y) * exp(-cover * 3.0);
vec3 col = acc * uExposure + star * uStars * uStar * (1.0 - uBlend);
// Tonemapped on luminance, not per channel: the hot lobe of a fold stays
// violet instead of clipping to white, and only the very peak heats up.
//
// The shoulder is Reinhard, not 1 - exp(-lm). The centreline's meander swings
// the whole frame's brightness threefold over a few minutes, and at the top of
// that swing the exponential reached 1.0 across a fifth of the frame: flat,
// fully opaque, folds and corridor gone. Reinhard only approaches 1, so a
// surge stays a surge. uExposure is 11 to put the opening seconds back.
float lm = max(col.r, max(col.g, col.b));
float peak = lm / (1.0 + lm);
// Re-saturating exponent: rays that crossed both the green floor and the
// violet folds average out milky, and the pow pulls the hue back.
vec3 display = pow(col / max(lm, 1e-4), vec3(1.6));
float heat = peak * peak;
display = mix(display, vec3(1.0), heat * heat * 0.22);
display = mix(display, display * display, uBlend);
display = dither(display, gl_FragCoord.xy, 1.0 / 255.0);
fragColor = composite(display, peak * uAlpha);
}
`;
source · components/ui/kajastus.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 { hiddenOnGround } from "./atmospheres-ground";
import { useGlCanvas } from "./use-gl-canvas";
import { useTokenColors } from "./use-token-colors";
import { kajastusFragmentShader } from "./kajastus-shader";
import {
type KajastusColors,
type KajastusParams,
kajastusUniforms,
resolveKajastusParams,
setKajastusColors,
setKajastusFrame,
setKajastusParams,
} from "./kajastus-uniforms";
const ROLES = ["accent", "accent-alt", "ink"] as const;
export interface KajastusProps extends React.HTMLAttributes<HTMLDivElement> {
/** Rate of the streaming rays and the fold drift. Defaults to 1. */
speed?: number;
/** Overall strength, 0..1. Defaults to 1. */
opacity?: number;
/** Dark grounds emit, light grounds stain. Defaults to the resolved bg. */
mode?: BlendMode;
/** Override the altitude ramp. Green low, violet high, cold stars behind. */
colors?: { low?: string; high?: string; star?: string };
/** Escape hatch for the field parameters, for tuning demos. */
params?: Partial<KajastusParams>;
children?: React.ReactNode;
}
/** Seconds into the animation the reduced-motion still frame is taken from. */
const STILL_TIME = 8;
export const Kajastus = React.forwardRef<HTMLDivElement, KajastusProps>(
(
{
speed = 1,
opacity = 1,
mode,
colors,
params,
className,
children,
...props
},
forwardedRef,
) => {
const cLow = colors?.low;
const cHigh = colors?.high;
const cStar = colors?.star;
const scopeRef = React.useRef<HTMLDivElement | null>(null);
const tokens = useTokenColors(ROLES, { scopeRef });
const blend = resolveBlendMode(mode, tokens.bg);
const ramp = React.useMemo<KajastusColors>(
() => ({
low: cLow ? resolveColor(cLow) : tokens.colors["accent-alt"],
high: cHigh ? resolveColor(cHigh) : tokens.colors.accent,
star: cStar ? resolveColor(cStar) : tokens.colors.ink,
}),
[cLow, cHigh, cStar, tokens],
);
const rampRef = React.useRef(ramp);
rampRef.current = ramp;
const speedRef = React.useRef(speed);
speedRef.current = speed;
const opacityRef = React.useRef(opacity);
opacityRef.current = opacity;
const blendRef = React.useRef(blend);
blendRef.current = blend;
const resolved = resolveKajastusParams(params);
const paramsRef = React.useRef(resolved);
paramsRef.current = resolved;
const canvas = useGlCanvas({
fragment: kajastusFragmentShader,
uniforms: () => kajastusUniforms(rampRef.current, paramsRef.current),
enabled: !hiddenOnGround("kajastus", blend),
maxDpr: 1.5,
renderScale: 0.5,
stillTime: STILL_TIME,
onFrame: (u, frame) => {
setKajastusColors(u, rampRef.current);
setKajastusParams(u, paramsRef.current);
setKajastusFrame(u, {
time: frame.time * speedRef.current,
alpha: opacityRef.current,
blend: blendUniform(blendRef.current),
});
},
});
const { redraw } = canvas;
// biome-ignore lint/correctness/useExhaustiveDependencies: the still frame must repaint when the theme repaints the ramp.
React.useEffect(() => {
redraw();
}, [redraw, ramp, blend]);
const vaultOn = 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-fluid={vaultOn ? "on" : "off"}
className={cn("relative isolate overflow-hidden", className)}
{...props}
>
{vaultOn ? (
<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>
);
},
);
Kajastus.displayName = "Kajastus";