docs / atmospheres / hehku
Hehku
one continuous ribbon of light heated like a filament, coiling slowly through the void with no ends and no seam. thin runs are cold violet; where the coil bunches and looks through itself the light gathers and blooms toward white.
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 itone figure per view. it is the environment, not a decorationnever on a ground it cannot outshine. on savi it renders nothingnot a card or panel background. it needs a void to coil througha11y
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
one long coil
a single closed curve, cold where it runs thin and bright where it bunches
customize
speedwander and spin rate, keep it slow
opacityoverall strength, 0 to 1
modeauto reads the ground, or force emit / stain
coolthe thin cold runs
hotwhere the coil bunches and blooms
thicknessradius of the emitting core
glowfalloff, larger is thinner and colder
bloomhalo level counted as looking through coil
exposurebrightness of the ramp
advanced16
segmentscapsules in the chain, more is smoother
radius · majorsize of the torus the knot winds around
radius · minorthickness of that torus
winding · pturns about the axis, coprime with q
winding · qturns through the hole, coprime with p
scale · xwidth of the finished figure
scale · yheight of the finished figure
scale · zdepth of the finished figure
driftwander of the curve away from the pure knot
driftRatehow fast that wander and roll move
spinrevolutions per second about the vertical
tiltfixed lean of the whole figure, radians
disteye distance from the coil
focallens length, higher is flatter
offset · xlateral eye position, crops the coil
offset · yvertical eye position
usage
import { Hehku } from "usva/atmospheres/hehku";
<Hehku
speed={1}
>
<Hero />
</Hehku>props
| prop | type | default | notes |
|---|---|---|---|
| children | ReactNode | — | rendered above the coil, in normal flow. |
| speed | number | 1 | rate of the coil's wander and spin. it is meant to stay slow. |
| 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 | { cool?; hot? } | accent · accent-alt | the two ends of the heat ramp. cool sinks under accent, hot lifts accent-alt toward white. |
| params | Partial<FilamentParams> | FILAMENT_DEFAULTS | the curve: segments, radius, winding, scale, drift, driftRate, spin, tilt, thickness, glow. the windings must stay coprime integers or the knot degenerates into one circle traced several times. |
| view | Partial<FilamentView> | FILAMENT_VIEW | the camera and the ramp: dist, focal, offset, bloom, exposure. the offset holds the eye off-axis so the coil crops at the frame. |
get it
npx shadcn add https://usva.build/r/hehku.jsonsource · components/ui/filament-curve.tsexactly what this command copies
/**
* One closed torus knot, sampled into a chain of capsules. Every term is
* periodic in an integer multiple of the curve parameter, so the last knot
* lands exactly on the first and the filament has no ends: one continuous
* ribbon that passes behind and in front of itself.
*/
const TAU = Math.PI * 2;
export interface FilamentParams {
/** Capsules in the chain. Knots emitted is segments + 1. */
segments: number;
/** Major and minor radius of the torus the knot winds around. */
radius: [number, number];
/** Windings [about the axis, through the hole]. Keep them coprime, or the
* knot degenerates into one circle traced several times. */
winding: [number, number];
/** Anisotropic scale of the finished figure. */
scale: [number, number, number];
/** Slow wander of the curve away from the pure knot. */
drift: number;
/** Rate of the wander, and of the roll of the strand around its tube. */
driftRate: number;
/** Revolutions per second about the vertical. */
spin: number;
/** Fixed tilt of the whole figure, radians. */
tilt: number;
/** Radius of the emitting core, world units. */
thickness: number;
/** Falloff of exp(-glow * d^2). Larger is thinner and colder. */
glow: number;
}
export const FILAMENT_DEFAULTS: FilamentParams = {
segments: 96,
radius: [2.1, 1.05],
winding: [2, 3],
scale: [1.85, 1.12, 1],
drift: 0.4,
driftRate: 0.06,
spin: 0.01,
tilt: 0.6,
thickness: 0.16,
glow: 26,
};
export function resolveFilamentParams(
overrides: Partial<FilamentParams> = {},
): FilamentParams {
return { ...FILAMENT_DEFAULTS, ...overrides };
}
/** Flat xyz triples, segments + 1 knots, closed. The drift phase rolls the
* strand around its tube, so the coil keeps threading through itself without
* the figure ever changing character. */
export function filamentKnots(time: number, p: FilamentParams): number[] {
const [major, minor] = p.radius;
const [pw, qw] = p.winding;
const [sx, sy, sz] = p.scale;
const spin = time * p.spin * TAU;
const cs = Math.cos(spin);
const ss = Math.sin(spin);
const ct = Math.cos(p.tilt);
const st = Math.sin(p.tilt);
const dt = time * p.driftRate;
const knots: number[] = [];
for (let i = 0; i <= p.segments; i++) {
const u = (i % p.segments) * (TAU / p.segments);
const tube = minor + p.drift * Math.sin(2 * u + dt * 1.3);
const roll = qw * u + dt * 0.7;
const w = major + tube * Math.cos(roll);
const x = w * Math.cos(pw * u) * sx;
const y = (w * Math.sin(pw * u) + p.drift * Math.sin(u + dt)) * sy;
const z = tube * Math.sin(roll) * sz;
const yt = y * ct - z * st;
const zt = y * st + z * ct;
knots.push(x * cs + zt * ss, yt, zt * cs - x * ss);
}
return knots;
}
source · components/ui/filament-shader.tsexactly what this command copies
import { glsl } from "./atmospheres-glsl";
/**
* Hehku is one object, not a field: a single filament coiling through void,
* heated until the places where it bunches against itself glow green-white
* while the long thin runs stay deep violet.
*
* There is no march. Around a thin capsule the squared distance along a ray is
* a parabola, so the ray integral of the gaussian core has a closed form:
* exp(-k * d0^2) / sin(theta), with d0 the closest approach and theta the angle
* between ray and segment. That 1 / sin factor is the physics of the piece: a
* ray running along the filament collects more light, which is why the coil
* blooms exactly where it turns toward the eye or bunches against itself.
*/
const MAX_KNOTS = 97;
export const MAX_FILAMENT_SEGMENTS = MAX_KNOTS - 1;
export const filamentFragmentShader = /* glsl */ `#version 300 es
precision highp float;
uniform vec2 uResolution;
uniform vec3 uKnots[${MAX_KNOTS}];
uniform int uSegments;
uniform float uDist;
uniform float uFocal;
uniform vec2 uOffset;
uniform float uThickness;
uniform float uGlow;
uniform float uBloom;
uniform vec3 uCool;
uniform vec3 uHot;
uniform float uExposure;
uniform float uAlpha;
uniform float uBlend;
out vec4 fragColor;
${glsl("dither", "composite")}
const int MAX_SEGMENTS = ${MAX_KNOTS - 1};
void main() {
vec2 uv = (gl_FragCoord.xy * 2.0 - uResolution) / uResolution.y;
vec3 ro = vec3(uOffset, uDist);
vec3 rd = normalize(vec3(uv, -uFocal));
float spark = 0.0;
float core = 0.0;
float halo = 0.0;
for (int s = 0; s < MAX_SEGMENTS; s++) {
if (s >= uSegments) break;
vec3 a = uKnots[s];
vec3 ba = uKnots[s + 1] - a;
float ba2 = max(dot(ba, ba), 1e-5);
vec3 w = ro - a;
float bd = dot(rd, ba);
// The denominator is ba2 * sin^2(theta); the floor caps the alignment
// bloom so a ray staring straight down a segment stays finite.
float denom = max(ba2 - bd * bd, ba2 * 0.1);
float h = clamp((dot(w, ba) - bd * dot(w, rd)) / denom, 0.0, 1.0);
vec3 q = a + ba * h;
float t = max(dot(q - ro, rd), 0.0);
vec3 pc = ro + rd * t - q;
float dr = length(pc);
float d = max(dr - uThickness, 0.0);
// The coil's slow wander can sweep the strand through the eye point, and
// a pass through the camera would white out the whole frame; the near
// fade lets it slide past the lens instead.
float boost = inversesqrt(max(1.0 - bd * bd / ba2, 0.1))
* smoothstep(0.1, 0.9, t);
spark += exp(-uGlow * 4.0 * dr * dr) * boost;
core += exp(-uGlow * d * d) * boost;
halo += exp(-uGlow * 0.14 * d * d) * boost;
// The chain is closed, so every knot is the clamped end of two segments
// and would be counted twice. One point term per segment start pays the
// whole loop back exactly once.
float ta = max(dot(-w, rd), 0.0);
float dra = length(w + rd * ta);
float da = max(dra - uThickness, 0.0);
float nfa = smoothstep(0.1, 0.9, ta);
spark -= exp(-uGlow * 4.0 * dra * dra) * nfa;
core -= exp(-uGlow * da * da) * nfa;
halo -= exp(-uGlow * 0.14 * da * da) * nfa;
}
spark = max(spark, 0.0);
core = max(core, 0.0);
halo = max(halo, 0.0);
float bunch = max(halo - uBloom, 0.0);
float heat = 1.0 - exp(-bunch * bunch * 0.1);
// The heat ramp runs violet body, green-white strand centre, white where the
// coil bunches: incandescence, not a flat white clip.
vec3 glowRamp = mix(uHot, vec3(1.0), heat * 0.7);
vec3 col = (uCool * (core * 0.5 + halo * 0.06)
+ glowRamp * (spark * (0.45 + 1.1 * heat) + 0.4 * core * heat))
* uExposure;
// Tonemapped on luminance, not per channel, so the bright passes saturate
// toward the heat ramp instead of clipping to flat white.
float lum = max(col.r, max(col.g, col.b));
float peak = 1.0 - exp(-lum);
vec3 display = col / max(lum, 1e-4);
display = mix(display, display * display, uBlend);
display = dither(display, gl_FragCoord.xy, 1.0 / 255.0);
fragColor = composite(display, peak * uAlpha);
}
`;
source · components/ui/filament.tsexactly what this command copies
import type { Rgb } from "./atmospheres-color";
import {
setUniform,
type Uniforms,
} from "./atmospheres-gl";
import type { FilamentParams } from "./filament-curve";
import { MAX_FILAMENT_SEGMENTS } from "./filament-shader";
export interface FilamentColors {
/** The thin cold runs. */
cool: Rgb;
/** Where the coil bunches and light accumulates. */
hot: Rgb;
}
export interface FilamentView {
/** Eye distance along +z. */
dist: number;
focal: number;
/** Lateral eye position. Off-axis, so the coil crops at the frame instead
* of sitting whole in the middle of it. */
offset: [number, number];
/** Halo level above which a ray counts as looking through bunched coil. */
bloom: number;
exposure: number;
}
export const FILAMENT_VIEW: FilamentView = {
dist: 2.9,
focal: 1,
offset: [1.2, 1.9],
bloom: 6,
exposure: 1.9,
};
export function resolveFilamentView(
overrides: Partial<FilamentView> = {},
): FilamentView {
return { ...FILAMENT_VIEW, ...overrides };
}
/** The thin runs sink well under the token: deep cold violet, not lavender. */
export function coolFrom(accent: Rgb): Rgb {
return [accent[0] * 0.5, accent[1] * 0.3, accent[2] * 0.9];
}
/** Green heated toward white: the bunched passes are the only bright thing. */
export function hotFrom(accentAlt: Rgb): Rgb {
const lift = (c: number) => c + (1 - c) * 0.35;
return [lift(accentAlt[0]), lift(accentAlt[1]), lift(accentAlt[2])];
}
/** ogl reads array uniforms only when Array.isArray passes, so the knots reach
* the GPU as a plain flat number[]. A Float32Array uploads nothing at all. */
function padKnots(knots: readonly number[]): number[] {
const size = (MAX_FILAMENT_SEGMENTS + 1) * 3;
const out = knots.slice(0, size);
const tail = out.slice(-3);
while (out.length < size) out.push(...(tail.length === 3 ? tail : [0, 0, 0]));
return out;
}
export interface FilamentFrame {
knots: readonly number[];
segments: number;
alpha: number;
blend: number;
}
export function filamentUniforms(
colors: FilamentColors,
params: FilamentParams,
view: FilamentView,
): Uniforms {
return {
uResolution: { value: [1, 1] },
uKnots: { value: padKnots([]) },
uSegments: { value: Math.min(params.segments, MAX_FILAMENT_SEGMENTS) },
uDist: { value: view.dist },
uFocal: { value: view.focal },
uOffset: { value: [view.offset[0], view.offset[1]] },
uThickness: { value: params.thickness },
uGlow: { value: params.glow },
uBloom: { value: view.bloom },
uCool: { value: colors.cool },
uHot: { value: colors.hot },
uExposure: { value: view.exposure },
uAlpha: { value: 1 },
uBlend: { value: 0 },
};
}
export function setFilamentColors(u: Uniforms, colors: FilamentColors): void {
setUniform(u, "uCool", colors.cool);
setUniform(u, "uHot", colors.hot);
}
export function setFilamentShape(
u: Uniforms,
params: FilamentParams,
view: FilamentView,
): void {
setUniform(u, "uThickness", params.thickness);
setUniform(u, "uGlow", params.glow);
setUniform(u, "uDist", view.dist);
setUniform(u, "uFocal", view.focal);
setUniform(u, "uOffset", [view.offset[0], view.offset[1]]);
setUniform(u, "uBloom", view.bloom);
setUniform(u, "uExposure", view.exposure);
}
export function setFilamentFrame(u: Uniforms, frame: FilamentFrame): void {
setUniform(u, "uKnots", padKnots(frame.knots));
setUniform(u, "uSegments", Math.min(frame.segments, MAX_FILAMENT_SEGMENTS));
setUniform(u, "uAlpha", frame.alpha);
setUniform(u, "uBlend", frame.blend);
}
source · components/ui/hehku.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 {
coolFrom,
type FilamentColors,
type FilamentView,
filamentUniforms,
hotFrom,
resolveFilamentView,
setFilamentColors,
setFilamentFrame,
setFilamentShape,
} from "./filament";
import {
type FilamentParams,
filamentKnots,
resolveFilamentParams,
} from "./filament-curve";
import { filamentFragmentShader } from "./filament-shader";
const ROLES = ["accent", "accent-alt"] as const;
export interface HehkuProps extends React.HTMLAttributes<HTMLDivElement> {
/** Rate of the coil's wander and spin. Defaults to 1. It should stay slow. */
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 two ends of the heat ramp. */
colors?: { cool?: string; hot?: string };
/** Escape hatch for the curve, for tuning demos. */
params?: Partial<FilamentParams>;
/** Escape hatch for the camera and the heat ramp. */
view?: Partial<FilamentView>;
children?: React.ReactNode;
}
/** Seconds into the coil the reduced-motion still frame is taken from. */
const STILL_TIME = 14;
export const Hehku = React.forwardRef<HTMLDivElement, HehkuProps>(
(
{
speed = 1,
opacity = 1,
mode,
colors,
params,
view,
className,
children,
...props
},
forwardedRef,
) => {
const cCool = colors?.cool;
const cHot = colors?.hot;
const scopeRef = React.useRef<HTMLDivElement | null>(null);
const tokens = useTokenColors(ROLES, { scopeRef });
const blend = resolveBlendMode(mode, tokens.bg);
const heat = React.useMemo<FilamentColors>(
() => ({
cool: cCool ? resolveColor(cCool) : coolFrom(tokens.colors.accent),
hot: cHot ? resolveColor(cHot) : hotFrom(tokens.colors["accent-alt"]),
}),
[cCool, cHot, tokens],
);
const heatRef = React.useRef(heat);
heatRef.current = heat;
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 curve = resolveFilamentParams(params);
const curveRef = React.useRef(curve);
curveRef.current = curve;
const camera = resolveFilamentView(view);
const cameraRef = React.useRef(camera);
cameraRef.current = camera;
const canvas = useGlCanvas({
fragment: filamentFragmentShader,
uniforms: () =>
filamentUniforms(heatRef.current, curveRef.current, cameraRef.current),
enabled: !hiddenOnGround("hehku", blend),
maxDpr: 1.5,
stillTime: STILL_TIME,
onFrame: (u, frame) => {
const p = curveRef.current;
const time = frame.time * speedRef.current;
setFilamentColors(u, heatRef.current);
setFilamentShape(u, p, cameraRef.current);
setFilamentFrame(u, {
knots: filamentKnots(time, p),
segments: p.segments,
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 heat ramp.
React.useEffect(() => {
redraw();
}, [redraw, heat, blend]);
const coilOn = 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={coilOn ? "on" : "off"}
className={cn("relative isolate overflow-hidden", className)}
{...props}
>
{coilOn ? (
<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>
);
},
);
Hehku.displayName = "Hehku";