usva
uihehkusource: ok · hit
Hehku
npx shadcn add "https://usva.build/r/hehku.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · heavywebglanimationmotion
webglogl
Registry dependencies
https://usva.build/r/atmospheres-core.json
Files4 · source cached 10/8/2026, 9:43:49 PM
filament-curve.ts
/**
* 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;
}
filament-shader.ts
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);
}
`;
filament.ts
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);
}
hehku.tsx
"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";