shadimage
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 · heavywebgl
animationmotion
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";

More from usva

Browse all components in this library →