Manual setup
Not using the shadcn CLI? Copy the runtime plus the orbs you want into your project. They import nothing but React.
1. Check your alias
The orb imports the runtime from @/components/ui/orbkit-core. If your project uses a different alias, adjust the import at the top of the orb file — that is the only path either file references.
// tsconfig.json
{
"compilerOptions": {
"paths": { "@/*": ["./*"] }
}
}2. components/ui/orbkit-core.tsx
"use client";
import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
/* ----------------------------------------------------------------------------
Orbkit core — raw WebGL shader orb runtime. No dependencies.
An orb is a full-screen triangle rendered into a transparent canvas by a
fragment shader. Every orb declares a parameter schema (sliders + colors);
the values are uploaded as uniforms each frame from a ref, so a controls
panel can tune them live without ever remounting the canvas (a remount would
drop the WebGL context).
Animation model: every state synthesizes two volume signals — input (user
speech energy) and output (agent speech energy) — smooths them, and the
shaders react to those. The flow clock's speed itself follows the output
volume, so orbs visibly quicken when the agent is talking.
---------------------------------------------------------------------------- */
export type OrbState = "idle" | "thinking" | "speaking";
export const ORB_STATES = ["idle", "thinking", "speaking"] as const;
function clamp01(n: number) {
return Math.min(1, Math.max(0, n));
}
/** Per-state [input, output] volume synthesis. */
/**
* Transition rate shared by params, colours and the flow-speed multiplier.
* They must move together: if the rate multiplier eases faster than the look,
* a state change spins the orb up before it has finished cross-fading, which
* reads as a lurch.
*
* This drives a critically damped spring rather than the exponential ease it
* used to. An exponential's velocity is highest at the instant the target
* changes, so every state change began with a jolt — and for params that are
* spatial frequencies (Corona's warpFreq travels 5.25 -> 19.5 between states)
* that jolt sweeps the field through its intermediate frequencies at maximum
* rate, which is what read as the transition "scrambling".
*
* A spring starts at rest and accelerates, so the sweep is spread across the
* transition instead of front-loaded. Measured on that warpFreq move it is a
* 20% lower peak rate of change (20.3/s vs 25.3/s) AND it arrives sooner —
* 1.50s to within 2% against the exponential's 2.18s, since an exponential
* only ever asymptotes toward its target.
*/
const PARAM_EASE = 4;
/*
One step of a critically damped spring, implicit (semi-implicit Euler would
blow up at the frame times a backgrounded tab produces). Returns nothing and
writes through the scratch pair so the hot loop allocates nothing.
*/
const springOut = { x: 0, v: 0 };
function springStep(x: number, v: number, target: number, dt: number, omega: number) {
const f = 1 + 2 * dt * omega;
const oo = omega * omega;
const hoo = dt * oo;
const hhoo = dt * hoo;
const detInv = 1 / (f + hhoo);
springOut.x = (f * x + dt * v + hhoo * target) * detInv;
springOut.v = (v + hoo * (target - x)) * detInv;
}
function targetVolumes(state: OrbState, t: number): [number, number] {
switch (state) {
case "idle":
return [0, 0.3];
case "speaking":
return [
clamp01(0.65 + Math.sin(t * 4.8) * 0.22),
clamp01(0.75 + Math.sin(t * 3.6) * 0.22)
];
case "thinking": {
const base = 0.38 + 0.07 * Math.sin(t * 0.7);
const wander = 0.05 * Math.sin(t * 2.1) * Math.sin(t * 0.37 + 1.2);
return [clamp01(base + wander), clamp01(0.48 + 0.12 * Math.sin(t * 1.05 + 0.6))];
}
}
}
/* ------------------------------ param schema ------------------------------- */
export interface OrbParamDef {
key: string;
label: string;
min: number;
max: number;
step: number;
default: number;
/**
* Rate params. The engine integrates them into a clock
* (`clock += dt * value * volumeSpeed`) and uploads the clock instead of the
* raw value, so changing the rate never jumps the phase — the motion speeds
* up or slows down rather than snapping to a new position.
*/
integrate?: boolean;
}
export interface OrbColorDef {
key: string;
label: string;
/** hex, e.g. `#ff8b73` */
default: string;
}
export interface OrbVariant {
key: string;
label: string;
note: string;
/** GLSL fragment shader body. Uniform declarations are generated for you. */
frag: string;
params: OrbParamDef[];
colors: OrbColorDef[];
/**
* Per-state parameter targets. The engine glides each param toward the
* active state's preset. Params passed explicitly via the `params` prop
* always win over the preset.
*/
statePresets?: Partial<Record<OrbState, Record<string, number>>>;
/**
* Per-state colour targets, the colour counterpart of `statePresets`.
* Kept a separate map because presets are numeric and colours are hex
* strings — a union would lose type safety on both. Colours glide in RGB
* on the same easing as params, so a state change cross-fades rather
* than cutting. Colours passed explicitly via the `colors` prop always
* win, exactly as with params.
*/
stateColors?: Partial<Record<OrbState, Record<string, string>>>;
}
export type OrbParamValues = Partial<Record<string, number>>;
export type OrbColorValues = Partial<Record<string, string>>;
/** Every param and color at its schema default. */
export function defaultValuesFor(variant: OrbVariant): {
params: Record<string, number>;
colors: Record<string, string>;
} {
return {
params: Object.fromEntries(variant.params.map((p) => [p.key, p.default])),
colors: Object.fromEntries(variant.colors.map((c) => [c.key, c.default]))
};
}
export function hexToRgb(hex: string): [number, number, number] {
let h = hex.replace("#", "").trim();
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
const n = parseInt(h, 16);
if (h.length !== 6 || Number.isNaN(n)) return [1, 1, 1];
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
}
/* ------------------------------- GLSL shared ------------------------------- */
const VERT = `
attribute vec2 aPos;
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
`;
/**
* Prelude prepended to every orb fragment shader: uniforms, value noise, fbm,
* and the centered aspect-corrected UV helper.
*/
export const ORB_GLSL_HELPERS = `
precision highp float;
uniform vec2 uRes;
uniform float uTime; // slow ambient clock (half real-time)
uniform float uAnim; // flow clock — its speed follows the output volume
uniform float uInput; // input volume 0..1: user speech energy
uniform float uOutput; // output volume 0..1: agent speech energy
float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
return mix(
mix(hash(i), hash(i + vec2(1.0, 0.0)), f.x),
mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), f.x),
f.y
);
}
float fbm(vec2 p) {
float v = 0.0;
float a = 0.5;
for (int i = 0; i < 5; i++) {
v += a * noise(p);
p = p * 2.03 + vec2(11.7, 7.3);
a *= 0.5;
}
return v;
}
vec2 orbUV() { return (2.0 * gl_FragCoord.xy - uRes) / min(uRes.x, uRes.y); }
// GLSL ES 1.0 has no tanh() — it arrived in ES 3.0. Shader-golf listings lean
// on it as a tone-mapper, so it ships here. Clamped against exp() overflow;
// accurate for the non-negative accumulators those shaders produce.
vec3 tanh3(vec3 x) {
x = clamp(x, -10.0, 10.0);
vec3 e = exp(2.0 * x);
return (e - 1.0) / (e + 1.0);
}
`;
function paramUniformDecls(variant: OrbVariant): string {
return [
...variant.params.map((p) => `uniform float uP_${p.key};`),
...variant.colors.map((c) => `uniform vec3 uC_${c.key};`)
].join("\n");
}
/* ------------------------------- engine ------------------------------------ */
/**
* Per-canvas context-lifecycle controller. Created on first mount of a canvas
* and kept for the element's whole life — the router can hide a page and show
* the same DOM again, and React re-runs effects on the same canvas, so the
* lost/restored listeners must outlive any single effect run: an uncanceled
* webglcontextlost event marks the context permanently unrestorable.
*/
interface CanvasContextController {
/** Whether a mounted orb currently wants this context alive. */
desired: boolean;
/** Builds a render generation; returns its teardown. Rebound per effect run. */
start: (() => () => void) | null;
/** Teardown of the live generation, if one is running. */
stopGen: (() => void) | null;
}
const canvasControllers = new WeakMap<HTMLCanvasElement, CanvasContextController>();
function compile(gl: WebGLRenderingContext, type: number, src: string): WebGLShader | null {
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, src);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error("[orbkit] shader compile error:", gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
return null;
}
return shader;
}
/* ----------------------------------------------------------------------------
Wrappers — optional decoration drawn around, under and over the orb.
A wrapper is pure CSS/SVG, never a shader: the orb keeps its own canvas and
the decoration is composited on top of it by the browser. That keeps the
whole set free for any orb (no per-variant shader work), costs nothing on
the GPU budget the shaders are already spending, and means swapping one
wrapper for another at runtime never touches the WebGL context. Turning a
wrapper on or off does, since that is what moves the canvas into or out of
the wrapper element — see the effect's `wrapped` dependency.
Layout: a wrapped orb becomes a square box, and the canvas is absolutely
positioned inside it with the spec's `inset`. The FOOTPRINT is unchanged —
`size` is still the outer diameter — so dropping a wrapper onto an existing
orb reflows nothing; the orb itself just shrinks to leave the ring room.
Colour: every layer that isn't physically light or shadow paints in
`currentColor`, so a wrapper picks up the surrounding text colour and reads
correctly on light and dark pages with no configuration. `wrapperColor`
sets that colour when you want something other than the inherited one.
---------------------------------------------------------------------------- */
export const ORB_WRAPPERS = [
"none",
"glass",
"ring",
"dotted",
"ticks",
"reticle",
"grid",
"halftone",
"scanlines"
] as const;
export type OrbWrapper = (typeof ORB_WRAPPERS)[number];
/*
Keyframes for the animated wrappers, shipped inside the component so an orb
stays a single self-contained file with nothing to add to a global
stylesheet. React 19 hoists a <style href precedence> into <head> and
de-duplicates it, so a page full of wrapped orbs emits this exactly once;
older React renders it inline, which is redundant but harmless.
Reduced motion parks all of it. The shader runtime already honours the same
preference for the orb itself (see the reduce-motion branch in the render
loop), and a ring that keeps spinning around a frozen orb would be the worse
half of the two still moving.
*/
const WRAPPER_STYLE_HREF = "orbkit-wrapper";
const WRAPPER_CSS = `
@keyframes orbkit-w-spin { to { transform: rotate(360deg); } }
@keyframes orbkit-w-roll { from { transform: translateY(-110%); } to { transform: translateY(360%); } }
@media (prefers-reduced-motion: reduce) {
.orbkit-w-anim { animation: none !important; }
}
`;
interface WrapperSpec {
/**
* How far the canvas sits inside the box, in percent, leaving the
* decoration room. Applied as explicit width/height rather than as `inset`:
* a canvas is a REPLACED element, so an absolutely positioned one with
* `left` and `right` both set does not stretch between them — it keeps its
* intrinsic size and the over-constrained edge is dropped. The orb would
* then be drawn into a canvas the size of the page.
*/
inset: number;
/**
* Soft circular mask on the canvas. Only the wrappers that read as a
* CONTAINER set one — a bubble has to hold the orb, whereas a bezel sits
* beside it and clipping the halo there would just amputate the glow.
*/
mask?: string;
/** Cast by the assembly as a whole, on the outer box. */
shadow?: string;
/** True when the spec uses one of the keyframes above. */
animated?: boolean;
/** Painted beneath the canvas. */
under?: ReactNode;
/** Painted over it. */
over?: ReactNode;
}
const DISC: CSSProperties = { borderRadius: "50%" };
/** One absolutely-positioned decoration layer, filling the wrapper box. */
function Layer({
inset = 0,
style,
className
}: {
inset?: number | string;
style: CSSProperties;
className?: string;
}) {
return (
<span
aria-hidden="true"
className={className}
style={{ position: "absolute", inset, pointerEvents: "none", ...style }}
/>
);
}
/**
* A free-floating highlight — glass's specular and its bounce. These do not go
* through `Layer` because they are placed with `left`/`top`/`width`, and a
* style object that sets those on top of `Layer`'s `inset` shorthand is mixing
* shorthand and longhand for the same property: React warns about it, and the
* result depends on key order rather than on anything you would want to rely
* on.
*/
function Highlight({ style }: { style: CSSProperties }) {
return (
<span
aria-hidden="true"
style={{ position: "absolute", borderRadius: "50%", pointerEvents: "none", ...style }}
/>
);
}
/**
* The mask that makes a wrapper HOLD the orb: cuts the canvas back to the
* bubble and stops a hairline short of the rim, so the orb never quite touches
* the glass.
*
* The percentage has to be derived rather than written down. `closest-side`
* measures the CANVAS, and a wrapper with a negative inset draws its canvas
* LARGER than the bubble — at -7 the canvas is 114% of the box, so the rim
* sits at 100/1.14 = 87.7% of the canvas's own radius, not at 100%.
*
* The gap is real pixels rather than a share of the size: it reads as the same
* band at 120px and at 900px, which a percentage would not.
*/
const RIM_GAP_PX = 4;
function rimMask(inset: number): string {
const rim = (100 / (1 - (2 * inset) / 100)).toFixed(2);
// Feathered over the final pixel, so the cut is not a razor edge.
const solid = RIM_GAP_PX + 0.5;
const clear = RIM_GAP_PX - 0.5;
return `radial-gradient(circle closest-side, #000 calc(${rim}% - ${solid}px), rgba(0,0,0,0) calc(${rim}% - ${clear}px))`;
}
/** Both spellings, since Safari still wants the prefix for mask-image. */
function masked(image: string): CSSProperties {
return { WebkitMaskImage: image, maskImage: image };
}
const svgLayer: CSSProperties = {
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
pointerEvents: "none",
overflow: "visible"
};
/** Glass's overfill, shared by its inset and the mask derived from it. */
const GLASS_INSET = -4;
const WRAPPER_SPECS: Record<Exclude<OrbWrapper, "none">, WrapperSpec> = {
/*
glass — a blown bubble with the orb suspended inside it.
Five layers in the order light actually arrives: the body brightening
toward the key light, the Fresnel ring where a sphere's edge turns almost
edge-on and reflects nearly everything, the rim itself, the window
reflection, and the bounce coming back up off whatever the bubble is
sitting on. All of it is white and black rather than `currentColor` —
glass has no colour of its own, only the light it moves around.
*/
glass: {
/*
Negative on purpose. An orb's shader does not necessarily paint to the
edge of its canvas — most draw a sphere with transparent margin around
it — so a canvas sized to the bubble leaves a dead ring between the orb
and the rim, which is not what a thing suspended in glass looks like.
Oversizing the canvas by 14% pushes the sphere out to the rim, and
`rimMask` cuts whatever overflows — a few pixels short of the glass, so
the orb sits just inside it rather than welded to it. Orbs that already fill
their canvas lose a few percent off the limb, which is the same crop the
reference bubble makes.
*/
inset: GLASS_INSET,
mask: rimMask(GLASS_INSET),
shadow: "0 24px 48px -26px rgba(0,0,0,0.55)",
over: (
<>
<Layer
style={{
...DISC,
background:
"radial-gradient(ellipse 80% 70% at 28% 20%, rgba(255,255,255,0.16), rgba(255,255,255,0.03) 45%, rgba(255,255,255,0) 72%)"
}}
/>
<Layer
style={{
...DISC,
background:
"radial-gradient(circle closest-side, rgba(255,255,255,0) 0%, rgba(255,255,255,0.0) 55%, rgba(255,255,255,0.05) 99.5%, rgba(255,255,255,0) 100%)",
overflow: 'hidden'
}}
/>
<Layer
style={{
...DISC,
boxShadow: '3px 6px 10px #ffffff20 inset'
}}
/>
{/*
The shell's own darkening, just inside the rim. Invisible on a dark
page — it is black over black — and doing all the work on a light
one, where the white highlights below have nothing to stand out
against and the bubble would otherwise read as a bare drop shadow.
*/}
<Layer
style={{
...DISC,
background:
"radial-gradient(circle closest-side, rgba(0,0,0,0) 78%, rgba(0,0,0,0.05) 93%, rgba(0,0,0,0.02) 100%)"
}}
/>
<Layer
style={{
...DISC,
boxShadow:
"inset 0 6px 12px -7px rgba(255,255,255,0.05), inset 0 -9px 16px -9px rgba(255,255,255,0.1), 0 0 0 1px rgba(0,0,0,0.07)"
}}
/>
<Highlight
style={{
left: "15%",
top: "10%",
width: "38%",
height: "22%",
transform: "rotate(-25deg)",
background:
"radial-gradient(closest-side, rgba(255,255,255,0.9), rgba(255,255,255,0.3) 55%, rgba(255,255,255,0) 100%)",
filter: 'blur(10px)'
}}
/>
</>
)
},
/* ring — two hairlines and nothing else. The restrained one. */
ring: {
inset: 9,
over: (
<>
<Layer style={{ ...DISC, border: "1px solid currentColor", opacity: 0.22 }} />
<Layer inset="5%" style={{ ...DISC, border: "1px solid currentColor", opacity: 0.1 }} />
</>
)
},
/*
dotted — evenly spaced dots around the circumference, turning slowly.
Drawn as one dashed circle with round caps and a near-zero dash length, so
each dash collapses to a dot. `pathLength="64"` renormalizes the path to 64
units first, which is what makes the count exact: the dash period is
literally 1/64th of the circle, so the pattern closes on itself with no
seam where the last gap would otherwise be short.
*/
dotted: {
inset: 10,
animated: true,
over: (
<svg
aria-hidden="true"
viewBox="0 0 100 100"
className="orbkit-w-anim"
style={{ ...svgLayer, animation: "orbkit-w-spin 48s linear infinite" }}
>
<circle
cx="50"
cy="50"
r="47"
fill="none"
stroke="currentColor"
strokeWidth="1.7"
strokeLinecap="round"
pathLength={64}
strokeDasharray="0.0001 0.9999"
opacity={0.45}
/>
</svg>
)
},
/*
ticks — an instrument bezel: a fine minor scale every 6 degrees with a
longer major tick every 30. Both are one repeating conic gradient masked
down to an annulus, so the tick count is set by the gradient's period and
the tick LENGTH by how far in the mask reaches.
*/
ticks: {
inset: 12,
over: (
<>
<Layer
style={{
...DISC,
opacity: 0.32,
background:
"repeating-conic-gradient(from -0.5deg, transparent 0deg 0.2deg, currentColor 0.4deg 0.6deg, transparent 0.8deg 6deg)",
...masked(
"radial-gradient(circle closest-side, transparent 88%, #000 90%, #000 97%, transparent 99%)"
)
}}
/>
<Layer
style={{
...DISC,
opacity: 0.6,
background:
"repeating-conic-gradient(from -0.75deg, transparent 0deg 0.25deg, currentColor 0.5deg 1deg, transparent 1.25deg 30deg)",
...masked(
"radial-gradient(circle closest-side, transparent 80%, #000 82%, #000 97%, transparent 99%)"
)
}}
/>
<Layer style={{ ...DISC, border: "1px solid currentColor", opacity: 0.12 }} />
</>
)
},
/* reticle — viewfinder furniture: corner brackets, cardinal ticks, a track. */
reticle: {
inset: 13,
over: (
<svg aria-hidden="true" viewBox="0 0 100 100" style={svgLayer}>
<g fill="none" stroke="currentColor" strokeWidth="1.2" opacity="0.5">
<path d="M1 13 L1 1 L13 1" />
<path d="M87 1 L99 1 L99 13" />
<path d="M99 87 L99 99 L87 99" />
<path d="M13 99 L1 99 L1 87" />
</g>
<g fill="none" stroke="currentColor" strokeWidth="1" opacity="0.38">
<path d="M50 1 L50 9" />
<path d="M50 91 L50 99" />
<path d="M1 50 L9 50" />
<path d="M91 50 L99 50" />
</g>
<circle
cx="50"
cy="50"
r="46"
fill="none"
stroke="currentColor"
strokeWidth="0.7"
opacity="0.22"
/>
</svg>
)
},
/*
grid — a graticule laid over the orb and masked back to the disc, so the
mesh appears to be etched on the glass in front of it rather than drawn on
the page behind. The lines are 1px whatever the size; the SPACING is a
percentage, so the cell count holds from a gallery thumbnail to a
full-bleed hero.
*/
grid: {
inset: 8,
/*
The ruling sits UNDER the canvas: graph paper the orb rests on, not a
mesh laid over its face. Drawn on top it crosshatched the shader — the
one thing the wrapper is meant to frame. The ring stays over, since it
only ever meets the transparent margin at the canvas edge.
*/
under: (
<Layer
style={{
...DISC,
opacity: 0.18,
backgroundImage:
"repeating-linear-gradient(to right, currentColor 0 1px, transparent 1px 12.5%), repeating-linear-gradient(to bottom, currentColor 0 1px, transparent 1px 12.5%)",
...masked("radial-gradient(circle closest-side, #000 86%, rgba(0,0,0,0) 99%)")
}}
/>
),
over: <Layer style={{ ...DISC, border: "1px solid currentColor", opacity: 0.2 }} />
},
/*
halftone — a print screen over the outer band of the orb. The mask keeps
the middle clear, so the dots read as the image breaking up toward its
edge instead of a texture pasted across the whole face.
*/
halftone: {
inset: 6,
over: (
<Layer
style={{
...DISC,
opacity: 0.5,
backgroundImage: "radial-gradient(currentColor 22%, transparent 24%)",
backgroundSize: "7px 7px",
...masked(
"radial-gradient(circle closest-side, transparent 40%, #000 80%, #000 94%, rgba(0,0,0,0) 100%)"
)
}}
/>
)
},
/*
scanlines — a phosphor tube. Black lines rather than `currentColor`,
because scanlines are the UNLIT gaps between rows and stay dark whatever
the page is; the slow bright band rolling down is the vertical hold
drifting, which is the part that reads as a CRT rather than as stripes.
*/
scanlines: {
inset: 0,
animated: true,
over: (
<>
<Layer
style={{
...DISC,
backgroundImage:
"repeating-linear-gradient(to bottom, rgba(0,0,0,0.45) 0 1px, rgba(0,0,0,0) 1px 3px)",
...masked("radial-gradient(circle closest-side, #000 84%, rgba(0,0,0,0) 100%)")
}}
/>
<span
aria-hidden="true"
style={{
position: "absolute",
inset: 0,
borderRadius: "50%",
overflow: "hidden",
pointerEvents: "none"
}}
>
<span
className="orbkit-w-anim"
style={{
position: "absolute",
left: 0,
right: 0,
top: 0,
height: "30%",
background:
"linear-gradient(to bottom, rgba(255,255,255,0) 0%, rgba(255,255,255,0.07) 50%, rgba(255,255,255,0) 100%)",
animation: "orbkit-w-roll 7s linear infinite"
}}
/>
</span>
<Layer style={{ ...DISC, boxShadow: "inset 0 0 40px -8px rgba(0,0,0,0.5)" }} />
</>
)
}
};
export interface ShaderOrbProps {
/** The orb definition: shader + param schema + state presets. */
variant: OrbVariant;
/** Drives the synthesized volume signals. Defaults to `"idle"`. */
state?: OrbState;
/** Rendered size in CSS pixels. Ignored when `className` sizes the canvas. */
size?: number;
/** Explicit param overrides. Any key present here wins over the state preset. */
params?: OrbParamValues;
/** Explicit color overrides, as hex strings. */
colors?: OrbColorValues;
/**
* Per-state parameter targets, overriding the variant's own. Merged KEY BY
* KEY over what the orb already defines, so `{ thinking: { churn: 1.62 } }`
* retouches one param of one state and leaves every other param — and the
* other two states — exactly as the orb ships them.
*
* This is the prop form of the variant's `statePresets`, so you can retune
* an orb's states from the outside without forking its file. Values still
* glide, so switching states cross-fades into your targets. An explicit
* `params` value outranks this, the same way it outranks the variant.
*/
statePresets?: Partial<Record<OrbState, Record<string, number>>>;
/** The colour counterpart of `statePresets`, merged the same key-by-key way. */
stateColors?: Partial<Record<OrbState, Record<string, string>>>;
/**
* Per-state volume drive, the third member of the same family. Use it to
* give each state its own energy; use `volumes` below instead when you have
* a real signal to feed in, such as live mic level.
*/
stateVolumes?: Partial<Record<OrbState, { input?: number; output?: number }>>;
/**
* Overrides the synthesized volume signals for the active state. The engine
* normally derives these from `state` — a slow breath at idle, a restless
* wander while thinking, speech-shaped peaks while speaking — and most
* shaders read them as their reactivity. Setting either channel here pins
* it instead, which is how the playground lets you dial each state's drive
* independently. Omit a channel to keep its synthesized motion.
*/
volumes?: { input?: number; output?: number };
/** Freeze the animation on the current frame. */
paused?: boolean;
/**
* Stop rendering while the orb is scrolled out of view. Defaults to `true` —
* a page full of orbs would otherwise run a WebGL loop per card.
*/
pauseOffscreen?: boolean;
/** Device-pixel-ratio ceiling. Defaults to `2`. */
maxDpr?: number;
/**
* Decoration drawn around the orb — a glass bubble, a dotted bezel, a
* viewfinder. Defaults to `"none"`, which renders the bare canvas exactly as
* it always has, with no extra element in the tree.
*
* A wrapper never changes the orb's footprint: `size` stays the outer
* diameter and the canvas is inset inside it, so switching one on reflows
* nothing around it.
*/
wrapper?: OrbWrapper;
/**
* The colour a wrapper draws its lines and dots in. Defaults to
* `currentColor` — the inherited text colour — which is what makes the
* bezels legible on a light and a dark page without being told which one
* they are on. `glass` ignores it: glass has no colour of its own.
*/
wrapperColor?: string;
/** Applied to the outermost element — the wrapper when there is one. */
className?: string;
/** Merged onto the outermost element's style. */
style?: CSSProperties;
/** Accessible label. When omitted the orb is hidden from assistive tech. */
ariaLabel?: string;
}
export function ShaderOrb({
variant,
state = "idle",
size,
params,
colors,
statePresets,
stateColors,
stateVolumes,
volumes,
paused = false,
pauseOffscreen = true,
maxDpr = 2,
wrapper = "none",
wrapperColor,
className,
style,
ariaLabel
}: ShaderOrbProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const spec = wrapper === "none" ? undefined : WRAPPER_SPECS[wrapper];
const wrapped = spec !== undefined;
// Live refs: the render loop reads these every frame, so changing a param
// never re-runs the GL setup effect (which would drop the context). Synced in
// an effect rather than during render — a ref write during render is unsafe
// under concurrent rendering, and the loop picks the new value up on the very
// next frame anyway.
/*
Whether the canvas has drawn a frame yet, tracked per variant because a
variant swap mounts a brand new canvas (see the `key` below).
A mounted-but-never-drawn canvas is at the browser's mercy: rather than
the transparent rectangle you would expect, a page that mounts a dozen at
once gets white boxes — and in some browsers a broken-image placeholder —
for as long as the compositor has nothing to raster. That window is not
small here: every orb compiles a full fragment shader synchronously in its
own mount effect, so on the gallery grid the first canvases sit empty
while the last ones are still compiling. Holding each canvas invisible
until its own first frame lands is what makes the grid fade in cleanly
instead of flashing. One state change per orb, once, on mount.
*/
const [paintedKey, setPaintedKey] = useState<string | null>(null);
const painted = paintedKey === variant.key;
const stateRef = useRef<OrbState>(state);
const paramsRef = useRef<OrbParamValues | undefined>(params);
const colorsRef = useRef<OrbColorValues | undefined>(colors);
const statePresetsRef = useRef(statePresets);
const stateColorsRef = useRef(stateColors);
const stateVolumesRef = useRef(stateVolumes);
const volumesRef = useRef(volumes);
const pausedRef = useRef(paused);
useEffect(() => {
stateRef.current = state;
paramsRef.current = params;
colorsRef.current = colors;
statePresetsRef.current = statePresets;
stateColorsRef.current = stateColors;
stateVolumesRef.current = stateVolumes;
volumesRef.current = volumes;
pausedRef.current = paused;
}, [state, params, colors, statePresets, stateColors, stateVolumes, volumes, paused]);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const gl = canvas.getContext("webgl", {
alpha: true,
// No MSAA: the geometry is a single full-screen triangle, so there are no
// primitive edges to antialias — softness comes from the shaders. Leaving
// it on costs the multisample buffers plus a resolve every frame.
antialias: false,
premultipliedAlpha: true
});
if (!gl) return;
const loseExt = gl.getExtension("WEBGL_lose_context");
/*
A "generation" is everything tied to a live context: program, buffers,
observers, render loop. Browsers cap live WebGL contexts per page and
evict the oldest past the cap, and an evicted orb's canvas stays blank
forever unless the app rebuilds — so generations tear down and rebuild on
the lost/restored events instead of assuming the context is immortal.
*/
let announcedPaint = false;
const startGeneration = (): (() => void) => {
if (gl.isContextLost()) return () => {};
// Every generation announces its own first frame: a context that was
// lost and restored has an empty drawing buffer and is hidden again
// (below), so it has to earn its reveal back.
announcedPaint = false;
const vs = compile(gl, gl.VERTEX_SHADER, VERT);
const fs = compile(
gl,
gl.FRAGMENT_SHADER,
ORB_GLSL_HELPERS + paramUniformDecls(variant) + variant.frag
);
const releaseShaders = () => {
if (vs) gl.deleteShader(vs);
if (fs) gl.deleteShader(fs);
};
if (!vs || !fs) return releaseShaders;
const prog = gl.createProgram();
if (!prog) return releaseShaders;
gl.attachShader(prog, vs);
gl.attachShader(prog, fs);
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
console.error("[orbkit] program link error:", gl.getProgramInfoLog(prog));
gl.deleteProgram(prog);
return releaseShaders;
}
gl.useProgram(prog);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
const aPos = gl.getAttribLocation(prog, "aPos");
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
const uRes = gl.getUniformLocation(prog, "uRes");
const uTime = gl.getUniformLocation(prog, "uTime");
const uAnim = gl.getUniformLocation(prog, "uAnim");
const uInput = gl.getUniformLocation(prog, "uInput");
const uOutput = gl.getUniformLocation(prog, "uOutput");
const paramLocs = variant.params.map((p) => ({
def: p,
loc: gl.getUniformLocation(prog, `uP_${p.key}`)
}));
const colorLocs = variant.colors.map((c) => ({
def: c,
loc: gl.getUniformLocation(prog, `uC_${c.key}`)
}));
/* --- sizing: track the element box, not a one-shot measurement ------- */
// Backing-store scale, stepped down by the adaptive-resolution logic in
// the loop when the GPU can't hold frame rate. CSS size never changes —
// the browser upscales, which these soft shaders absorb gracefully.
let resScale = 1;
const resize = () => {
const dpr = Math.min(window.devicePixelRatio || 1, maxDpr) * resScale;
const w = Math.max(1, Math.round(canvas.clientWidth * dpr));
const h = Math.max(1, Math.round(canvas.clientHeight * dpr));
if (canvas.width !== w || canvas.height !== h) {
canvas.width = w;
canvas.height = h;
gl.viewport(0, 0, w, h);
}
/*
Uploaded UNCONDITIONALLY, outside the size guard. A rebuilt
generation (React strict-mode remount, a restored context) links a
fresh program whose uRes starts at zero — and the canvas usually
already holds the right backing size, so an upload gated behind
the resize never ran. With uRes = 0, orbUV() divides by zero and
every fragment lands transparent: a healthy context, a bound
program, and a permanently blank orb.
*/
gl.uniform2f(uRes, w, h);
};
resize();
const resizeObserver =
typeof ResizeObserver !== "undefined" ? new ResizeObserver(resize) : null;
resizeObserver?.observe(canvas);
/* --- visibility: don't burn a render loop on an offscreen orb -------- */
let visible = !pauseOffscreen;
const intersectionObserver =
pauseOffscreen && typeof IntersectionObserver !== "undefined"
? new IntersectionObserver(
(entries) => {
visible = Boolean(entries[0]?.isIntersecting);
if (visible) {
last = performance.now() / 1000;
}
},
{ rootMargin: "150px 0px", threshold: 0 }
)
: null;
if (intersectionObserver) {
intersectionObserver.observe(canvas);
} else {
visible = true;
}
const reduceMotion =
typeof window.matchMedia === "function" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/* --- driver state ---------------------------------------------------- */
let tSec = 0;
// random phase so two orbs on the same page never look synchronized
let anim = Math.random() * 100;
let speed = 0.1;
const cur = { in: 0, out: 0.3 };
const presets = variant.statePresets;
const paramCur: Record<string, number> = {};
const paramVel: Record<string, number> = {};
const paramClocks: Record<string, number> = {};
const colorCur: Record<string, [number, number, number]> = {};
const colorVel: Record<string, [number, number, number]> = {};
let speedVel = 0;
const [initialIn, initialOut] = targetVolumes(stateRef.current, 0);
cur.in = initialIn;
cur.out = initialOut;
let last = performance.now() / 1000;
let raf = 0;
// smoothed frame time for the adaptive-resolution check
let frameEma = 1 / 60;
const uploadAndDraw = (dt: number, snap = false) => {
// Synthesized from the state, unless a channel is pinned via
// `volumes`. Pinned values still glide on the same easing, so dialing
// one in the playground cross-fades rather than jumping.
const [tin, tout] = targetVolumes(stateRef.current, tSec);
// Same order as params and colours: direct prop, then the per-state
// map, then the engine's own synthesis.
const liveVolumes = volumesRef.current;
const stateVolume = stateVolumesRef.current?.[stateRef.current];
const targetIn = liveVolumes?.input ?? stateVolume?.input ?? tin;
const targetOut = liveVolumes?.output ?? stateVolume?.output ?? tout;
const kVol = 1 - Math.exp(-dt * 12);
cur.in += (targetIn - cur.in) * kVol;
cur.out += (targetOut - cur.out) * kVol;
/*
Flow speed follows the output volume. It multiplies every integrated
clock's increment, so it is a RATE: easing it quickly makes the orb
visibly lurch — a state change would spin the orb up hard before the
params had finished gliding.
It is therefore eased on the same constant as the params below, so a
state change ramps its motion over the same half second that its look
takes to cross-fade. The steady-state values are unchanged, so a
speaking orb still flows faster than an idle one; only the transition
into that rate is gradual.
*/
const targetSpeed = 0.1 + (1 - Math.pow(cur.out - 1, 2)) * 0.9;
if (snap) {
speed = targetSpeed;
speedVel = 0;
} else {
springStep(speed, speedVel, targetSpeed, dt, PARAM_EASE);
speed = springOut.x;
speedVel = springOut.v;
}
anim += dt * speed;
gl.uniform1f(uTime, tSec * 0.5);
gl.uniform1f(uAnim, anim);
gl.uniform1f(uInput, cur.in);
gl.uniform1f(uOutput, cur.out);
// Resolution order per param: explicit `params` → `statePresets`
// prop → the variant's own preset → schema default. The two middle
// steps are per-key, so overriding one param of one state leaves the
// rest of that state alone. Values glide rather than snap.
const liveParams = paramsRef.current;
const statePreset = presets?.[stateRef.current];
const overridePreset = statePresetsRef.current?.[stateRef.current];
for (const { def, loc } of paramLocs) {
const explicit = liveParams?.[def.key];
const target =
typeof explicit === "number"
? explicit
: (overridePreset?.[def.key] ?? statePreset?.[def.key] ?? def.default);
const curVal = paramCur[def.key] ?? target;
let next: number;
if (snap) {
next = target;
paramVel[def.key] = 0;
} else {
springStep(curVal, paramVel[def.key] ?? 0, target, dt, PARAM_EASE);
next = springOut.x;
paramVel[def.key] = springOut.v;
}
paramCur[def.key] = next;
if (def.integrate) {
const clock =
(paramClocks[def.key] ?? (paramClocks[def.key] = Math.random() * 100)) +
dt * speed * next;
paramClocks[def.key] = clock;
gl.uniform1f(loc, clock);
} else {
gl.uniform1f(loc, next);
}
}
// Same resolution order and same easing as params, so a state change
// cross-fades the palette instead of cutting to it.
const liveColors = colorsRef.current;
const stateColor = variant.stateColors?.[stateRef.current];
const overrideColor = stateColorsRef.current?.[stateRef.current];
for (const { def, loc } of colorLocs) {
const target = hexToRgb(
liveColors?.[def.key] ??
overrideColor?.[def.key] ??
stateColor?.[def.key] ??
def.default
);
const curCol = (colorCur[def.key] ??= [...target] as [number, number, number]);
const velCol = (colorVel[def.key] ??= [0, 0, 0]);
for (let i = 0; i < 3; i++) {
if (snap) {
curCol[i] = target[i];
velCol[i] = 0;
} else {
springStep(curCol[i], velCol[i], target[i], dt, PARAM_EASE);
curCol[i] = springOut.x;
velCol[i] = springOut.v;
}
}
gl.uniform3f(loc, curCol[0], curCol[1], curCol[2]);
}
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
// Deferred a microtask: the first of these draws runs synchronously
// inside this effect, and a sync setState there trips the compiler
// lint. A microtask still resolves before the browser paints, so the
// reveal is not delayed by a frame.
if (!announcedPaint) {
announcedPaint = true;
queueMicrotask(() => {
/*
Re-check: the context can be evicted between this draw and the
microtask, and mounting one more orb anywhere on the page is
enough to do it — opening the details drawer over a full gallery
is exactly that. Revealing on the strength of a frame that has
already been thrown away puts a dead canvas on screen, which is
what the browser draws its broken-canvas placeholder over. The
generation that follows the restore announces again.
*/
if (gl.isContextLost()) return;
setPaintedKey(variant.key);
});
}
};
const releaseGL = () => {
resizeObserver?.disconnect();
intersectionObserver?.disconnect();
gl.deleteProgram(prog);
gl.deleteShader(vs);
gl.deleteShader(fs);
gl.deleteBuffer(buf);
};
if (reduceMotion) {
// One representative frame, then stop — snapped straight onto the
// state's targets, since a spring would only be part-way there.
tSec = 1;
uploadAndDraw(1, true);
return releaseGL;
}
const loop = () => {
raf = requestAnimationFrame(loop);
const now = performance.now() / 1000;
const dt = Math.min(now - last, 0.05);
last = now;
if (!visible || pausedRef.current) return;
tSec += dt;
/*
Adaptive resolution. When the smoothed frame time sits above ~30fps,
the GPU is drowning in fragment work (these shaders are pure fill
cost), so step the backing store down 20% and re-measure. Steps only
go down — never back up — so the resolution can't oscillate. The
warm-up guard keeps page-load jank (hydration, first compiles) from
triggering a downgrade the GPU never asked for.
*/
/*
Only unstalled frames are evidence about GPU fill cost. `dt` above is
clamped at 0.05, so a frame that hits the clamp is the main thread
having been blocked — a slider drag re-rendering React, a GC pause, a
tab regaining focus — and feeding those in made UI jank look
identical to a drowning GPU.
*/
if (dt < 0.05) frameEma += (dt - frameEma) * 0.08;
if (tSec > 1.5 && frameEma > 1 / 34 && resScale > 0.5) {
resScale = Math.max(0.5, resScale * 0.8);
frameEma = 1 / 60; // require fresh evidence before the next step
resize();
} else if (tSec > 1.5 && frameEma < 1 / 55 && resScale < 1) {
/*
And step back up once frames are comfortably fast again. This used
to be one-way, on the reasoning that it could not then oscillate —
but that also meant one transient stall permanently halved the
orb's resolution, and at half resolution a high-frequency shader
aliases into shimmer that reads as the shader itself misbehaving.
The gap between the two thresholds (29ms down, 18ms up) is the
hysteresis that stops it hunting.
*/
resScale = Math.min(1, resScale / 0.8);
frameEma = 1 / 60;
resize();
}
uploadAndDraw(dt);
};
/*
First frame synchronously, before entering the rAF loop. rAF does not
run at all in hidden documents (background tabs, embedded previews),
so a freshly mounted orb would otherwise sit fully transparent until
the page next becomes visible — a grid of mounted, healthy, blank
canvases. The sync frame guarantees every mount paints: background
documents get a static frame, visible ones start animating over it.
dt = 1 lands the param glide on its targets, as in the reduce-motion
frame above.
*/
uploadAndDraw(1);
loop();
return () => {
cancelAnimationFrame(raf);
releaseGL();
};
};
/*
Wire the canvas's lifecycle controller. The listeners are attached ONCE
per canvas element and never removed, deliberately: the lost event must
be canceled even while no orb is mounted on the canvas — an uncanceled
webglcontextlost marks the context permanently unrestorable, and the
router can show this exact canvas again later. Whether a loss leads to
a revival is decided by `desired`, not by listener presence.
*/
/*
A canvas whose context has gone is not simply blank: Chrome paints its
broken-image placeholder over the element's whole box — the white square
you see on a reloaded grid. Hide the canvas the moment the context is
lost, and let the generation that follows a restore reveal it again.
*/
const hideNow = () => {
/*
Both, deliberately. The style write lands in this tick — the browser
paints its placeholder over a dead canvas immediately, and a busy main
thread can hold a React update for several frames. The state change is
what keeps React's own view in sync, so the reveal that follows a
restore clears the inline value again rather than fighting it.
*/
canvas.style.opacity = "0";
setPaintedKey(null);
};
const onContextLostHide = () => hideNow();
canvas.addEventListener("webglcontextlost", onContextLostHide);
/*
Hand the context back before the next document asks for one.
A hard reload never runs this effect's cleanup — the document is
discarded whole — so the outgoing page's contexts are still alive while
the incoming page allocates its own. On a grid of orbs that puts the
live count past the browser's ~16 cap, and the ones it evicts are
exactly the canvases that come back as placeholders until the restore
path catches them. `persisted` is a bfcache suspend, where the page is
shown again untouched and must keep everything it holds.
*/
const onPageHide = (event: PageTransitionEvent) => {
if (event.persisted) return;
try {
loseExt?.loseContext();
} catch {
// Already released — nothing to hand back.
}
};
window.addEventListener("pagehide", onPageHide);
let ctl = canvasControllers.get(canvas);
if (!ctl) {
const created: CanvasContextController = { desired: false, start: null, stopGen: null };
canvas.addEventListener("webglcontextlost", (event) => {
event.preventDefault(); // always cancel — keeps the context restorable
created.stopGen?.();
created.stopGen = null;
if (created.desired) {
/*
Ask for the context back — but in a LATER task. The browser only
marks a loss as restorable once the lost event's dispatch has
completed and it has seen the canceled flag, so a restoreContext()
issued during dispatch (or before it, as the mount path may) is
silently refused. This is the path a synchronous cleanup+setup
pair hits — React re-running the effect on the same canvas loses
the context and wants it right back. For losses we didn't cause
(eviction, GPU reset) the call may refuse; the canceled event then
lets the browser restore on its own schedule.
*/
setTimeout(() => {
if (!created.desired) return;
try {
loseExt?.restoreContext();
} catch {
// Natural loss — restoration is the browser's call now.
}
}, 0);
}
});
canvas.addEventListener("webglcontextrestored", () => {
if (created.desired && created.start) {
created.stopGen = created.start();
}
});
canvasControllers.set(canvas, created);
ctl = created;
}
const controller = ctl;
controller.desired = true;
controller.start = startGeneration;
if (gl.isContextLost()) {
// A previous run on this canvas released the context (effect re-run, or
// the router re-showing a kept-alive page). If the lost event already
// dispatched this request is honored now; if it is still queued, the
// lost handler above re-requests it on dispatch.
try {
loseExt?.restoreContext();
} catch {
// No restore path — the orb stays blank rather than throwing.
}
} else {
controller.stopGen = startGeneration();
}
return () => {
/*
Hide BEFORE tearing the context down.
This cleanup releases the context deliberately, and the lost event it
provokes is dispatched asynchronously — by which point the listener
below is unhooked and the replacement effect has not drawn yet. That
leaves a revealed canvas with a dead context, which is precisely what
the browser paints its broken-canvas placeholder over. The window is
not rare: any prop change in this effect's deps re-runs it, so it hits
every time the drawer switches preview example (maxDpr differs on the
layout one) or a wrapper is toggled.
*/
hideNow();
canvas.removeEventListener("webglcontextlost", onContextLostHide);
window.removeEventListener("pagehide", onPageHide);
controller.desired = false;
controller.start = null;
controller.stopGen?.();
controller.stopGen = null;
/*
Release the context NOW instead of when the canvas is garbage
collected. Browsers cap live WebGL contexts per page (~8–16) and evict
the oldest when the cap is hit — client-side navigation that unmounts
and remounts a page of orbs otherwise piles up zombie contexts until
freshly mounted orbs get evicted and render blank.
*/
try {
loseExt?.loseContext();
} catch {
// Context already lost — nothing to release.
}
};
/*
`wrapped` is in here because turning a wrapper on or off moves the canvas
from being this component's root element to being a child of the wrapper
div — React drops the old element and mounts a new one, and the GL
context, its observers and its render loop all belong to the old one. Any
other prop leaves the element alone, INCLUDING a swap between two
wrappers: the canvas keeps its slot among the decoration layers, so
glass -> ring reuses the context instead of rebuilding it.
*/
}, [variant, pauseOffscreen, maxDpr, wrapped]);
const sizeStyle: CSSProperties =
size === undefined ? {} : { width: size, height: size };
/*
Spread ahead of the caller's `style`, so an orb that wants to own its own
opacity still can — it simply opts out of the reveal.
A hard flip, deliberately: no transition, no fade. A hidden document
(background tab, embedded preview) does not advance CSS transitions, so a
faded reveal left orbs pinned at zero in exactly the case the synchronous
first frame above exists to serve — mounted, healthy, and invisible. The
cut is not a pop either way, since it happens on the frame the orb first
has something to show.
*/
const revealStyle: CSSProperties = painted ? {} : { opacity: 0 };
const canvas = (
<canvas
// A lost WebGL context can't be reused, so each variant gets a fresh canvas.
key={variant.key}
ref={canvasRef}
className={spec ? undefined : className}
style={
spec
? {
display: "block",
position: "absolute",
left: `${spec.inset}%`,
top: `${spec.inset}%`,
width: `${100 - 2 * spec.inset}%`,
height: `${100 - 2 * spec.inset}%`,
...revealStyle,
...(spec.mask ? masked(spec.mask) : {})
}
: { display: "block", ...sizeStyle, ...revealStyle, ...style }
}
role={!spec && ariaLabel ? "img" : undefined}
aria-label={spec ? undefined : ariaLabel}
aria-hidden={!spec && ariaLabel ? undefined : true}
/>
);
if (!spec) return canvas;
/*
Wrapped: the box becomes the orb's footprint and the canvas is absolutely
positioned inside it. `under`, the canvas and `over` are all positioned
with an auto z-index, so they paint in DOM order — decoration behind the
orb, then the orb, then decoration in front of it.
`aspectRatio` is the fallback for the sizeless case: `size` is optional
(callers may size the orb with a class instead), and an absolutely
positioned canvas contributes nothing to its parent's height, so without
it a class that only sets a width would collapse the box to zero.
*/
return (
<div
className={className}
style={{
position: "relative",
aspectRatio: "1 / 1",
...(spec.shadow ? { borderRadius: "50%", boxShadow: spec.shadow } : {}),
...sizeStyle,
...(wrapperColor ? { color: wrapperColor } : {}),
...style
}}
role={ariaLabel ? "img" : undefined}
aria-label={ariaLabel}
aria-hidden={ariaLabel ? undefined : true}
>
{spec.animated ? (
<style
href={WRAPPER_STYLE_HREF}
precedence="default"
dangerouslySetInnerHTML={{ __html: WRAPPER_CSS }}
/>
) : null}
{spec.under}
{canvas}
{spec.over}
</div>
);
}
3. components/ui/shdr-01.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-01 — a cut-glass orb whose own shell does the dispersing.
Ported from a golfed twigl listing:
for(float i,z,d,s;i++<2e1;o+=(cos(s-z+vec4(0,1,8,0))+1.)/d){
vec3 p=z*normalize(FC.rgb*2.-r.xyy),a=p;
for(d=2.;d++<7.;)a-=sin(a*d+t+i).yzx/d;
z+=d=abs(2.-max(p=abs(p),p.y).x)+abs(cos(s=a.z+a.y-t))/7.;}
o=tanh(o/2e2);
What it actually is, decoded:
- abs(2. - max(|x|,|y|)) is an infinite SQUARE TUBE along z, half-width 2,
and the camera sits at the origin inside it — an endless glowing tunnel.
- abs(cos(a.z + a.y - t))/7. adds translucent SHEETS wherever the warped
field crosses cos zero; the march slows there, and since each step is
weighted 1/d, slow means bright.
- cos(s - z + vec4(0,1,8,0)) reads one palette phase per channel — that
per-channel offset is the whole "dispersion": the sheets split into
rainbow striations, coupled to depth through the -z term.
Port decisions, each one a documented trap in the README:
- A tunnel is an open shape and reads as a portal, not an orb — and a
prism bounded inside a ball reads as an object in a jar. So the ORB IS
THE OBJECT: the square tube becomes a sphere-radius shell evaluated on
the turbulence-warped point, which makes the ball's own surface the
thing that disperses. Rays grazing the limb ride the shell for a long
arc — many small-d, high-weight steps — so the rim glows the way glass
does, for free. The README's closed-shell trap (accumulation on a shell
is uniform) does not bite because the detail comes from the warped
sheet field, not from the accumulation itself.
- The silhouette is cut ANALYTICALLY in main() from each ray's closest
approach to the sphere, so the edge is exact and tunably sharp rather
than a fuzzy envelope fade.
- The golfed listing relies on i, z, d, s starting at zero. Uninitialised
locals are UNDEFINED in GLSL ES 1.0 — everything is explicit below.
- The clock only ever enters as an ADDITIVE PHASE (inside sin/cos), never
scaling a per-step quantity, so the unbounded integrated clock is safe
here — no per-step degeneration to fix, unlike the sweep-angle bug this
repo hit before.
- The prism is oriented with real orthonormal rotations: a static tilt and
a spin driven by its own integrated clock, so changing the spin rate
never jumps the phase. No golfed cos-phase "rotation" matrices — those
breathe scale and squash the silhouette.
---------------------------------------------------------------------------- */
/*
* Step counts are `#define`s: ES 1.0 requires constant loop bounds.
* TURB is 5 to match the original's five octaves (d = 3..7).
*/
const DISPERSION_FRAG = `
#define STEPS 60
#define TURB 5
#define AA 1
// Volume-reactive values, resolved once per fragment in main().
float dispersionTurb;
float dispersionExposure;
mat2 rot2(float a) {
float c = cos(a);
float s = sin(a);
return mat2(c, -s, s, c);
}
vec3 dispersionRender(vec2 fragCoord) {
float animTime = uP_speed; // integrated clock: turbulence + sheet drift
float spinAng = uP_spin; // integrated clock: prism precession
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
vec3 ro = vec3(0.0, 0.0, uP_camDist);
vec3 rd = normalize(vec3(uv, -uP_focal));
vec3 acc = vec3(0.0);
// transmittance carried front-to-back, as in shdr-21 and the README's
// diffusion note — near sheets veil far ones, which is where the depth
// read comes from
float T = 1.0;
/*
March only the span that can contribute. The tube is infinite, so a ray
grazing its wall far in FRONT of the ball would otherwise stall there —
small d, step after step — and exhaust STEPS before ever reaching the
envelope, leaving a dark notch across the orb. Start at the envelope's
near edge and break past its far edge; all 60 steps land where the
envelope is non-zero.
*/
float z = max(uP_camDist - uP_envRadius * 1.3, 0.0);
float zEnd = uP_camDist + uP_envRadius * 1.3;
for (int it = 0; it < STEPS; it++) {
vec3 p = ro + rd * z;
// orient the prism: static tilt about x, then precession about y from
// the spin clock. Real rotations — see the header note.
vec3 q = p;
q.xz = rot2(spinAng) * q.xz;
q.yz = rot2(uP_tilt) * q.yz;
// turbulence in the prism's rotating frame; the +float(it) offset is the
// original's +i, decorrelating octaves per step for a smoky depth
vec3 a = q;
for (int j = 0; j < TURB; j++) {
float dj = float(j) + 3.0;
a -= dispersionTurb * sin(a * dj + animTime + float(it)).yzx / dj;
}
/*
The orb's own shell, in place of the original's square tube
(golfed there as max(p=abs(p),p.y).x — just max(|x|,|y|)). Evaluated
on the WARPED point, so the turbulence shimmers the surface itself
like an oil film; at turb 0 it is a perfect glass shell. The cos
sheets fill the interior with the dispersive volume.
*/
float wall = abs(length(a) - uP_envRadius);
float s = a.z + a.y - animTime;
float d = max(wall + abs(cos(s)) / uP_sheets, 1e-4);
/*
Per-channel palette: one cosine phase per channel, scaled by uP_disperse
(0 collapses to monochrome breathing, 1 is the original rainbow). The
-z term couples depth into the phase, which is what turns the sheets
into striations; uP_stria scales it.
The CLAMP is load-bearing, same as the other accumulators here: 1/d
spikes where a ray grazes the wall exactly where a sheet sits, and one
unclamped sample would own the whole 60-step sum at some phases.
*/
vec3 w = (cos(s - z * uP_stria + vec3(0.0, 1.0, 8.0) * uP_disperse) + 1.0) / d;
w = min(w, vec3(uP_stepClamp));
/*
Envelope: bounds the sheet glow (the cos field lives EVERYWHERE in
space, not just inside the ball) and adds the uP_fill floor that
guarantees a body. The outer bound sits 12% PAST the radius on
purpose: the shell IS the radius now, the silhouette is cut
analytically in main(), and a hard cut only reads as a sharp edge if
there is still emission left at the boundary to cut. envCore is where
the plateau saturates — 1 keeps the shell at full strength, lower
values pull the brightness into the core.
*/
float env = smoothstep(uP_envRadius * 1.12, uP_envRadius * uP_envCore, length(p));
w = (w + uP_fill) * env;
acc += T * w;
T *= exp(-dot(w, vec3(0.299, 0.587, 0.114)) * uP_scatter);
z += d;
if (T < 0.004 || z > zEnd) break;
}
return acc;
}
void main() {
dispersionTurb = uP_turb * (1.0 + 0.5 * uInput);
dispersionExposure = uP_exposure * (1.0 - 0.35 * uOutput);
vec3 acc = vec3(0.0);
#if AA > 1
for (int mx = 0; mx < AA; mx++) {
for (int my = 0; my < AA; my++) {
vec2 offset = vec2(float(mx), float(my)) / float(AA) - 0.5;
acc += dispersionRender(gl_FragCoord.xy + offset);
}
}
acc /= float(AA * AA);
#else
acc = dispersionRender(gl_FragCoord.xy);
#endif
// tanh tone map, as in the original but per channel and with a tunable
// knee — the envelope and transmittance change the accumulator's scale
// completely, so the golfed /2e2 constant means nothing here
vec3 col = tanh3(acc / max(dispersionExposure, 1.0));
col = pow(clamp(col, 0.0, 1.0), vec3(uP_contrast));
// saturation about luminance, then the tint
float lum = dot(col, vec3(0.299, 0.587, 0.114));
col = mix(vec3(lum), col, uP_saturation);
col *= uC_tint;
// alpha from the brightest channel, not luminance — a saturated violet
// fringe has low luminance but must not go transparent
float peak = max(col.r, max(col.g, col.b));
float a = clamp(peak * uP_alphaGain, 0.0, 1.0);
/*
Analytic silhouette: the perpendicular distance from the sphere's centre
to this pixel's ray, against the shell radius. Exact — not a fade of the
accumulated glow — which is what makes the edge read as cut glass.
uP_edge trades the transition band: 1 is a couple of pixels, 0 falls
back to a soft feather. Colour AND alpha, as always.
*/
vec3 mrd = normalize(vec3(orbUV(), -uP_focal));
float closest = length(cross(vec3(0.0, 0.0, uP_camDist), mrd));
float band = mix(0.35, 0.012, clamp(uP_edge, 0.0, 1.0));
float mask = 1.0 - smoothstep(uP_envRadius * (1.0 - band), uP_envRadius * 1.005, closest);
col *= mask;
a *= mask;
// Fade colour as well as alpha — with premultiplied output, fading only
// alpha leaves the pixel emitting at full brightness up to the cutoff,
// which reads as a hard rim. With the analytic mask doing the real work
// this is only a safety taper at the frame boundary.
float r2d = length(orbUV());
float fade = 1.0 - smoothstep(uP_edgeFade, 1.0, r2d);
col *= fade;
a *= fade;
// Emitted light, so rgb is already premultiplied — do NOT scale by alpha
// again (see the same note in shdr-31).
gl_FragColor = vec4(col, a);
}
`;
export const shdr01Orb: OrbVariant = {
key: "shdr-01",
label: "SHDR-01",
note: "cut-glass orb with a dispersive, turbulent interior",
frag: DISPERSION_FRAG,
params: [
{ key: "speed", label: "Anim speed", min: 0.015, max: 10, step: 0.05, default: 0.5, integrate: true },
{ key: "spin", label: "Spin rate", min: 0, max: 5, step: 0.03, default: 0.25, integrate: true },
{ key: "camDist", label: "Camera distance", min: 1, max: 50, step: 0.3, default: 7 },
{ key: "focal", label: "Lens", min: 0.15, max: 15, step: 0.1, default: 2.25 },
{ key: "tilt", label: "Field tilt", min: 0, max: 4, step: 0.02, default: 0.5 },
{ key: "turb", label: "Turbulence", min: 0, max: 5, step: 0.03, default: 0.3 },
{ key: "sheets", label: "Sheet density", min: 1, max: 60, step: 0.5, default: 7 },
{ key: "disperse", label: "Dispersion", min: 0, max: 5, step: 0.03, default: 1 },
{ key: "stria", label: "Striation depth", min: 0, max: 10, step: 0.05, default: 1 },
{ key: "envRadius", label: "Envelope radius", min: 0.15, max: 15, step: 0.1, default: 2.6 },
{ key: "envCore", label: "Envelope core", min: 0.3, max: 1.02, step: 0.01, default: 1 },
{ key: "fill", label: "Body fill", min: 0, max: 100, step: 0.3, default: 1.5 },
{ key: "stepClamp", label: "Step clamp", min: 0.3, max: 300, step: 1.5, default: 20 },
{ key: "scatter", label: "Diffusion", min: 0, max: 0.5, step: 0.003, default: 0.02 },
{ key: "exposure", label: "Exposure", min: 1.5, max: 1500, step: 10, default: 60 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 15, step: 0.1, default: 1 },
{ key: "saturation", label: "Saturation", min: 0, max: 4, step: 0.02, default: 1 },
{ key: "alphaGain", label: "Alpha gain", min: 0.05, max: 15, step: 0.1, default: 2 },
{ key: "edge", label: "Edge sharpness", min: 0, max: 1, step: 0.01, default: 1 },
{ key: "edgeFade", label: "Halo falloff", min: 0.1, max: 3, step: 0.015, default: 0.98 }
],
colors: [{ key: "tint", label: "Tint", default: "#ffffff" }],
statePresets: {
idle: {
speed: 0.5,
spin: 0.25,
turb: 0.3,
disperse: 1,
sheets: 7,
exposure: 60,
scatter: 0.02,
alphaGain: 2
},
thinking: {
speed: 0.6,
spin: 0.5,
turb: 0.35,
disperse: 1.1,
sheets: 7,
exposure: 57,
scatter: 0.019,
alphaGain: 2.1
},
// loudest: fast drift, dense sheets, wide rainbow
speaking: {
speed: 1.2,
spin: 0.7,
turb: 0.55,
disperse: 1.5,
sheets: 5.5,
exposure: 45,
scatter: 0.015,
alphaGain: 2.5
}
}
};
export type Shdr01Props = Omit<ShaderOrbProps, "variant">;
export function Shdr01({ size = 280, ...rest }: Shdr01Props) {
return <ShaderOrb variant={shdr01Orb} size={size} {...rest} />;
}
export default Shdr01;
4. components/ui/shdr-02.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-02 — ornate scrollwork wrapped onto a sphere.
Ten layers, each running the same nine-step iterative warp on a 2D point:
v = p; for f in 1..9: v += sin(v.yx * f + i + t) / f
The `v.yx` swizzle is what makes it ornate rather than noisy — each step
feeds a coordinate back into the other axis, so the field folds into scrolls
and shells instead of blurring. Brightness is `1 / length(v)`: wherever the
warp happens to land a point near the origin, that layer flares. The layer
index `i` both offsets the warp phase and picks the colour, so the ten sheets
are differently coloured and never coincide.
The original is a full-screen 2D field. Rather than mask a disc out of it —
which just reads as a flat coin — the pattern is sampled through a
STEREOGRAPHIC projection of the orb's dome, so it compresses toward the rim
the way a texture on a real sphere does, and rolls as the dome rotates.
---------------------------------------------------------------------------- */
/*
* Layer and warp counts are `#define`s: ES 1.0 requires constant loop bounds.
* 10x9 is ~90 sin() per pixel — light next to the raymarched orbs.
*/
const ROCAILLE_FRAG = `
#define LAYERS 10
#define WARP 9
void main() {
vec2 uv = orbUV();
float R = uP_radius + uP_swell * uInput;
float r2d = length(uv);
float mask = smoothstep(0.012, -0.012, r2d - R);
float nr = clamp(r2d / max(R, 0.001), 0.0, 1.0);
float z = sqrt(max(1.0 - nr * nr, 0.0));
float animTime = uP_speed; // integrated clock
vec3 sp = vec3(uv / max(R, 0.001), z);
/*
Stereographic projection: sphere → plane. Equal steps in screen space map to
ever-larger steps in pattern space as the rim is approached, which is exactly
the foreshortening that sells a flat field as wrapped geometry. uP_bulge
softens the divisor — higher flattens it back toward a disc.
DO NOT rotate sp in 3D before this. Spinning the dome about Y mixes sp.x
into sp.z, so near the rim the divisor collapses toward zero, p explodes,
length(v) goes huge, and 1/length(v) leaves most of the sphere black. That
is what hollowed the orb out. The projection needs sp.z to stay the
view-facing component.
Motion comes from animTime inside the warp below instead, which changes the
scrollwork without ever touching the projection. If you want the pattern to
travel, rotate or translate p here in 2D — that is projection-safe.
*/
vec2 p = sp.xy / (sp.z + 1.0 + uP_bulge) * uP_zoom;
// projection-safe 2D drift, in place of a dome spin
float sw = animTime * uP_swirl;
p = mat2(cos(sw), -sin(sw), sin(sw), cos(sw)) * p;
// input volume tightens the warp; output volume brightens the layers
float warpFreq = uP_warpFreq * (1.0 + 0.35 * uInput);
float gain = uP_gain * (0.75 + 0.7 * uOutput);
vec4 acc = vec4(0.0);
for (int i = 1; i <= LAYERS; i++) {
float fi = float(i);
vec2 v = p;
for (int j = 1; j <= WARP; j++) {
float f = float(j);
v += sin(v.yx * f * warpFreq + fi + animTime) / f;
}
// uP_coreClamp guards the divide and doubles as the flare size — the
// original has no guard and relies on length(v) never hitting zero.
//
// uP_falloff is the FILL control. The original's plain 1/length(v) decays
// fast, so only the knots where the warp lands near the origin light up and
// the rest of the sphere stays near black. An exponent below 1 flattens the
// tail — at length(v)=10 a 0.6 power is ~4x brighter than 1/x — which lifts
// the filigree between the knots without blowing the knots themselves out.
float rad = pow(max(length(v), uP_coreClamp), uP_falloff);
acc += (cos(fi + vec4(0.0, 1.0, 2.0, 3.0) + uP_hueShift) + 1.0) / 6.0 / rad;
}
// the original squares before tone-mapping, which is what crushes the dim
// filigree and leaves the bright scrollwork
vec3 col = tanh3(acc.rgb * acc.rgb * gain);
// rim light, so the silhouette reads as a ball rather than a cut-out
float fresnel = pow(1.0 - z, uP_rimPow);
col += vec3(fresnel) * uP_rim;
float lum = dot(col, vec3(0.2126, 0.7152, 0.0722));
float visibility = clamp(lum * uP_alphaGain + uP_baseVis + fresnel * 0.25, 0.0, 1.0);
// Surface-lit and mask-bounded, so alpha is coverage: premultiply normally.
// (Unlike Corona and Nimbus, which are emissive and must not be.)
float a = mask * visibility;
gl_FragColor = vec4(col * a, a);
}
`;
export const shdr02Orb: OrbVariant = {
key: "shdr-02",
label: "SHDR-02",
note: "ornate scrollwork on a rolling dome",
frag: ROCAILLE_FRAG,
params: [
{ key: "speed", label: "Anim speed", min: 0.015, max: 10, step: 0.05, default: 0.5, integrate: true },
{ key: "swirl", label: "Swirl", min: 0, max: 3, step: 0.015, default: 0.06 },
{ key: "radius", label: "Radius", min: 0.15, max: 3, step: 0.015, default: 0.9 },
{ key: "swell", label: "Input swell", min: 0, max: 1, step: 0.01, default: 0.06 },
{ key: "zoom", label: "Pattern zoom", min: 0.15, max: 40, step: 0.2, default: 4.4 },
{ key: "bulge", label: "Sphere bulge", min: 0, max: 10, step: 0.05, default: 0.35 },
{ key: "warpFreq", label: "Warp frequency", min: 0.05, max: 10, step: 0.05, default: 1.5 },
{ key: "hueShift", label: "Hue shift", min: 0, max: 6.283, step: 0.05, default: 0 },
{ key: "coreClamp", label: "Flare size", min: 0.003, max: 3, step: 0.015, default: 0.12 },
{ key: "falloff", label: "Fill", min: 0.05, max: 4, step: 0.05, default: 1 },
{ key: "gain", label: "Exposure", min: 0.015, max: 10, step: 0.05, default: 0.55 },
{ key: "rim", label: "Rim light", min: 0, max: 3, step: 0.015, default: 0.12 },
{ key: "rimPow", label: "Rim tightness", min: 0.15, max: 15, step: 0.1, default: 2.2 },
{ key: "alphaGain", label: "Alpha gain", min: 0.05, max: 15, step: 0.1, default: 2.4 },
{ key: "baseVis", label: "Base visibility", min: 0, max: 1.5, step: 0.01, default: 0.08 }
],
colors: [],
/*
* No dome rotation in any state — see the projection note in the shader. The
* states differ by how fast the scrollwork evolves and how dense it is.
*
* `swirl` is deliberately absent from every preset: the rotation angle is
* animTime * swirl, so a per-state swirl value makes a state change sweep
* the angle by (accumulated clock) x (delta) — the whole dome visibly spins
* while the preset glides. Held constant, the angle stays continuous and a
* state change only retimes the scrollwork.
*/
statePresets: {
// calm: slow evolution, open scrollwork
idle: {
speed: 0.5,
zoom: 4.4,
warpFreq: 1.5,
coreClamp: 0.12,
falloff: 1,
gain: 0.55,
rim: 0.12,
alphaGain: 2.4
},
thinking: {
speed: 0.65,
zoom: 4.6,
warpFreq: 1.6,
coreClamp: 0.11,
falloff: 0.96,
gain: 0.6,
rim: 0.13,
alphaGain: 2.5
},
/*
speaking is SPEED-led, like hydrogen's: the scrollwork keeps the idle
structure but reforms itself several times faster, with hotter knots and
MORE contrast (falloff above 1), so it reads as the same orb answering
at speed. Densifying the pattern here (higher zoom/warpFreq) is what
used to make speaking look like a blurry mesh: more detail per pixel and
a flatter falloff wash the filigree into fog.
warpFreq 1.2 is deliberate: the shader scales it by the input volume,
and speaking synthesizes input around 0.65, so the EFFECTIVE frequency
lands back at the crisp ~1.5 that idle shows at zero input.
*/
speaking: {
speed: 3,
zoom: 4.4,
warpFreq: 1.2,
coreClamp: 0.07,
falloff: 1.1,
gain: 0.85,
rim: 0.2,
alphaGain: 3
}
}
};
export type Shdr02Props = Omit<ShaderOrbProps, "variant">;
export function Shdr02({ size = 280, ...rest }: Shdr02Props) {
return <ShaderOrb variant={shdr02Orb} size={size} {...rest} />;
}
export default Shdr02;
5. components/ui/shdr-03.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-03 — a turbulent belt of light girdling the ball, coloured by its
own distance field.
Ported from a golfed twigl listing:
for(float i,z,d;i++<8e1;o+=(cos(d/.1+vec4(0,2,4,0))+1.)/d*z)
{vec3 p=z*normalize(FC.rgb*2.-r.xyy),
a=normalize(cos(vec3(4,2,0)+t-d*8.));
p.z+=5.,a=a*dot(a,p)-cross(a,p);
for(d=1.;d++<9.;)a+=sin(a*d+t).yzx/d;
z+=d=.05*abs(length(p)-3.)+.04*abs(a.y);}
o=tanh(o/1e4);
What it actually is, decoded:
- THE FIELD IS A SPHERE PLUS A PLANE, added. abs(length(p) - 3) is the
distance to a shell of radius three; abs(a.y) is the distance to the
plane y = 0 of the TURBULENT frame. A sum is small only where both
terms are, so what lights up is the intersection — a great circle
round the ball, dragged out of true by eight octaves of warp. A belt,
not a shell.
- THE SHELL TERM READS THE RAW POINT AND THE PLANE TERM READS THE
WARPED ONE. That asymmetry is the whole composition: the ball stays a
clean sphere while the belt writhes across it. Warp both and the
sphere goes with it.
- THE ROTATION AXIS IS FED BY THE PREVIOUS STEP'S DENSITY. The listing
writes cos(vec3(4,2,0) + t - d*8) where d is left over from the last
iteration, so the axis — and with it the belt's tilt — settles as the
ray closes on the surface and swings away from it out in the open. It
is a one-line feedback loop hiding inside a constant.
- HUE COMES FROM THE DENSITY, NOT FROM DEPTH. cos(d/.1 + ...) bands the
colour along the distance field itself, so the belt is contoured in
rainbow like a topographic map of its own edge — where shdr-18
colours by depth along the ray and shdr-22 by step index.
- The weight is z/d, not 1/d: it brightens what is FAR as well as what
is close to the surface, so the far limb of the belt burns hotter
than the near one.
Port decisions, each one a documented trap or rule in the README:
- THE ORB IS THE OBJECT, and this listing hands it over: the shell it
traces is the ball. All that was needed was the family envelope and
analytic silhouette to bound the residue, sized past the shell so the
belt is not shaved at the limb.
- The golfed rotation, a*dot(a,p) - cross(a,p) with unit a, is an exact
minus-90-degree Rodrigues — orthonormal, against the README's usual
warning about golfed rotations. Same construction as shdr-18.
- The listing relies on d being ZERO on the first iteration, where the
axis reads it before anything has written it. Uninitialised locals are
UNDEFINED in GLSL ES 1.0 — explicit here, and it matters more than
usual because that value steers the geometry rather than a phase.
- No step-length weighting: 1/d is the density, as in shdr-22 and
shdr-18, and multiplying by the step would cancel it exactly. The
clamp bounds it instead.
- Both singular divisors need floors. The step floor is the BELT WIDTH,
which is the only material control here.
- Emitted light, so rgb is already premultiplied and alpha comes from
the peak channel (see shdr-31).
---------------------------------------------------------------------------- */
/*
* Step counts are `#define`s: ES 1.0 requires constant loop bounds. STEPS and
* TURB are the listing's i++ < 8e1 and d++ < 9 (which runs d = 2..9).
*/
const ECLIPTIC_FRAG = `
#define STEPS 80
#define TURB 8
#define AA 1
// Volume-reactive values, resolved once per fragment in main().
float eclipticTurb;
float eclipticPlane;
float eclipticExposure;
float eclipticWidth;
vec3 eclipticRender(vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
vec3 ro = vec3(0.0, 0.0, uP_camDist);
vec3 rd = normalize(vec3(uv, -uP_focal));
float animTime = uP_speed; // integrated clock: the warp
float wander = uP_wander; // integrated clock: the belt's tilt
vec3 acc = vec3(0.0);
// transmittance carried front-to-back — the near belt veils the far one
float T = 1.0;
// march only the span the envelope can light, as in shdr-01
float z = max(uP_camDist - uP_envRadius * 1.3, 0.0);
float zEnd = uP_camDist + uP_envRadius * 1.3;
/*
The listing's feedback variable, explicit. On the first iteration the
axis below reads this before anything has written it — zero is what the
golfed version gets, so zero is what it gets here.
*/
float d = 0.0;
for (int it = 0; it < STEPS; it++) {
vec3 p = ro + rd * z;
/*
The axis, steered by the PREVIOUS step's density: the belt's tilt
settles as the ray closes on the surface and swings away from it out
in the open. The three phases are far enough apart that the cosines
can never null together, so the normalize is safe without a guard.
*/
vec3 axis = normalize(cos(wander + vec3(4.0, 2.0, 0.0) - d * uP_feedback));
// the exact minus-90-degree rotation about that axis
vec3 a = dot(axis, p) * axis - cross(axis, p);
// eight octaves of plain feedback warp — no lattice quantizer here,
// unlike its cousins shdr-22 and shdr-04
for (int j = 0; j < TURB; j++) {
float f = float(j) + 2.0;
a += eclipticTurb * sin(a * f + animTime).yzx / f;
}
/*
Sphere plus plane. The shell reads the RAW point so the ball stays a
ball; the plane reads the WARPED one so the belt writhes across it.
uP_plane at zero drops the belt and lights the whole shell, which is
worth being able to see once.
*/
d = uP_shellW * abs(length(p) - uP_shellR) + eclipticPlane * abs(a.y);
d = max(d, eclipticWidth);
/*
Hue from the DENSITY — the belt contoured in rainbow along its own
distance field — and the listing's z in the numerator, which burns
the far limb hotter than the near one.
*/
vec3 w = cos(d * uP_hue + vec3(0.0, 2.0, 4.0) * uP_spread) + 1.0;
w *= z / d;
w = min(w, vec3(uP_stepClamp));
// envelope: plateau through the ball, cut 12% past the radius so the
// analytic silhouette in main() still has emission left to cut
float env = smoothstep(uP_envRadius * 1.12, uP_envRadius * uP_envCore, length(p));
w = (w + uP_fill) * env;
acc += T * w;
T *= exp(-dot(w, vec3(0.299, 0.587, 0.114)) * uP_scatter);
z += d;
if (T < 0.004 || z > zEnd) break;
}
return acc;
}
void main() {
/*
The SURGE: tightness and warp swept together on one phase, so the belt
gathers into a hard warped girdle and then opens back into a smooth
shell. Two params, one gesture — swept apart they read as two unrelated
things happening at once.
Absolute bounds rather than a swing around what was dialled, so the range
is exactly the range: tightness 0.1 to 0.3, warp 0.5 to 1.6. That is why
the mix sits OUTSIDE the volume terms below — folding the surge under
them would shave the top of both ranges by whatever the agent happened to
be doing. At surge zero those terms are all that is left, so a state that
does not ask for this is untouched.
*/
float surge = 0.5 - 0.5 * cos(uAnim * 4.0);
eclipticTurb = mix(uP_turb * (1.0 + 0.4 * uInput), mix(0.5, 1.6, surge), uP_surge);
// the belt broadens toward a full shell while the agent speaks
eclipticPlane = mix(uP_plane * (1.0 - 0.35 * uOutput), mix(0.1, 0.3, surge), uP_surge);
eclipticExposure = uP_exposure * (1.0 - 0.3 * uOutput);
/*
The belt BREATHES. At pulse zero the width is exactly what was dialled,
so a state that does not ask for this is untouched; at one it sweeps the
whole way from nothing to that width and back, once every few seconds.
It cannot truly reach zero. Belt width is the march's step floor — see
the port note above — and a zero step stalls the ray on one point, which
with no scatter to close the transmittance accumulates the clamp eighty
times into a white flare. The floor is the param's own minimum, four
times finer than what the resting belt uses, so it reads as gone.
Off uAnim rather than a raw clock, so the breath quickens with the agent
like every other motion in the engine.
*/
eclipticWidth = max(uP_width * (1.0 - uP_pulse * (0.5 + 0.5 * cos(uAnim * 3.0))), 5e-4);
vec3 acc = vec3(0.0);
#if AA > 1
for (int mx = 0; mx < AA; mx++) {
for (int my = 0; my < AA; my++) {
vec2 offset = (vec2(float(mx), float(my)) + 0.5) / float(AA) - 0.5;
acc += eclipticRender(gl_FragCoord.xy + offset);
}
}
acc /= float(AA * AA);
#else
acc = eclipticRender(gl_FragCoord.xy);
#endif
// tanh tone map per channel — the envelope and transmittance change the
// accumulator's scale, so the golfed /1e4 knee is a tunable here
vec3 col = tanh3(acc / max(eclipticExposure, 1.0));
col = pow(clamp(col, 0.0, 1.0), vec3(uP_contrast));
// saturation about luminance, then the tint
float lum = dot(col, vec3(0.299, 0.587, 0.114));
col = mix(vec3(lum), col, uP_saturation);
col *= uC_tint;
// alpha from the brightest channel, not luminance — a deep blue contour
// has low luminance but must not go transparent
float peak = max(col.r, max(col.g, col.b));
float a = clamp(peak * uP_alphaGain, 0.0, 1.0);
// Analytic silhouette — identical construction to shdr-01: exact
// ray-to-centre distance against the radius, colour AND alpha.
vec3 mrd = normalize(vec3(orbUV(), -uP_focal));
float closest = length(cross(vec3(0.0, 0.0, uP_camDist), mrd));
float band = mix(0.35, 0.012, clamp(uP_edge, 0.0, 1.0));
float mask = 1.0 - smoothstep(uP_envRadius * (1.0 - band), uP_envRadius * 1.005, closest);
col *= mask;
a *= mask;
// safety taper at the frame boundary — colour as well as alpha
float r2d = length(orbUV());
float fade = 1.0 - smoothstep(uP_edgeFade, 1.0, r2d);
col *= fade;
a *= fade;
// Emitted light, so rgb is already premultiplied — do NOT scale by alpha
// again (see the same note in shdr-31).
gl_FragColor = vec4(col, a);
}
`;
export const shdr03Orb: OrbVariant = {
key: "shdr-03",
label: "SHDR-03",
note: "a turbulent belt of light girdling the ball, contoured in rainbow",
frag: ECLIPTIC_FRAG,
params: [
{ key: "speed", label: "Anim speed", min: 0.015, max: 10, step: 0.05, default: 0.6, integrate: true },
{ key: "wander", label: "Belt tilt drift", min: 0, max: 5, step: 0.02, default: 0.3, integrate: true },
{ key: "feedback", label: "Tilt feedback", min: 0, max: 40, step: 0.1, default: 1.5 },
{ key: "camDist", label: "Camera distance", min: 1, max: 50, step: 0.3, default: 5 },
{ key: "focal", label: "Lens", min: 0.15, max: 15, step: 0.05, default: 1.05 },
{ key: "shellR", label: "Shell radius", min: 0.2, max: 20, step: 0.1, default: 3 },
{ key: "shellW", label: "Shell weight", min: 0.005, max: 1, step: 0.005, default: 0.12 },
{ key: "plane", label: "Belt tightness", min: 0, max: 1, step: 0.005, default: 0.15 },
{ key: "turb", label: "Warp", min: 0, max: 4, step: 0.02, default: 0.55 },
{ key: "width", label: "Belt width", min: 0.0005, max: 0.4, step: 0.0005, default: 0.004 },
{ key: "pulse", label: "Belt breathing", min: 0, max: 1, step: 0.01, default: 0 },
{ key: "surge", label: "Belt surge", min: 0, max: 1, step: 0.01, default: 0 },
{ key: "hue", label: "Contour hue", min: 0, max: 60, step: 0.1, default: 25 },
{ key: "spread", label: "Colour spread", min: 0, max: 3, step: 0.02, default: 1 },
{ key: "envRadius", label: "Envelope radius", min: 0.15, max: 20, step: 0.1, default: 3.3 },
{ key: "envCore", label: "Envelope core", min: 0.3, max: 1.02, step: 0.01, default: 0.92 },
{ key: "fill", label: "Body fill", min: 0, max: 40, step: 0.05, default: 0.1 },
{ key: "stepClamp", label: "Step clamp", min: 5, max: 20000, step: 25, default: 1500 },
{ key: "scatter", label: "Diffusion", min: 0, max: 0.1, step: 0.0002, default: 0.0006 },
{ key: "exposure", label: "Exposure", min: 20, max: 100000, step: 50, default: 1500 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 15, step: 0.05, default: 1.2 },
{ key: "saturation", label: "Saturation", min: 0, max: 4, step: 0.02, default: 1.25 },
{ key: "alphaGain", label: "Alpha gain", min: 0.05, max: 15, step: 0.1, default: 2 },
{ key: "edge", label: "Edge sharpness", min: 0, max: 1, step: 0.01, default: 1 },
{ key: "edgeFade", label: "Halo falloff", min: 0.1, max: 3, step: 0.015, default: 0.98 }
],
colors: [{ key: "tint", label: "Tint", default: "#ffffff" }],
/*
Staged on BELT TIGHTNESS, which is the only control that changes what
the object is: high and the light is a single girdle, low and it opens
out into the whole shell. Warp and belt width carry the rest.
*/
statePresets: {
/*
at rest: a hairline thread on a small, thin shell. The belt is left
loose — a twentieth of the way to thinking's wire — so it is the WARP,
more than double what searching carries and the highest steady value of
the three, that gives the light its shape rather than the plane
confining it. Hue is pushed warm and the
envelope opened to its ceiling, which is what lets so fine a line still
read as a body.
*/
idle: {
speed: 0.6,
wander: 0.3,
feedback: 2,
shellR: 2,
shellW: 0.085,
plane: 0.085,
turb: 1.54,
width: 0.001,
hue: 39.8,
spread: 0.94,
envCore: 1.02,
stepClamp: 1475,
exposure: 1450,
scatter: 0.0006,
alphaGain: 2
},
/*
searching: the belt BREATHES. Width is on `pulse` at full depth, so the
band swells from nothing to fifty times the resting belt and closes
again every few seconds — the state's whole tell, and the reason warp
drops to under half of idle's: the shape comes from the breathing now,
not from the noise.
Under it the belt is also four times tighter than at rest and hunting
hard — the tilt three times as fast, the axis feedback near quadrupled
— on a shell pulled small and thin inside a much wider envelope, so
what pulses is a broad band on a small ball rather than a girdle.
*/
thinking: {
speed: 2,
wander: 1.1,
feedback: 7.5,
focal: 1.9,
shellR: 1.9,
shellW: 0.005,
plane: 0.345,
turb: 0.7,
width: 0.048,
pulse: 1,
hue: 28.3,
spread: 1.02,
envRadius: 7.4,
envCore: 0.84,
exposure: 2300,
scatter: 0,
alphaGain: 2
},
/*
answering: the belt SURGES. Tightness and warp are both on the surge at
full depth, sweeping 0.1 to 0.3 and 0.5 to 1.6 together about every
second and a half — from a loose, lightly warped band to a tight warped
girdle and back. Where thinking pulses one control, this one swings the
two that decide what the object is, which is why it reads as the
loudest of the three.
Tightness starts the sweep at exactly what is dialled here, so the
preset value is the loose end of the swing; warp does not, and its 0.14
is only what you would see with the surge turned off. What the preset
carries either way is the body under them — a broad shell, tilt
drifting near three times idle's, the axis feedback almost off — plus
eighteen times the resting belt width to keep the girdle solid at the
tight end.
*/
speaking: {
speed: 0.9,
wander: 0.84,
feedback: 0.3,
shellR: 1.8,
shellW: 0.19,
plane: 0.1,
turb: 0.14,
width: 0.018,
surge: 1,
spread: 1.04,
envRadius: 3.4,
exposure: 650,
scatter: 0.0003,
alphaGain: 2.7
}
},
// the contour ramp supplies the colour, so the tint only shifts its
// temperature: neutral at rest, cooled while searching, warmed while
// answering
stateColors: {
idle: { tint: "#ffffff" },
thinking: { tint: "#9db8ff" },
speaking: { tint: "#ffc492" }
}
};
export type Shdr03Props = Omit<ShaderOrbProps, "variant">;
export function Shdr03({ size = 280, ...rest }: Shdr03Props) {
return <ShaderOrb variant={shdr03Orb} size={size} {...rest} />;
}
export default Shdr03;
6. components/ui/shdr-04.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-04 — a hollow shell of light, faceted by a voxel lattice, approached
but never reached.
Ported from a golfed twigl listing:
vec3 p;
for(float i,z,f;i++<5e1;z+=f=.003+.1*abs(length(p)-5.),o.rgb+=(p/z+.8)/f)
for(p=z*(FC.rgb*2.-r.xyy)/r.y,p.z+=9.,f=1.;f++<7.;
p+=sin(round(p.zxy/.1)*.1*f-t)/f);
o=tanh(o/2e3);
What it actually is, decoded:
- THE SUBJECT IS A SPHERE, written down as one. abs(length(p) - 5) is
the distance to a shell of radius five, and the march sphere-traces
it. Of all the listings in this library this is the only one that did
not have to be argued onto a ball — it was already one.
- THE MARCH NEVER ARRIVES. The step is a tenth of the distance to the
shell, so the remaining gap falls by 10% per step and after fifty
steps is half a percent of where it started: an asymptotic approach
from outside that never crosses. Every step accumulates 1/step, so
the sum is dominated by the last few and the shell reads as a glowing
surface rather than as anything the ray passes through. The .003 floor
is the only thing bounding that sum, which makes it the SURFACE
WIDTH — the closest this shader has to a material.
- THE LATTICE PITCH IS FIXED ACROSS OCTAVES. round(p.zxy/.1)*.1 snaps
to a tenth-unit grid, and the octave index f multiplies the PHASE,
never the lattice. So all six octaves quantize on the SAME grid and
the displacement is piecewise constant on it — the shell comes out
faceted at one crisp scale instead of fractally rough. That is the
opposite of how shdr-22 and shdr-07 use the same trick, where
each octave gets its own lattice.
- THE COLOUR IS THE POSITION. p/z is the warped point over the distance
travelled, so x, y and z paint red, green and blue and the whole thing
washes toward the .8 white floor as the ray goes deeper. Near facets
are strongly coloured; far ones are white.
Port decisions, each one a documented trap or rule in the README:
- round() is ES 3.0 and does not exist in GLSL ES 1.0 — hand-written as
floor(x + 0.5).
- The march starts at the camera rather than at an envelope bound, which
is the reverse of the choice shdr-22 and orb-nova make. Here the
APPROACH IS THE IMAGE: skipping ahead to the shell would discard the
geometric ramp that builds the glow, and the first samples' large p/z
are what tint the near side.
- The listing leaves its ray direction unnormalized, so its z is not a
distance and its .003 and .1 are in a units system that depends on the
lens. Normalized here, which makes those two numbers mean what they
say and lets camera distance and lens move independently.
- The golfed listing relies on i, z and p starting at zero;
uninitialised locals are UNDEFINED in GLSL ES 1.0, explicit here. p in
particular is read by the step expression BEFORE the inner loop has
ever written it, on the first iteration only.
- Emitted light, so rgb is already premultiplied and alpha comes from
the peak channel (see shdr-31).
---------------------------------------------------------------------------- */
/*
* Step counts are `#define`s: ES 1.0 requires constant loop bounds. STEPS and
* TURB are the listing's i++ < 5e1 and f++ < 7 (which runs f = 2..7).
*/
const GEODE_FRAG = `
#define STEPS 50
#define TURB 6
#define AA 1
// Volume-reactive values, resolved once per fragment in main().
float geodeTurb;
float geodeWidth;
float geodeExposure;
// GLSL ES 1.0 has no round() — it arrived in ES 3.0. The listing quantizes
// with it, so it ships here.
vec3 roundv(vec3 x) { return floor(x + 0.5); }
vec3 geodeRender(vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
vec3 ro = vec3(0.0, 0.0, uP_camDist);
vec3 rd = normalize(vec3(uv, -uP_focal));
float animTime = uP_speed; // integrated clock
float shellR = uP_shellR;
float pitch = max(uP_pitch, 0.002);
// the run-away bound for rays that miss the shell entirely
float zEnd = uP_camDist + shellR * 2.5;
vec3 acc = vec3(0.0);
// The listing starts its march at the camera and lets the trace do the
// travelling — see the header. z is the distance already walked.
float z = 0.0;
for (int it = 0; it < STEPS; it++) {
vec3 p = ro + rd * z;
/*
Six octaves on ONE lattice. The pitch never changes; only the phase
multiplier does, so the displacement is piecewise constant on a
single grid and the shell facets at one scale.
*/
for (int j = 0; j < TURB; j++) {
float f = float(j) + 2.0;
p += geodeTurb * sin(roundv(p.zxy / pitch) * pitch * f - animTime) / f;
}
/*
Sphere trace toward the shell, on the WARPED point — so the facets
are what the ray is chasing, not a smooth ball underneath them. The
slack is the listing's tenth, and the floor is the surface width.
*/
float d = geodeWidth + uP_slack * abs(length(p) - shellR);
z += d;
/*
Position as colour, washing toward white with depth. Guarded on z:
the listing gets away with reading p/z here because its comma
operator advances z first, which is worth knowing before anyone
reorders these two lines.
*/
acc += (p * uP_hueGain / max(z, 1e-3) + uP_floorLevel) / d;
if (z > zEnd) break;
}
return acc;
}
void main() {
geodeTurb = uP_turb * (1.0 + 0.5 * uInput);
geodeWidth = max(uP_width * (1.0 - 0.4 * uOutput), 0.0002);
geodeExposure = uP_exposure * (1.0 - 0.3 * uOutput);
vec3 acc = vec3(0.0);
#if AA > 1
for (int mx = 0; mx < AA; mx++) {
for (int my = 0; my < AA; my++) {
vec2 offset = (vec2(float(mx), float(my)) + 0.5) / float(AA) - 0.5;
acc += geodeRender(gl_FragCoord.xy + offset);
}
}
acc /= float(AA * AA);
#else
acc = geodeRender(gl_FragCoord.xy);
#endif
// tanh tone map per channel — the golfed /2e3 knee is a tunable here
vec3 col = tanh3(acc / max(geodeExposure, 1.0));
col = pow(clamp(col, 0.0, 1.0), vec3(uP_contrast));
// saturation about luminance, then the tint
float lum = dot(col, vec3(0.299, 0.587, 0.114));
col = mix(vec3(lum), col, uP_saturation);
col *= uC_tint;
// alpha from the brightest channel, not luminance — a deep blue facet
// has low luminance but must not go transparent
float peak = max(col.r, max(col.g, col.b));
float a = clamp(peak * uP_alphaGain, 0.0, 1.0);
/*
Analytic silhouette against the shell, widened by uP_envScale because
the turbulence pushes the visible surface OUT past the nominal radius —
cut at the bare radius and the facets would be shaved flat all round
the limb.
*/
vec3 mrd = normalize(vec3(orbUV(), -uP_focal));
float closest = length(cross(vec3(0.0, 0.0, uP_camDist), mrd));
float sil = uP_shellR * uP_envScale;
float band = mix(0.35, 0.012, clamp(uP_edge, 0.0, 1.0));
float mask = 1.0 - smoothstep(sil * (1.0 - band), sil * 1.005, closest);
col *= mask;
a *= mask;
// safety taper at the frame boundary — colour as well as alpha
float r2d = length(orbUV());
float fade = 1.0 - smoothstep(uP_edgeFade, 1.0, r2d);
col *= fade;
a *= fade;
// Emitted light, so rgb is already premultiplied — do NOT scale by alpha
// again (see the same note in shdr-31).
gl_FragColor = vec4(col, a);
}
`;
export const shdr04Orb: OrbVariant = {
key: "shdr-04",
label: "SHDR-04",
note: "a hollow shell of light, faceted by a voxel lattice",
frag: GEODE_FRAG,
params: [
{ key: "speed", label: "Anim speed", min: 0.015, max: 10, step: 0.05, default: 0.6, integrate: true },
{ key: "camDist", label: "Camera distance", min: 1, max: 60, step: 0.3, default: 9 },
{ key: "focal", label: "Lens", min: 0.15, max: 15, step: 0.05, default: 1.35 },
{ key: "shellR", label: "Shell radius", min: 0.3, max: 20, step: 0.1, default: 5 },
{ key: "pitch", label: "Facet size", min: 0.005, max: 1.5, step: 0.005, default: 0.3 },
{ key: "turb", label: "Displacement", min: 0, max: 5, step: 0.02, default: 0.45 },
{ key: "slack", label: "Trace slack", min: 0.01, max: 0.9, step: 0.005, default: 0.1 },
{ key: "width", label: "Surface width", min: 0.0005, max: 0.3, step: 0.0005, default: 0.003 },
{ key: "hueGain", label: "Position hue", min: 0, max: 6, step: 0.02, default: 1 },
{ key: "floorLevel", label: "White floor", min: 0, max: 4, step: 0.02, default: 0.8 },
{ key: "envScale", label: "Silhouette margin", min: 1, max: 2, step: 0.01, default: 1.16 },
{ key: "exposure", label: "Exposure", min: 20, max: 40000, step: 20, default: 3000 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 15, step: 0.1, default: 1.3 },
{ key: "saturation", label: "Saturation", min: 0, max: 4, step: 0.02, default: 1.3 },
{ key: "alphaGain", label: "Alpha gain", min: 0.05, max: 15, step: 0.1, default: 2 },
{ key: "edge", label: "Edge sharpness", min: 0, max: 1, step: 0.01, default: 1 },
{ key: "edgeFade", label: "Halo falloff", min: 0.1, max: 3, step: 0.015, default: 0.98 }
],
colors: [{ key: "tint", label: "Tint", default: "#ffffff" }],
/*
Staged on displacement — how far the lattice pushes the shell out of
round — and on surface width, which is the only material control this
shader has.
FACET SIZE was held still across all three for a reason: it is a
quantizer, and a gliding quantizer pops instead of fading (the same rule
as shdr-14's cell grid and shdr-17's grain). Answering now moves it, 0.3
to 0.22, so the lattice re-snaps through the half second either side of
that state rather than cross-fading. Deliberate — see the note there.
*/
statePresets: {
// at rest: a shallow crust, the surface held thin and bright
idle: {
speed: 0.6,
turb: 0.45,
width: 0.003,
slack: 0.1,
hueGain: 1,
exposure: 3000,
contrast: 1.3
},
/*
searching: the lattice pushes HARD — displacement nearly doubled —
and the surface pulls to under half its idle width, so the facets
read as sharp shifting plates. The knee rises with them: this is the
dim, brittle state.
*/
thinking: {
speed: 1.8,
turb: 0.85,
width: 0.0012,
slack: 0.07,
hueGain: 1.7,
exposure: 4800,
contrast: 1.75
},
/*
answering: plates AND lamp, which the other two never are at once. The
lattice pushes almost as hard as it does while searching — 0.8 against
0.85 — but on ten times the idle surface width instead of a third of
it, so the facets stay sharp while the shell they sit on is wide open
and bright. Fastest of the three, on the widest silhouette margin, and
the most saturated.
Two things here break the file's own rules on purpose. Facet size drops
to 0.22, so the quantizer glides on the way in and out and the lattice
re-snaps rather than fading; see the staging note above. And the tint
goes COOL — cyan, cooler than the searching blue — against the warm
answering tint the colour note below describes.
*/
speaking: {
speed: 2,
pitch: 0.22,
turb: 0.8,
width: 0.032,
slack: 0.165,
hueGain: 0.5,
floorLevel: 0.82,
envScale: 1.67,
exposure: 1220,
contrast: 0.92,
saturation: 1.66
}
},
// the position ramp supplies the colour, so the tint only shifts its
// temperature: neutral at rest and cool for both of the busy states —
// blue while searching, a brighter cyan while answering
stateColors: {
idle: { tint: "#ffffff" },
thinking: { tint: "#9db8ff" },
speaking: { tint: "#94f3ff" }
}
};
export type Shdr04Props = Omit<ShaderOrbProps, "variant">;
export function Shdr04({ size = 280, ...rest }: Shdr04Props) {
return <ShaderOrb variant={shdr04Orb} size={size} {...rest} />;
}
export default Shdr04;
7. components/ui/shdr-05.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-05 — rainbow rings travelling through a lattice of lenses.
Ported from a two-line twigl listing, the shortest in this library:
vec2 p=(FC.xy*2.-r)/r.y*5.;
o=cos(length(tan(p)+p)-t+vec4(0,.7,1,3));
What it actually is, decoded:
- tan(p) IS THE LENS GRID. Componentwise tangent has a pole every PI, so
the plane is cut into a square lattice of cells and the coordinate
diverges at every cell wall. Inside a cell the map is a gentle
distortion; at the wall it throws the sample to infinity.
- ADDING p BACK IS WHAT KEEPS IT LEGIBLE. tan alone would map every cell
onto the whole plane and the cells would be identical; tan(p) + p
offsets each one by its own position, so every cell in the lattice
shows a DIFFERENT part of the ring field. The grid is a lattice of
lenses looking at different places, not a tiling of one image.
- THE PICTURE IS ONE COSINE of the length of that. Concentric rings, at
three channel phases a fraction of a radian apart, travelling on the
clock. Rings inward, colour fringing where the phases separate,
and where a cell wall is approached the rings pile up without limit.
- THERE IS NO TONE MAP. The listing ends on a raw cosine, so everything
below zero is clamped away by the display — half of every period is
hard black, which is what makes the bands read as edges rather than
as a gradient.
Port decisions, each one a documented trap or rule in the README:
- THE ORB IS THE OBJECT: a flat 2D field, so it is sampled through a
stereographic projection of the dome and finished with a fresnel
sheen, never masked out of the plane as a disc. Motion is
projection-safe 2D, as in shdr-08.
- tan() IS SOFTENED, and this is the one change the port could not do
without. sin/cos reaches infinite ring frequency at every cell wall,
which no amount of supersampling resolves — it is not a sampling
problem, it is unbounded bandwidth. sin*cos/(cos*cos + g) equals tan
wherever tan is finite and caps at 1/(2*sqrt(g)) where it is not, so
g sets how tightly the rings may crowd before the wall stops them.
The same softened reciprocal as shdr-26's cell poles and
shdr-09's rainbow fringe.
- Even softened the walls are the busiest thing on the ball, so this orb
supersamples where the smooth-field orbs do not.
- The listing's clamp at zero is kept: it is not an artefact of the
display, it is half the image.
- Surface-lit and mask-bounded, so alpha IS coverage — premultiplied
output, as in shdr-17.
---------------------------------------------------------------------------- */
/*
* AA is a `#define`: ES 1.0 requires constant loop bounds. Three is not
* caution here — the cell walls put the highest spatial frequency in this
* library right where the light is brightest.
*/
const CAUSTIC_FRAG = `
#define AA 3
// Volume-reactive values, resolved once per fragment in main().
float causticSoft;
float causticGain;
float causticSpread;
/*
Softened tangent. Equal to sin/cos wherever cos is not near zero, capped
at 1/(2*sqrt(g)) where it is — see the header for why the raw pole cannot
be supersampled away.
*/
vec2 tanSoft(vec2 x, float g) {
vec2 s = sin(x);
vec2 c = cos(x);
return s * c / (c * c + g);
}
vec3 causticRender(vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
float R = max(uP_radius, 0.001);
// the dome: the front hemisphere of a unit ball, in screen space
vec2 pl = uv / R;
float z = sqrt(max(1.0 - dot(pl, pl), 0.0));
float ring = uP_ring; // integrated clock: the rings travel
// stereographic wrap of the unrotated dome, as in shdr-08 — the lens
// lattice compresses toward the limb the way a texture on a sphere does
vec2 p = pl / (z + 1.0 + uP_bulge) * uP_scale;
// projection-safe 2D motion: the lattice turns and slides
float sw = uP_swirl; // integrated clock
p = mat2(cos(sw), -sin(sw), sin(sw), cos(sw)) * p;
p += vec2(uP_slide, uP_slide * 0.6); // integrated clock
/*
The lens lattice. Adding p back to its own tangent is what gives every
cell a different view instead of tiling one image — see the header.
*/
float L = length(tanSoft(p, causticSoft) * uP_lens + p);
/*
One cosine, three phases. The listing's (0, .7, 1) sit well under a
radian apart, so the channels overlap through most of a band and only
separate at its shoulders — white cores with coloured edges, not three
independent rainbows.
*/
vec3 col = cos(L * uP_freq - ring + vec3(0.0, 0.7, 1.0) * causticSpread);
// the listing's clamp: half of every period is hard black, and that is
// what makes these read as bands rather than as a gradient
col = max(col, vec3(0.0)) * causticGain;
col = pow(col, vec3(uP_contrast));
float lum = dot(col, vec3(0.299, 0.587, 0.114));
col = mix(vec3(lum), col, uP_saturation);
col *= uC_tint;
// a dark body under the bands, so the black half of the cosine reads as
// the ball rather than as a hole in it
col += uC_body * uP_floorLevel;
// dome shading keeps the ball a ball under the lattice
vec3 n = vec3(pl, z);
float lambert = clamp(dot(n, normalize(vec3(-0.45, 0.55, 0.72))), 0.0, 1.0);
col *= 0.6 + uP_light * lambert;
float fres = 1.0 - z;
fres = fres * fres * fres;
col += uC_sheen * uP_rim * fres;
return col;
}
void main() {
// Volume coupling: the user's voice lets the walls crowd tighter, the
// agent's brightens the bands and opens the colour split.
causticSoft = max(uP_poleSoft * (1.0 - 0.5 * uInput), 0.0008);
causticGain = uP_gain * (0.85 + 0.45 * uOutput);
causticSpread = uP_spread * (1.0 + 0.5 * uOutput);
vec2 uv = orbUV();
float mask = smoothstep(0.012, -0.012, length(uv) - max(uP_radius, 0.001));
if (mask <= 0.0) {
gl_FragColor = vec4(0.0);
return;
}
vec3 col = vec3(0.0);
#if AA > 1
for (int mx = 0; mx < AA; mx++) {
for (int my = 0; my < AA; my++) {
vec2 off = (vec2(float(mx), float(my)) + 0.5) / float(AA) - 0.5;
col += causticRender(gl_FragCoord.xy + off);
}
}
col /= float(AA * AA);
#else
col = causticRender(gl_FragCoord.xy);
#endif
// Surface orb bounded by a mask: alpha IS coverage, so premultiply — the
// opposite convention from the emissive orbs (see shdr-31).
float a = mask;
gl_FragColor = vec4(max(col, vec3(0.0)) * a, a);
}
`;
export const shdr05Orb: OrbVariant = {
key: "shdr-05",
label: "SHDR-05",
note: "rainbow rings travelling through a lattice of lenses",
frag: CAUSTIC_FRAG,
params: [
{ key: "ring", label: "Ring speed", min: 0, max: 8, step: 0.03, default: 0.9, integrate: true },
{ key: "swirl", label: "Swirl", min: 0, max: 3, step: 0.015, default: 0.05, integrate: true },
{ key: "slide", label: "Lattice slide", min: 0, max: 4, step: 0.02, default: 0.12, integrate: true },
{ key: "radius", label: "Radius", min: 0.15, max: 3, step: 0.015, default: 0.9 },
{ key: "scale", label: "Lattice scale", min: 0.3, max: 20, step: 0.1, default: 5 },
{ key: "bulge", label: "Dome bulge", min: 0, max: 4, step: 0.02, default: 0.3 },
{ key: "lens", label: "Lens strength", min: 0, max: 4, step: 0.02, default: 1 },
{ key: "poleSoft", label: "Wall softness", min: 0.0008, max: 0.5, step: 0.0008, default: 0.02 },
{ key: "freq", label: "Ring frequency", min: 0.05, max: 8, step: 0.05, default: 1 },
{ key: "spread", label: "Colour split", min: 0, max: 4, step: 0.02, default: 1 },
{ key: "gain", label: "Brightness", min: 0.05, max: 4, step: 0.02, default: 1.1 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 6, step: 0.05, default: 1 },
{ key: "saturation", label: "Saturation", min: 0, max: 4, step: 0.02, default: 1.15 },
{ key: "floorLevel", label: "Body fill", min: 0, max: 2, step: 0.01, default: 0.12 },
{ key: "light", label: "Key light", min: 0, max: 3, step: 0.015, default: 0.4 },
{ key: "rim", label: "Rim sheen", min: 0, max: 3, step: 0.015, default: 0.45 }
],
colors: [
{ key: "tint", label: "Tint", default: "#ffffff" },
{ key: "body", label: "Body", default: "#141a30" },
{ key: "sheen", label: "Sheen", default: "#bcd8ff" }
],
/*
Staged on WALL SOFTNESS, which decides how tightly the rings are
allowed to crowd before a cell wall stops them, and on ring frequency,
which decides how many bands are on the ball at all. Lattice scale
never moves between states — it sets the cell count, and a gliding cell
count reads as the ball inflating rather than as a change of mood.
Dome bulge and lens strength are staged too, so the SHAPE moves with the
mood as well as the lattice: resting flattens both, and the two busy
states drive them up — answering hardest, which puts bulge at 0 through
0.36 to 0.96 across the three. They are continuous geometry rather than
a quantizer, so gliding them is safe.
*/
statePresets: {
/*
at rest: a plain sphere of drifting bands. Dome bulge is at zero and
the lens down to under half its default — the only state that flattens
both — so the ball is read straight rather than through a lens, which
is what lets resting look settled even though the rings never stop
moving.
The walls are the TIGHTEST of the three here, under a third of
searching's and a fifth of answering's, so the rings crowd hard
against them; the colour split narrows to 0.7 with the key light
raised well over its default to put back the separation the narrower
split gives away.
*/
idle: {
ring: 0.9,
swirl: 0.195,
slide: 0.26,
bulge: 0,
lens: 0.38,
poleSoft: 0.012,
freq: 1.05,
spread: 0.7,
gain: 1.1,
contrast: 1,
saturation: 1.14,
light: 0.66
},
/*
searching: the ball comes UP. Where resting is flat and read straight,
this bulges the dome and drives the lens past one, so the bands are
magnified through the middle — and the drift roughly triples, swirl
and lattice slide together, on a ring clock three times as fast.
It is also the hardest-looking of the three by some way: contrast more
than triples over resting and saturation doubles, on a body fill three
times as deep and with the rim sheen switched off entirely, so nothing
softens the edge. The walls open to three times resting's, which stops
the rings being hairlines — this state reads through colour and shape
now, not through fineness.
*/
thinking: {
ring: 3,
swirl: 0.57,
slide: 0.88,
bulge: 0.36,
lens: 1.28,
poleSoft: 0.042,
freq: 1.3,
spread: 1.82,
gain: 0.88,
contrast: 3.2,
saturation: 2.36,
floorLevel: 0.4,
rim: 0
},
/*
answering: the FINEST banding of the three by a long way — ring
frequency near five times resting's and nearly four times searching's
— laid over the most strongly domed ball, bulge pushed almost to one
against resting's flat zero. Many tight bands, spread across a surface
curving away from you.
The walls open to five times resting's, which is what keeps banding
that fine from crowding into a solid field, and the drift is the
highest of the three on both controls. Colour split comes back to
about where resting holds it, so it is the fineness that carries this
state rather than the split.
*/
speaking: {
ring: 1.4,
swirl: 0.6,
slide: 1,
bulge: 0.96,
lens: 1.2,
poleSoft: 0.064,
freq: 4.8,
spread: 0.68,
gain: 1.6,
contrast: 0.75
}
},
// the ring phases supply the colour, so the tint only shifts temperature
// and the body carries the mood: neutral at rest, cold while searching,
// warm while answering
stateColors: {
idle: { tint: "#ffffff", body: "#141a30", sheen: "#bcd8ff" },
thinking: { tint: "#a6c0ff", body: "#080c26", sheen: "#7ea9ff" },
speaking: { tint: "#ffc492", body: "#2e1408", sheen: "#ffb277" }
}
};
export type Shdr05Props = Omit<ShaderOrbProps, "variant">;
export function Shdr05({ size = 280, ...rest }: Shdr05Props) {
return <ShaderOrb variant={shdr05Orb} size={size} {...rest} />;
}
export default Shdr05;
8. components/ui/shdr-06.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-06 — a hundred glowing lattices stacked through the depth of the ball,
interfering.
Ported from a one-line twigl listing:
vec2 p=(FC.xy*2.-r)/r.y/.3;
for(float f;f++<1e2;p+=.02*sin(p.yx+.5*t))
o+=(cos(f/27.+vec4(0,1,3,0))+1.1)/length(sin(p*sin(r+f)/.7));
o=tanh(o*o/4e4);
What it actually is, decoded:
- Each iteration draws ONE LATTICE OF GLOWS. sin(p*k) hits zero wherever
both components do, which is a rectangular grid of points, and
1/length of it lights every one of them. A hundred iterations lay a
hundred such grids over each other, and the interference between
their spacings is the whole image — moire in the strict sense.
- sin(r+f) is the SEED. r is the resolution and f the layer index, so
each layer gets its own frequency pair out of a sine of an integer —
the cheapest pseudo-random vector in shader golf. It is also why the
grids are axis-aligned but never the same size twice.
- The loop's increment, p += .02*sin(p.yx+.5t), walks the sample point a
little between layers, so the stack is not a hundred concentric grids
but a hundred grids each shifted a little further along a wandering
path. That is what turns the interference from static moire into
something that flows.
- cos(f/27 + vec4(0,1,3,0)) + 1.1 tints by LAYER INDEX, so depth through
the stack reads as hue.
- o*o before the knee is a contrast squarer, not a tone map: it crushes
the field between the glows and lets the glows themselves saturate.
Port decisions, each one a documented trap or rule in the README:
- THE ORB IS THE OBJECT: the hundred layers become a hundred DEPTHS
inside the ball rather than a hundred copies of one texture. Layer at
depth d is the stereographic projection of the point the view ray has
reached there, closed form —
st = pl / (z - d + (1+bulge)*|P|), |P|^2 = 1 - 2*d*z + d*d.
Be precise about what that is: a depth-graded REPROJECTION, not
refraction. It grades radially, identical for every layer at the dead
centre and spread hardest at the limb, which is why the layers all
agree in the middle of the ball and light it as a focal point. What
it buys is the one thing a flat texture cannot have — layers that
shear against each other as they drift instead of sliding as one
sheet.
- The listing's sin(r+f) seed is RESOLUTION DEPENDENT and, unlike the
lattice term in shdr-26, nothing cancels it: every layer would be
reseeded on a canvas resize. Replaced with a constant.
- Choosing that constant is not arbitrary. The two components differ
only by a fixed phase, so the frequency pair traces a closed curve in
frequency space rather than filling it — inherent to the listing. A
quarter-turn offset makes that curve a CIRCLE, so every lattice
aspect gets equal time; an arbitrary offset collapses it toward a
line and half the layers come out near-identical.
- length(sin(...)) is exactly zero at every lattice point, which is the
one place the listing wants to divide. Floored rather than guarded,
and the floor doubles as the GLOW SIZE — the same trick as
shdr-02's coreClamp and shdr-09's ring width.
- The golfed listing relies on f starting at zero; uninitialised locals
are UNDEFINED in GLSL ES 1.0, explicit here.
- Surface-lit and mask-bounded, so alpha IS coverage — premultiplied
output, as in shdr-17.
---------------------------------------------------------------------------- */
/*
* Step counts are `#define`s: ES 1.0 requires constant loop bounds. LAYERS is
* the listing's f++ < 1e2. The field is broad and smooth — each lattice puts
* only a handful of zeros across the ball — so this one does not need
* supersampling the way the thin-line orbs do.
*/
const MOIRE_FRAG = `
#define LAYERS 100
#define AA 1
/*
* The listing's sin(r + f), minus the resolution. The components are a
* quarter turn apart so the per-layer frequency pair walks a circle (see
* the header) — the base value only sets where on that circle layer one
* starts.
*/
const vec2 SEED = vec2(11.3, 12.87);
// Volume-reactive values, resolved once per fragment in main().
float moireDrift;
float moireGlow;
float moireHue;
vec3 moireRender(vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
float R = max(uP_radius, 0.001);
// the dome: the front hemisphere of a unit ball, in screen space
vec2 pl = uv / R;
float z = sqrt(max(1.0 - dot(pl, pl), 0.0));
float t = uP_speed; // integrated clock
/*
The layer-zero projection: the plain stereographic wrap of the dome's
surface. Every deeper layer is this divided by its own denominator, so
the whole parallax below costs one ratio per layer.
*/
float bulge = 1.0 + uP_bulge;
float den0 = z + bulge;
vec2 w = pl / den0 * uP_scale;
vec3 acc = vec3(0.0);
for (int li = 0; li < LAYERS; li++) {
float f = float(li) + 1.0;
float u = f / float(LAYERS);
/*
This layer's depth along the view ray, and the projection from the
point the ray has reached there. At depth 0 the denominator is den0
and the ratio is 1; deeper layers see the pattern from further
inside the ball, which spreads them at the limb and not at all
through the centre. That gradient is what shears the stack.
*/
float d = u * uP_depth;
float denu = z - d + bulge * sqrt(max(1.0 - 2.0 * d * z + d * d, 0.0));
vec2 q = w * (den0 / max(denu, 0.05));
/*
One lattice. sin(q * k) vanishes on a rectangular grid of points and
the reciprocal of its length lights every one; the floor on that
length is the glow's radius, and without it the divide is by exactly
zero at every lattice point.
*/
vec2 k = sin(SEED + f) / max(uP_freq, 0.001);
float g = max(length(sin(q * k)), moireGlow);
// tint by layer index — depth through the stack reads as hue
vec3 hue = cos(f * moireHue + vec3(0.0, 1.0, 3.0)) + 1.1;
acc += hue / g;
// the listing's walk between layers: the stack is a hundred grids
// each shifted a little further along a wandering path
w += moireDrift * sin(w.yx + t);
}
/*
The listing's knee is tanh(o*o/4e4) over an unnormalized sum of a
hundred layers. Dividing by the layer count first pulls the square's
scale down by 100*100, so the same knee is exactly 4 here — a number
that fits on a slider. The square is a contrast squarer, not a tone
map: it crushes the field between the glows.
*/
vec3 v = acc / float(LAYERS);
vec3 col = tanh3(v * v / max(uP_exposure, 0.0001));
col = pow(clamp(col, 0.0, 1.0), vec3(uP_contrast));
// saturation about luminance, then the tint
float lum = dot(col, vec3(0.299, 0.587, 0.114));
col = mix(vec3(lum), col, uP_saturation);
col *= uC_tint;
/*
Dome shading, kept gentle: the layers are emission seen THROUGH the
ball, so a hard lambert reads as a shadow thrown across the inside of
a lamp rather than as a lit surface.
*/
vec3 n = vec3(pl, z);
float lambert = clamp(dot(n, normalize(vec3(-0.45, 0.55, 0.72))), 0.0, 1.0);
col *= 0.6 + uP_light * lambert;
float fres = 1.0 - z;
fres = fres * fres * fres;
col += uC_sheen * uP_rim * fres;
return col;
}
void main() {
// Volume coupling: the user's voice widens the walk between layers, the
// agent's opens the glows and runs the hue through the stack faster.
moireDrift = uP_drift * (1.0 + 0.6 * uInput);
moireGlow = max(uP_glowSize * (1.0 - 0.3 * uOutput), 0.002);
moireHue = uP_hueRate * (1.0 + 0.35 * uOutput);
vec2 uv = orbUV();
float mask = smoothstep(0.012, -0.012, length(uv) - max(uP_radius, 0.001));
// A hundred lattices per sample, none of them worth paying for outside
// the silhouette.
if (mask <= 0.0) {
gl_FragColor = vec4(0.0);
return;
}
vec3 col = vec3(0.0);
#if AA > 1
for (int mx = 0; mx < AA; mx++) {
for (int my = 0; my < AA; my++) {
vec2 off = (vec2(float(mx), float(my)) + 0.5) / float(AA) - 0.5;
col += moireRender(gl_FragCoord.xy + off);
}
}
col /= float(AA * AA);
#else
col = moireRender(gl_FragCoord.xy);
#endif
// Surface orb bounded by a mask: alpha IS coverage, so premultiply — the
// opposite convention from the emissive orbs (see shdr-31).
float a = mask;
gl_FragColor = vec4(max(col, vec3(0.0)) * a, a);
}
`;
export const shdr06Orb: OrbVariant = {
key: "shdr-06",
label: "SHDR-06",
note: "a hundred glowing lattices stacked through the ball, interfering",
frag: MOIRE_FRAG,
params: [
{ key: "speed", label: "Anim speed", min: 0.015, max: 10, step: 0.05, default: 0.5, integrate: true },
{ key: "drift", label: "Layer walk", min: 0, max: 0.3, step: 0.002, default: 0.02 },
{ key: "radius", label: "Radius", min: 0.15, max: 3, step: 0.015, default: 0.9 },
{ key: "scale", label: "Pattern scale", min: 0.3, max: 20, step: 0.1, default: 4 },
{ key: "bulge", label: "Dome bulge", min: 0, max: 4, step: 0.02, default: 0.3 },
{ key: "depth", label: "Stack depth", min: 0, max: 1.6, step: 0.01, default: 0.7 },
{ key: "freq", label: "Lattice spacing", min: 0.05, max: 5, step: 0.01, default: 0.7 },
{ key: "glowSize", label: "Glow size", min: 0.002, max: 1, step: 0.002, default: 0.05 },
{ key: "hueRate", label: "Hue per layer", min: 0, max: 0.5, step: 0.002, default: 0.037 },
{ key: "exposure", label: "Exposure", min: 0.05, max: 200, step: 0.05, default: 4 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 10, step: 0.05, default: 1.1 },
{ key: "saturation", label: "Saturation", min: 0, max: 4, step: 0.02, default: 1.2 },
{ key: "light", label: "Key light", min: 0, max: 3, step: 0.015, default: 0.45 },
{ key: "rim", label: "Rim sheen", min: 0, max: 3, step: 0.015, default: 0.4 }
],
colors: [
{ key: "tint", label: "Tint", default: "#ffffff" },
{ key: "sheen", label: "Sheen", default: "#bcd8ff" }
],
/*
Staged on the two controls that decide what the interference looks
like: the WALK between layers, which sets how far the stack shears
against itself, and the GLOW SIZE, which sets whether the lattice
points read as pinpricks or as flooded light.
The clock is staged hard alongside them, and the two are doing separate
jobs: the walk is the AMPLITUDE of the shear and the clock is its RATE,
so a state's character comes from the pair. Small walk on a fast clock
twitches; a large walk on a fast clock churns.
Lattice spacing never moves between states — it sets how many grids land
on the ball, and a gliding count reads as the ball inflating rather than
as a mood.
*/
statePresets: {
// at rest: a slow shallow walk, glows open and soft
idle: {
speed: 0.5,
drift: 0.02,
depth: 0.7,
glowSize: 0.05,
hueRate: 0.037,
exposure: 4,
contrast: 1.1
},
/*
searching: quick and granular. The clock runs over five times resting
speed and the walk is five times as wide, on the deepest stack of the
three, so the hundred layers shear hard and visibly against each
other. The glows stay small — a third of resting — which is what keeps
the movement legible as movement: at this rate, fine points shifting
read as a scan across the ball rather than as a wash.
The DOME is pushed out hard too — bulge seven times its default, and
the only state that moves it. That deepens the denominator the whole
projection divides by, so the lattice lands finer on the ball and
spreads less toward the limb: the grain tightens and evens out at the
same time.
*/
thinking: {
speed: 2.6,
drift: 0.105,
bulge: 2.18,
depth: 1.25,
glowSize: 0.016,
hueRate: 0.095,
exposure: 6.5,
contrast: 1.55
},
/*
answering: the fastest clock of the three and the widest walk, but on
the SHALLOWEST stack — a third of resting's — so the hundred layers
barely disagree and travel nearly as one. That is what keeps it
readable at this rate: a coherent lattice walked hard, rather than a
hundred of them shearing apart into grain the way searching does.
The glows stay small, only a little over resting, so the lattice keeps
its points instead of flooding. What carries the state instead is the
KEY LIGHT, up to two and a half times its default and the only state
that touches it — enough dome shading that the flow reads as crossing
a lit ball rather than as a flat lamp changing pattern.
*/
speaking: {
speed: 4.5,
drift: 0.13,
depth: 0.26,
glowSize: 0.062,
hueRate: 0.045,
exposure: 2.7,
contrast: 0.9,
light: 1.11
}
},
// the layer ramp supplies its own rainbow, so the tint only shifts its
// temperature: neutral at rest, cooled while searching, warmed while
// answering
stateColors: {
idle: { tint: "#ffffff", sheen: "#bcd8ff" },
thinking: { tint: "#9db8ff", sheen: "#7ea9ff" },
speaking: { tint: "#ffc492", sheen: "#ffb277" }
}
};
export type Shdr06Props = Omit<ShaderOrbProps, "variant">;
export function Shdr06({ size = 280, ...rest }: Shdr06Props) {
return <ShaderOrb variant={shdr06Orb} size={size} {...rest} />;
}
export default Shdr06;
9. components/ui/shdr-07.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-07 — a twist wave travelling out through the ball, wound around a
lit polar column.
Ported from a golfed twigl listing:
for(float i,z,d,h;i++<5e1;o+=vec4(3,z,i,1)/d)
{vec3 p=z*normalize(FC.rgb*2.-r.xyy),a;a.y++;p.z+=7.;
a=mix(dot(a,p)*a,p,sin(h=length(p)-t))+cos(h)*cross(a,p);
for(d=0.;d++<9.;a+=sin(round(a*d)-t).zxy/d);z+=d=.1*length(a.xz);}
o=tanh(o/1e4);
A near relative of shdr-22 — same march skeleton, same colour-coded
accumulator, same cell-quantized turbulence. Three things make it a
different animal.
- THE ROTATION IS EXACT, AND ITS ANGLE VARIES. Written out,
mix(dot(a,p)*a, p, sin h) + cos(h)*cross(a,p) is
a(a.p)(1 - sin h) + p sin h + (a x p) cos h, which is Rodrigues with
cos(theta) = sin(h) and sin(theta) = cos(h) — a true orthonormal
rotation about the unit axis by theta = 90 degrees - h. Worth saying
plainly, because the README's standing warning is that golfed
rotations are NOT orthonormal and breathe the silhouette. This one is
the exception, twice over: shdr-22 hid an exact 90-degree case,
and this listing hides the general one.
- h = length(p) - t IS THE WHOLE EFFECT. The rotation angle is the
sample's RADIUS minus the clock, so every spherical shell is wound by
a different amount and the winding travels outward with time. Torsion
in the literal sense: twist per unit radius. It also means the field
is organised into shells concentric with the ball before anything
else touches it — this listing is an orb already.
- THE DENSITY IS AXIAL. length(a.xz) is the distance from the twist
axis, not from the origin, so the step collapses along the pole and
the axis lights as a column running through the ball. shdr-22
multiplies two radial lengths and gets streaks; this gets a spine.
The axis is a fixed vertical (a.y++ on a zeroed vec3), where vectors
wanders its axis with time. Fixed is right here: the column has to stay
somewhere for the twist to be read against.
Port decisions, each one a documented trap or rule in the README:
- round() is ES 3.0 and does not exist in GLSL ES 1.0 — hand-written as
floor(x + 0.5).
- The golfed listing builds its axis out of an uninitialised vec3
(a.y++). Uninitialised locals are UNDEFINED in ES 1.0, explicit here.
- The listing drives the travelling wave and the cell flicker off ONE
clock. They are separate motions — one is a wave crossing the ball,
the other is lattice shimmer — so they get separate integrated clocks
and can be tuned against each other.
- The axis leans by rotating the SAMPLE into the axis frame rather than
the axis into the world, so the density term can go on reading the
perpendicular plane as a.xz and the mechanism stays the listing's.
- p.z += 7 puts the camera outside already — re-derived as the family's
ro/rd pair so distance and lens are separate knobs.
- 1/d spikes where the turbulent point lands on the axis — clamped per
step, as in every accumulator here.
- Emitted light, so rgb is already premultiplied and alpha comes from
the peak channel (see shdr-31).
---------------------------------------------------------------------------- */
/*
* Step counts are `#define`s: ES 1.0 requires constant loop bounds. STEPS and
* TURB are the listing's i++ < 5e1 and d++ < 9.
*/
const TORSION_FRAG = `
#define STEPS 50
#define TURB 9
#define AA 1
// Volume-reactive values, resolved once per fragment in main().
float torsionTurb;
float torsionTwist;
float torsionExposure;
// GLSL ES 1.0 has no round() — it arrived in ES 3.0. The listing quantizes
// with it, so it ships here. Halves round up rather than to even, which is
// what a lattice quantizer wants anyway.
vec3 roundv(vec3 x) { return floor(x + 0.5); }
vec3 torsionRender(vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
vec3 ro = vec3(0.0, 0.0, uP_camDist);
vec3 rd = normalize(vec3(uv, -uP_focal));
float shimmer = uP_speed; // integrated clock: cell flicker
float wave = uP_wave; // integrated clock: the travelling twist
float spin = uP_spin; // integrated clock: roll about the axis
// the axis lean and the roll, applied to the SAMPLE rather than to the
// axis — see the header
float ct = cos(uP_tilt);
float st = sin(uP_tilt);
float cs = cos(spin);
float ss = sin(spin);
// the twist axis, unit by construction, which is what makes the
// Rodrigues rotation below exact
vec3 axis = vec3(0.0, 1.0, 0.0);
vec3 acc = vec3(0.0);
// transmittance carried front-to-back — near shells veil far ones
float T = 1.0;
// march only the span the envelope can light, as in shdr-01
float z = max(uP_camDist - uP_envRadius * 1.3, 0.0);
float zEnd = uP_camDist + uP_envRadius * 1.3;
for (int it = 0; it < STEPS; it++) {
vec3 world = ro + rd * z;
// into the axis frame: lean about X, then roll about Y
vec3 p = vec3(world.x, world.y * ct + world.z * st, -world.y * st + world.z * ct);
p = vec3(p.x * cs - p.z * ss, p.y, p.x * ss + p.z * cs);
/*
The travelling twist. h is the sample's radius (scaled by the twist
knob) minus the wave clock, and the line below is an exact rotation
about the axis by 90 degrees - h. Because h depends only on radius,
the winding is constant on spheres: the ball's own shells are the
structure, and raising uP_twist puts more turns between the core and
the surface.
*/
float h = length(p) * torsionTwist - wave;
vec3 a = mix(dot(axis, p) * axis, p, sin(h)) + cos(h) * cross(axis, p);
// cell-quantized turbulence: every lattice cell flickers on its own
// phase, the same construction shdr-22 uses
for (int j = 0; j < TURB; j++) {
float dj = float(j) + 1.0;
a += torsionTurb * sin(roundv(a * dj) - shimmer).zxy / dj;
}
/*
The axial density. At uP_column 0 this is the listing's
length(a.xz) — distance from the twist axis, so the step collapses
along the pole and the axis burns as a column. At 1 it is the plain
radial length and the column dissolves into shells.
*/
float d = uP_stepScale * mix(length(a.xz), length(a), uP_column);
d = max(d, uP_envRadius * 0.003);
/*
The march's own colour code, from the listing: red constant, green
by DEPTH into the ball, blue by STEP INDEX — the opposite assignment
from shdr-22, and the reason this orb runs cyan-blue where that
one runs red-green. Blue gets twice the clamp headroom: its ramp
runs to STEPS (50) where red is fixed at 3, and an equal clamp would
crush the step gradient first.
*/
vec3 w = vec3(3.0, (z - uP_camDist + uP_envRadius) * uP_hueDepth, float(it) * uP_hueStep) / d;
w = min(w, vec3(uP_stepClamp) * vec3(1.0, 1.0, 2.0));
/*
Normalize the clamped weight back to family units, exactly as in
shdr-22: without this line the clamp value leaks into total
energy and Exposure, Body fill and Diffusion all change meaning
whenever the clamp moves.
*/
w *= 20.0 / max(uP_stepClamp, 1.0);
// envelope: plateau through the ball, cut 12% past the radius so the
// analytic silhouette in main() still has emission left to cut
float env = smoothstep(uP_envRadius * 1.12, uP_envRadius * uP_envCore, length(world));
w = (w + uP_fill) * env;
acc += T * w;
T *= exp(-dot(w, vec3(0.299, 0.587, 0.114)) * uP_scatter);
z += d;
if (T < 0.004 || z > zEnd) break;
}
return acc;
}
void main() {
torsionTurb = uP_turb * (1.0 + 0.5 * uInput);
torsionExposure = uP_exposure * (1.0 - 0.35 * uOutput);
// the ball winds tighter while the agent speaks
torsionTwist = uP_twist * (1.0 + 0.4 * uOutput);
vec3 acc = vec3(0.0);
#if AA > 1
for (int mx = 0; mx < AA; mx++) {
for (int my = 0; my < AA; my++) {
vec2 offset = (vec2(float(mx), float(my)) + 0.5) / float(AA) - 0.5;
acc += torsionRender(gl_FragCoord.xy + offset);
}
}
acc /= float(AA * AA);
#else
acc = torsionRender(gl_FragCoord.xy);
#endif
// tanh tone map per channel — the envelope and transmittance change the
// accumulator's scale, so the golfed /1e4 knee is a tunable here
vec3 col = tanh3(acc / max(torsionExposure, 1.0));
col = pow(clamp(col, 0.0, 1.0), vec3(uP_contrast));
// saturation about luminance, then the tint
float lum = dot(col, vec3(0.299, 0.587, 0.114));
col = mix(vec3(lum), col, uP_saturation);
col *= uC_tint;
// alpha from the brightest channel, not luminance — a deep blue tail
// has low luminance but must not go transparent
float peak = max(col.r, max(col.g, col.b));
float a = clamp(peak * uP_alphaGain, 0.0, 1.0);
// Analytic silhouette — identical construction to shdr-01: exact
// ray-to-centre distance against the radius, colour AND alpha.
vec3 mrd = normalize(vec3(orbUV(), -uP_focal));
float closest = length(cross(vec3(0.0, 0.0, uP_camDist), mrd));
float band = mix(0.35, 0.012, clamp(uP_edge, 0.0, 1.0));
float mask = 1.0 - smoothstep(uP_envRadius * (1.0 - band), uP_envRadius * 1.005, closest);
col *= mask;
a *= mask;
// safety taper at the frame boundary — colour as well as alpha
float r2d = length(orbUV());
float fade = 1.0 - smoothstep(uP_edgeFade, 1.0, r2d);
col *= fade;
a *= fade;
// Emitted light, so rgb is already premultiplied — do NOT scale by alpha
// again (see the same note in shdr-31).
gl_FragColor = vec4(col, a);
}
`;
export const shdr07Orb: OrbVariant = {
key: "shdr-07",
label: "SHDR-07",
note: "a twist wave travelling out through the ball around a lit column",
frag: TORSION_FRAG,
params: [
{ key: "speed", label: "Cell shimmer", min: 0.015, max: 10, step: 0.05, default: 0.5, integrate: true },
{ key: "wave", label: "Wave speed", min: 0, max: 8, step: 0.03, default: 0.7, integrate: true },
{ key: "twist", label: "Twist", min: 0, max: 8, step: 0.02, default: 1 },
{ key: "spin", label: "Roll", min: 0, max: 3, step: 0.015, default: 0.1, integrate: true },
{ key: "tilt", label: "Axis lean", min: -1.5, max: 1.5, step: 0.015, default: 0.3 },
{ key: "camDist", label: "Camera distance", min: 1, max: 50, step: 0.3, default: 7 },
{ key: "focal", label: "Lens", min: 0.15, max: 15, step: 0.1, default: 2.25 },
{ key: "turb", label: "Cell turbulence", min: 0, max: 5, step: 0.03, default: 1 },
{ key: "column", label: "Column release", min: 0, max: 1, step: 0.01, default: 0 },
{ key: "stepScale", label: "Step scale", min: 0.005, max: 1.5, step: 0.005, default: 0.1 },
{ key: "hueDepth", label: "Depth hue", min: 0, max: 10, step: 0.03, default: 0.75 },
{ key: "hueStep", label: "Step hue", min: 0, max: 10, step: 0.03, default: 0.45 },
{ key: "envRadius", label: "Envelope radius", min: 0.15, max: 15, step: 0.1, default: 2.6 },
{ key: "envCore", label: "Envelope core", min: 0.3, max: 1.02, step: 0.01, default: 0.88 },
{ key: "fill", label: "Body fill", min: 0, max: 100, step: 0.3, default: 0.15 },
{ key: "stepClamp", label: "Step clamp", min: 3, max: 5000, step: 10, default: 400 },
{ key: "scatter", label: "Diffusion", min: 0, max: 0.5, step: 0.003, default: 0.01 },
{ key: "exposure", label: "Exposure", min: 1.5, max: 5000, step: 5, default: 60 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 15, step: 0.1, default: 1.3 },
{ key: "saturation", label: "Saturation", min: 0, max: 4, step: 0.02, default: 1.15 },
{ key: "alphaGain", label: "Alpha gain", min: 0.05, max: 15, step: 0.1, default: 2 },
{ key: "edge", label: "Edge sharpness", min: 0, max: 1, step: 0.01, default: 1 },
{ key: "edgeFade", label: "Halo falloff", min: 0.1, max: 3, step: 0.015, default: 0.98 }
],
colors: [{ key: "tint", label: "Tint", default: "#ffffff" }],
/*
Staged on the twist, which is the orb's whole subject: how many turns
of winding sit between the core and the surface. The wave clock is the
second lever — how fast that winding travels out through the shells —
and both are phase-safe (twist is a scale on radius, not on a clock).
*/
statePresets: {
// at rest: a slow half-turn of winding drifting outward
idle: {
speed: 0.5,
wave: 0.7,
twist: 1,
turb: 1,
column: 0,
exposure: 60,
scatter: 0.01,
alphaGain: 2
},
/*
searching: the ball WINDS UP — three and a half times the twist, so
the shells shear hard against each other — while the wave almost
stops. Exposure goes UP, not down: a wound spring is stored energy,
and this state is the darkest of the three on purpose.
*/
thinking: {
speed: 1.5,
wave: 0.15,
twist: 3.6,
turb: 1.35,
column: 0,
exposure: 88,
scatter: 0.011,
alphaGain: 2
},
/*
answering: the winding UNWINDS to a third of idle and the wave races
out through the shells at more than four times idle — the tension
released outward — with the column let go and the knee less than a
third of the thinking state's. The release is the bright one.
*/
speaking: {
speed: 0.9,
wave: 3.2,
twist: 0.35,
turb: 0.85,
column: 0.45,
exposure: 26,
scatter: 0.006,
alphaGain: 2.7
}
},
// the march colour-codes itself, so the tint only shifts temperature:
// neutral at rest, cooled while searching, warmed while answering
stateColors: {
idle: { tint: "#ffffff" },
thinking: { tint: "#9db8ff" },
speaking: { tint: "#ffc492" }
}
};
export type Shdr07Props = Omit<ShaderOrbProps, "variant">;
export function Shdr07({ size = 280, ...rest }: Shdr07Props) {
return <ShaderOrb variant={shdr07Orb} size={size} {...rest} />;
}
export default Shdr07;
10. components/ui/shdr-08.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-08 — mother-of-pearl: contour bands wrapped around the ball, every band
carrying its own hue.
Ported from a five-line twigl listing:
vec2 p=FC.xy*6./r.y;
for(float i;i++<1e1;i)
p+=sin(p.yx*i+i*i+t*i+r)/i;
o=tanh(.2/tan(p.y+vec4(0,.1,.3,0)));
o*=o;
What it actually is, decoded:
- The loop is a TEN-OCTAVE FEEDBACK WARP. Each octave reads the previous
octave's result with its components SWAPPED (p.yx), which is what curls
the field instead of merely rippling it. i*i is a per-octave phase so
the octaves never line up, and t*i runs octave ten at ten times the
clock — fine detail boils while the large structure barely moves.
- The last line is the whole image. tan() has a zero every PI, so
.2/tan() blows up there and tanh saturates into a flat crest; tan is
unbounded at PI/2, so .2/tan passes through zero and the valley floor
is exact black. The band period is PI, not 2PI.
- vec4(0,.1,.3,0) is a PER-CHANNEL PHASE. The spacing is not linear —
0, 1, 3 in units of the split — so red and green sit close together
and blue lags far behind: every band edge breaks into a warm shoulder
on one side and a deep blue one on the other.
- o *= o folds the sign (cot is negative on half of every period) and
squares the contrast in one move.
Port decisions, each one a documented trap or rule in the README:
- THE ORB IS THE OBJECT: this is a flat 2D field, so it is sampled
through a stereographic projection of the dome — the bands compress
toward the limb the way layers on a real shell do — and finished with
a fresnel sheen. It is NOT a disc masked out of the plane.
- The projection is taken on the UNROTATED dome, and all motion is
projection-safe 2D (a swirl of the plane plus a drift across the
bands). Rolling the dome in 3D first mixes sp.x into sp.z and the
divisor collapses — the trap written up at length in shdr-02.
- Nacre is thin-film interference, so hue follows the layer AND the
viewing angle: the cosine palette is keyed to the band coordinate
itself, and 1 - z rotates that hue toward the limb, so the ball's own
curvature colours the pattern.
- The listing's +r adds the RESOLUTION as phase. Free entropy in a demo;
here it would re-seed the whole pattern on every canvas resize, so a
fixed vec2 does the same job and holds still.
- .2/tan(x) is .2*cot(x), computed as cos/sin so there is ONE guarded
division. Dividing by tan() guards nothing — tan is itself unbounded
where cos is zero.
- The golfed listing relies on i starting at zero; uninitialised locals
are UNDEFINED in GLSL ES 1.0, explicit here.
- Surface-lit and mask-bounded, so alpha IS coverage — premultiplied
output, as in shdr-17.
---------------------------------------------------------------------------- */
/*
* Step counts are `#define`s: ES 1.0 requires constant loop bounds. OCTAVES
* is the listing's i++ < 1e1. AA supersamples the band function, which is the
* only defence against limb aliasing available here — WebGL 1 has no fwidth
* without an extension, and deriving the footprint through ten octaves of
* feedback warp analytically is not worth the algebra for a shader this cheap.
*/
const NACRE_FRAG = `
#define OCTAVES 10
#define AA 2
const float TAU = 6.28318530718;
// The listing's +r, minus the resolution dependence (see the header).
const vec2 SEED = vec2(4.7, 2.3);
// Volume-reactive values, resolved once per fragment in main().
float nacreWarp;
float nacreThick;
float nacreGain;
/*
The listing's entire tone map: o = tanh(.2 / tan(x)); o *= o.
The square folds the sign, so abs() on the denominator is not an
approximation here — it is exact, and it removes the branch.
*/
vec3 cotBands(vec3 x, float k) {
vec3 b = tanh3(k * cos(x) / max(abs(sin(x)), vec3(1e-4)));
return b * b;
}
vec3 nacreRender(vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
float R = max(uP_radius, 0.001);
// the dome: the front hemisphere of a unit ball, in screen space
vec2 pl = uv / R;
float z = sqrt(max(1.0 - dot(pl, pl), 0.0));
vec3 n = vec3(pl, z);
float t = uP_speed; // integrated clock: the boil
/*
Stereographic projection, on the UNROTATED dome. Equal steps in screen
space map to ever-larger steps in pattern space toward the rim, which
is the foreshortening that sells a flat field as wrapped geometry.
uP_bulge softens the divisor — higher flattens the shell back toward a
disc, lower crowds the layers into the limb.
*/
vec2 p = n.xy / (n.z + 1.0 + uP_bulge) * uP_scale;
// projection-safe 2D motion, in place of a dome spin: the plane turns,
// and the bands travel across themselves
float sw = uP_swirl; // integrated clock
p = mat2(cos(sw), -sin(sw), sin(sw), cos(sw)) * p;
p.y -= uP_flow; // integrated clock
/*
The ten-octave feedback warp. q is fed back into itself with the
components swapped, so each octave curls what the last one drew.
*/
vec2 q = p;
for (int j = 0; j < OCTAVES; j++) {
float i = float(j) + 1.0;
q += nacreWarp * sin(q.yx * i + i * i + t * i + SEED) / i;
}
// the bands, with the listing's uneven per-channel phase kept as a ratio
// so one slider widens the whole split
vec3 band = cotBands(vec3(q.y) + vec3(0.0, 1.0, 3.0) * uP_split, nacreThick);
float lev = dot(band, vec3(1.0 / 3.0));
/*
A dark body colour under the bands, so the valleys read as the shell
itself rather than as holes punched through the ball. The band term
stays PER-CHANNEL through the palette multiply — that is what carries
the colour fringing; collapsing it to lev first would throw away the
only thing the vec4 phase was for.
*/
vec3 col = uC_deep * uP_floor;
col += band * mix(uC_low, uC_crest, smoothstep(0.1, 0.9, lev)) * nacreGain;
/*
Thin-film interference. The cosine palette is keyed to the band
coordinate, so every layer carries its own hue — the inside of a shell
— and uP_view rotates that hue with the viewing angle through 1 - z,
which means the sphere's curvature is doing the colouring. Multiplied
in rather than mixed to, so it bends hues without erasing the palette.
*/
vec3 irid = 0.5 + 0.5 * cos(TAU * (q.y * uP_irisScale + (1.0 - z) * uP_view + t * 0.03 + vec3(0.0, 0.33, 0.67)));
col = mix(col, col * (0.25 + 1.9 * irid), uP_iris);
col = pow(max(col, vec3(0.0)), vec3(uP_contrast));
// dome shading keeps the ball a ball under the pattern
float lambert = clamp(dot(n, normalize(vec3(-0.45, 0.55, 0.72))), 0.0, 1.0);
col *= 0.35 + uP_light * lambert;
// fresnel sheen: the wet gloss of a shell, and the thing that keeps the
// limb reading as a surface where the bands have compressed to a blur
float fres = 1.0 - z;
fres = fres * fres * fres;
col += uC_sheen * uP_rim * fres;
return col;
}
void main() {
/*
The BEAT: warp driven between 0.6 and 1.8 on a slow, unbroken cycle —
one full swing every second and a third — with nothing held at either
end. Warp is the amplitude of the octave loop that folds the bands, so
sweeping it three to one makes the whole field draw in and open again
rather than change colour or brightness — a swell, not a flash.
Absolute bounds, so the range is exactly the range; that is why the mix
sits outside the volume term below. At beat zero nothing here applies
and the dialled warp stands, so a state that does not ask for it is
untouched.
Well under 3Hz, which matters: above that a full-field oscillation is
in the band photosensitivity guidance warns about, and this one covers
the whole ball. The rate is the constant below — raising it much past
18 walks back into that range.
*/
float beat = 0.5 - 0.5 * cos(uAnim * 5.0);
// Volume coupling: the user's voice churns the warp harder, the agent's
// widens the crests and brightens them.
nacreWarp = mix(uP_warp * (1.0 + 0.45 * uInput), mix(0.6, 1.8, beat), uP_beat);
nacreThick = uP_thick * (1.0 + 0.6 * uOutput);
nacreGain = uP_gain * (0.85 + 0.4 * uOutput);
vec2 uv = orbUV();
float mask = smoothstep(0.012, -0.012, length(uv) - max(uP_radius, 0.001));
// Nothing outside the silhouette is ever visible, so skip AA * AA warps
// for it rather than shading transparent sky.
if (mask <= 0.0) {
gl_FragColor = vec4(0.0);
return;
}
vec3 col = vec3(0.0);
#if AA > 1
for (int mx = 0; mx < AA; mx++) {
for (int my = 0; my < AA; my++) {
vec2 off = (vec2(float(mx), float(my)) + 0.5) / float(AA) - 0.5;
col += nacreRender(gl_FragCoord.xy + off);
}
}
col /= float(AA * AA);
#else
col = nacreRender(gl_FragCoord.xy);
#endif
// Surface orb bounded by a mask: alpha IS coverage, so premultiply — the
// opposite convention from the emissive orbs (see shdr-31).
float a = mask;
gl_FragColor = vec4(max(col, vec3(0.0)) * a, a);
}
`;
export const shdr08Orb: OrbVariant = {
key: "shdr-08",
label: "SHDR-08",
note: "mother-of-pearl contour bands, each layer its own hue",
frag: NACRE_FRAG,
params: [
{ key: "speed", label: "Boil", min: 0.015, max: 10, step: 0.05, default: 0.35, integrate: true },
{ key: "flow", label: "Band drift", min: 0, max: 5, step: 0.03, default: 0.25, integrate: true },
{ key: "swirl", label: "Swirl", min: 0, max: 3, step: 0.015, default: 0.06, integrate: true },
{ key: "radius", label: "Radius", min: 0.15, max: 3, step: 0.015, default: 0.9 },
{ key: "scale", label: "Pattern scale", min: 0.3, max: 20, step: 0.1, default: 5.5 },
{ key: "bulge", label: "Dome bulge", min: 0, max: 4, step: 0.02, default: 0.3 },
{ key: "warp", label: "Warp", min: 0, max: 3, step: 0.02, default: 1 },
{ key: "beat", label: "Warp beat", min: 0, max: 1, step: 0.01, default: 0 },
{ key: "thick", label: "Band width", min: 0.02, max: 2, step: 0.01, default: 0.2 },
{ key: "split", label: "Chromatic split", min: 0, max: 1, step: 0.005, default: 0.1 },
{ key: "iris", label: "Iridescence", min: 0, max: 2, step: 0.01, default: 0.7 },
{ key: "irisScale", label: "Iridescence scale", min: 0, max: 2, step: 0.005, default: 0.315 },
{ key: "view", label: "Angle shift", min: 0, max: 3, step: 0.01, default: 1.34 },
{ key: "floor", label: "Body fill", min: 0, max: 3, step: 0.01, default: 0.8 },
{ key: "gain", label: "Brightness", min: 0.05, max: 5, step: 0.05, default: 1.1 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 10, step: 0.05, default: 1.15 },
{ key: "light", label: "Key light", min: 0, max: 3, step: 0.015, default: 0.75 },
{ key: "rim", label: "Rim sheen", min: 0, max: 3, step: 0.015, default: 0.5 }
],
/*
* Four stops: the shell body the bands sit on, the two ends of the band
* ramp, and the fresnel sheen. The iridescence multiplies a rainbow over
* all of them, so the palette sets the mood and the shimmer supplies the
* rest of the hues.
*/
colors: [
{ key: "deep", label: "Shell body", default: "#0d1430" },
{ key: "low", label: "Dim layer", default: "#2fb8c6" },
{ key: "crest", label: "Bright layer", default: "#fff1de" },
{ key: "sheen", label: "Sheen", default: "#bfe4ff" }
],
/*
Staged on the two levers that are phase-safe integrated clocks — the
boil and the band drift — plus band width, which is the orb's loudest
single control: narrow crests read as taut lines, wide ones flood the
shell with light.
IRIDESCENCE SCALE AND ANGLE SHIFT ARE ALL BUT FIXED, and the reason
matters. Both multiply their way into the cosine that makes the
thin-film rainbow, so they are spatial FREQUENCIES, not amounts: easing
one across a wide span sweeps the field through every frequency in
between, which races the fringes across the shell instead of
cross-fading them. Pattern scale is held fixed here for the same reason,
as are cell count in shdr-05 and lattice spacing in shdr-06.
So they live on the params above, where resting and answering take them
untouched. Searching nudges them and only just — scale to 0.83 of the
fixed value, angle shift to 0.90 — which moves the fringes by well under
one of their own periods, and reads as the shimmer settling rather than
as a scramble. That margin is the whole budget: a state that wanted
twice or a third of these would have to snap them, not glide.
Iridescence itself — the amount — is safe to stage at any span: it is a
plain mix toward the same rainbow, so it fades rather than moves.
*/
statePresets: {
/*
at rest: slow and BRIGHT. The clocks stay gentle — a slow boil, the
layers barely drifting — which is what still reads as at rest, but
everything about the surface is turned up under them. Crests run wide,
three quarters of the answering state's width, on more than twice the
default brightness and the highest gain of the three.
The shimmer carries the rest: iridescence near half again its default,
laid over the fixed scale and angle shift the staging note above keeps
out of the presets — the rainbow swings hard as the dome curves away,
and it does so identically in all three states. The dome is bulged
well past default and the body fill pulled back beneath it, so the
light sits in the bands rather than in the shell behind them.
*/
idle: {
speed: 0.37,
flow: 0.24,
swirl: 0.06,
bulge: 0.8,
warp: 0.96,
thick: 0.54,
split: 0.1,
iris: 1.02,
floor: 0.57,
gain: 2.45,
contrast: 1.65,
rim: 0.495
},
/*
searching: the field CHURNS in place — boil at over four times resting,
warp half again — while the layers all but stop drifting, a quarter of
resting's flow on a third of its swirl. Crests hold exactly resting's
width, so nothing about the BANDS says searching; what says it is that
they are boiling hard and going nowhere.
The shimmer is the other half of it: iridescence at nearly twice
resting's, the strongest of the three by a wide margin, over a split
that answering now matches. And still deliberately the dimmest —
tension reads as held light, not spent light.
*/
thinking: {
speed: 1.61,
flow: 0.06,
swirl: 0.015,
warp: 1.56,
thick: 0.54,
split: 0.3,
iris: 1.89,
irisScale: 0.26,
view: 1.21,
floor: 0.77,
gain: 0.85,
contrast: 1.65,
rim: 0.495
},
/*
answering: the field SWELLS and TRAVELS. Warp is on the beat at full
depth, swinging 0.6 to 1.8 across a second and a third with nothing
held at either end, so the bands draw in and open again slowly enough
to watch — that swell IS the state, and the dialled warp below is only
what you would see with the beat off.
Under it everything is moving: the fastest boil of the three, and the
hardest drift anywhere in this orb at ten times resting's, on crests
pulled to half the width the other two both hold. Iridescence drops to
the lowest of the three while the split opens out to match searching's
— so this state spends its light on MOVEMENT rather than on shimmer,
which is what keeps the two apart now that both run their bands hard.
*/
speaking: {
speed: 2,
flow: 2.49,
swirl: 0.18,
warp: 1.84,
beat: 1,
thick: 0.27,
split: 0.3,
iris: 0.45,
gain: 1.75,
contrast: 0.78
}
},
// abalone at rest, cold pearl while searching, warm fire-opal while
// answering — the at-a-glance read, as in the sibling orbs
stateColors: {
idle: {
deep: "#0d1430",
low: "#2fb8c6",
crest: "#fff1de",
sheen: "#bfe4ff"
},
thinking: {
deep: "#0a0f2c",
low: "#6f7cff",
crest: "#dfe8ff",
sheen: "#9fd0ff"
},
speaking: {
deep: "#2a0f22",
low: "#ff7a4d",
crest: "#fff0c9",
sheen: "#ffc9a8"
}
}
};
export type Shdr08Props = Omit<ShaderOrbProps, "variant">;
export function Shdr08({ size = 280, ...rest }: Shdr08Props) {
return <ShaderOrb variant={shdr08Orb} size={size} {...rest} />;
}
export default Shdr08;
11. components/ui/shdr-09.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-09 — torn rings of rainbow light, worn as the ball's own latitudes.
Ported from a golfed twigl listing (its feedback variant):
vec2 p=(FC.xy*2.-r)/r.y/.2,v;
for(float i,l,f;i++<1e1;
o+=.03/max(l=length(v)-i,-l*3.)*(cos(t-i*.4+.1/l+vec4(0,1,2,3))+1.1))
for(v=p,f=0.;f++<9.;v+=sin(ceil(v*f+i*.9)-t/2.)/f);
o=max(tanh(o+(o=texture(b,(FC.xy+r.y*.05*sin(FC.xy+FC.yx/.6))/r))*o),.0);
What it actually is, decoded:
- TEN CONCENTRIC RINGS. The accumulator lives in the outer loop's
increment slot, so it runs after the inner loop: each ring re-warps
the ORIGINAL point from scratch (v=p) with its own seed i*.9, then
draws the circle of radius i through that ring's private distortion.
Ten rings, ten different tears — they are not ten copies of one shape.
- The warp is CELL-QUANTIZED, sin(ceil(v*f) - t): every lattice cell
flickers on its own phase, the same construction shdr-22 uses for
its voxel shimmer, here in 2D and re-seeded per ring.
- max(l, -l*3.) is an ASYMMETRIC absolute value — l outside the ring,
3|l| inside. So 1/that lights the outer shoulder of every ring three
times as hard as the inner one, and the rings read as expanding
wavefronts rather than as symmetric wires.
- .1/l inside the colour phase is the good part. It sweeps through the
entire hue wheel in the last hair of distance before the ring, so
each ring carries a rainbow fringe that compresses to a hard line
exactly where the glow peaks.
- The last line is FRAME FEEDBACK: o + prev*o, where prev is the
previous frame read through a pixel-scale sinusoidal scramble. That
is what smears the rings into trails in the original.
Port decisions, each one a documented trap or rule in the README:
- THE ORB IS THE OBJECT, and this listing gets a better mapping than
the family's usual one. A field of concentric circles about the
origin does not want a stereographic wrap — the circles ARE latitude
rings, so radius maps to the POLAR ANGLE from the dome's axis and the
foreshortening is exact instead of approximated.
- That mapping also has no pole to collapse. acos() is defined across
the whole sphere, so unlike shdr-08 and shdr-26 — where the
stereographic divisor forbids it — this dome rolls freely in 3D, and
rings sweep into view over the limb as it turns.
- THE FEEDBACK TERM IS NOT PORTED. The engine renders one pass into one
canvas with no previous-frame texture, so there is nothing to sample;
adding a ping-pong framebuffer is an engine change, not an orb one.
What is lost is the temporal smear. What the rings do in space is all
here.
- Both singular divisors are softened rather than guarded: the glow
divisor is floored, which doubles as the LINE WIDTH (the same trick
as shdr-02's coreClamp), and the .1/l colour phase becomes
l/(l*l+g), which is 1/l everywhere the listing cared about and finite
on the ring itself. Left raw, the hue sweep reaches infinite frequency
exactly where the glow is brightest — the one place aliasing is
guaranteed to show.
- The golfed listing relies on i, l and f starting at zero;
uninitialised locals are UNDEFINED in GLSL ES 1.0, explicit here.
- Surface-lit and mask-bounded, so alpha IS coverage — premultiplied
output, as in shdr-17.
---------------------------------------------------------------------------- */
/*
* Step counts are `#define`s: ES 1.0 requires constant loop bounds. RINGS and
* TURB are the listing's i++ < 1e1 and f++ < 9. Every ring pays for its own
* warp, so this is RINGS * TURB sines per sample before supersampling.
*/
const IRIS_FRAG = `
#define RINGS 10
#define TURB 9
#define AA 2
// Volume-reactive values, resolved once per fragment in main().
float irisWarp;
float irisGlow;
float irisFringe;
vec3 irisRender(vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
float R = max(uP_radius, 0.001);
// the dome: the front hemisphere of a unit ball, in screen space
vec2 pl = uv / R;
float z = sqrt(max(1.0 - dot(pl, pl), 0.0));
vec3 n = vec3(pl, z);
float t = uP_speed; // integrated clock
// tilt about X, then roll about Y on its own integrated clock
float ct = cos(uP_tilt);
float st = sin(uP_tilt);
vec3 sp = vec3(n.x, n.y * ct - n.z * st, n.y * st + n.z * ct);
float cr = cos(uP_spin);
float sr = sin(uP_spin);
sp = vec3(sp.x * cr - sp.z * sr, sp.y, sp.x * sr + sp.z * cr);
/*
Radius becomes the polar angle from the dome's axis (see the header),
so ring i lands on the latitude at angle i / uP_scale and the rings
crowd toward the limb the way a globe's latitudes do. acos is defined
on the whole sphere, so the roll above can put the axis anywhere —
including behind the visible face, which sweeps the outer rings into
view over the limb.
*/
float pol = acos(clamp(sp.z, -1.0, 1.0));
vec2 dir = sp.xy / max(length(sp.xy), 1e-4);
vec2 p = dir * pol * uP_scale;
vec3 acc = vec3(0.0);
for (int ri = 0; ri < RINGS; ri++) {
float i = float(ri) + 1.0;
/*
Each ring re-warps the ORIGINAL point with its own seed, exactly as
the listing does — this loop is why the rings tear differently
instead of nesting like tree rings.
*/
vec2 v = p;
for (int j = 0; j < TURB; j++) {
float f = float(j) + 1.0;
v += irisWarp * sin(ceil(v * f + i * uP_seed) - t * 0.5) / f;
}
float l = length(v) - i;
// the asymmetric absolute value, with the floor doubling as the line
// width — a wider floor is a fatter, softer wavefront
float side = max(max(l, -uP_inner * l), uP_lineSoft);
/*
The hue sweep, softened. l/(l*l+g) tracks 1/l off the ring and rolls
over to a finite peak on it, so the rainbow compresses into a fringe
of finite width instead of an aliased band.
*/
float fr = irisFringe * l / (l * l + uP_fringeSoft);
vec3 hue = cos(t - i * uP_ringPhase + fr + vec3(0.0, 1.0, 2.0)) + 1.1;
acc += (irisGlow / side) * hue;
}
// the listing's tanh knee, with the divisor exposed
vec3 col = tanh3(acc / max(uP_exposure, 0.001));
col = pow(clamp(col, 0.0, 1.0), vec3(uP_contrast));
// saturation about luminance, then the tint
float lum = dot(col, vec3(0.299, 0.587, 0.114));
col = mix(vec3(lum), col, uP_saturation);
col *= uC_tint;
// dome shading keeps the ball a ball under the rings — gentler than the
// sibling orbs use, because these rings are emission and a hard lambert
// reads as a shadow thrown across a light source
float lambert = clamp(dot(n, normalize(vec3(-0.45, 0.55, 0.72))), 0.0, 1.0);
col *= 0.55 + uP_light * lambert;
float fres = 1.0 - z;
fres = fres * fres * fres;
col += uC_sheen * uP_rim * fres;
return col;
}
void main() {
// Volume coupling: the user's voice tears the rings harder, the agent's
// brightens them and opens the rainbow fringe.
irisWarp = uP_warp * (1.0 + 0.5 * uInput);
irisGlow = uP_glow * (0.85 + 0.5 * uOutput);
irisFringe = uP_fringe * (1.0 + 0.6 * uOutput);
vec2 uv = orbUV();
float mask = smoothstep(0.012, -0.012, length(uv) - max(uP_radius, 0.001));
// Ninety sines per sample before supersampling — none of them worth
// paying for outside the silhouette.
if (mask <= 0.0) {
gl_FragColor = vec4(0.0);
return;
}
vec3 col = vec3(0.0);
#if AA > 1
for (int mx = 0; mx < AA; mx++) {
for (int my = 0; my < AA; my++) {
vec2 off = (vec2(float(mx), float(my)) + 0.5) / float(AA) - 0.5;
col += irisRender(gl_FragCoord.xy + off);
}
}
col /= float(AA * AA);
#else
col = irisRender(gl_FragCoord.xy);
#endif
// Surface orb bounded by a mask: alpha IS coverage, so premultiply — the
// opposite convention from the emissive orbs (see shdr-31).
float a = mask;
gl_FragColor = vec4(max(col, vec3(0.0)) * a, a);
}
`;
export const shdr09Orb: OrbVariant = {
key: "shdr-09",
label: "SHDR-09",
note: "torn rings of rainbow light worn as the ball's latitudes",
frag: IRIS_FRAG,
params: [
{ key: "speed", label: "Anim speed", min: 0.015, max: 10, step: 0.05, default: 0.6, integrate: true },
{ key: "spin", label: "Roll", min: 0, max: 3, step: 0.015, default: 0.12, integrate: true },
{ key: "tilt", label: "Tilt", min: -1.5, max: 1.5, step: 0.015, default: 0.4 },
{ key: "radius", label: "Radius", min: 0.15, max: 3, step: 0.015, default: 0.9 },
{ key: "scale", label: "Ring spacing", min: 0.3, max: 20, step: 0.1, default: 3.5 },
{ key: "warp", label: "Tear", min: 0, max: 3, step: 0.02, default: 0.45 },
{ key: "seed", label: "Ring seed", min: 0, max: 3, step: 0.01, default: 0.9 },
{ key: "glow", label: "Ring glow", min: 0, max: 1, step: 0.002, default: 0.05 },
{ key: "lineSoft", label: "Ring width", min: 0.002, max: 1, step: 0.002, default: 0.05 },
{ key: "inner", label: "Inner falloff", min: 0.2, max: 12, step: 0.05, default: 3 },
{ key: "fringe", label: "Rainbow fringe", min: 0, max: 2, step: 0.005, default: 0.1 },
{ key: "fringeSoft", label: "Fringe width", min: 0.001, max: 1, step: 0.001, default: 0.003 },
{ key: "ringPhase", label: "Ring hue step", min: 0, max: 3, step: 0.01, default: 0.8 },
{ key: "exposure", label: "Exposure", min: 0.05, max: 20, step: 0.05, default: 1.1 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 10, step: 0.05, default: 1.1 },
{ key: "saturation", label: "Saturation", min: 0, max: 4, step: 0.02, default: 1.2 },
{ key: "light", label: "Key light", min: 0, max: 3, step: 0.015, default: 0.5 },
{ key: "rim", label: "Rim sheen", min: 0, max: 3, step: 0.015, default: 0.4 }
],
colors: [
{ key: "tint", label: "Tint", default: "#ffffff" },
{ key: "sheen", label: "Sheen", default: "#b9d6ff" }
],
/*
Staged on the two things the rings own: how hard they TEAR, and how
wide the wavefront is. Ring spacing never moves between states — it
sets how many rings are on the ball, and a gliding count reads as the
ball inflating rather than as a change of mood.
*/
statePresets: {
// at rest: slow, softly torn, wide calm wavefronts
idle: {
speed: 0.6,
spin: 0.12,
warp: 0.45,
glow: 0.05,
lineSoft: 0.05,
fringe: 0.1,
exposure: 1.1,
contrast: 1.15
},
/*
searching: the rings tear right open — warp near six times idle — and
broaden rather than thin, so the ball reads as churning instead of
brittle. The rainbow fringe runs almost to full and softens by two
orders of magnitude, which is what turns the tears into wide spectral
bands; exposure, saturation and the rim all lift together to keep that
readable. Speed and spin sit at the schema defaults, so the ball keeps
idle's rotation and the whole change reads in the surface, not the
motion.
*/
thinking: {
warp: 2.54,
glow: 0.2,
lineSoft: 0.152,
fringe: 0.96,
fringeSoft: 0.854,
ringPhase: 2.53,
exposure: 1.75,
contrast: 1.15,
saturation: 2.22,
light: 1.035,
rim: 0.66
},
/*
answering: the tears RELAX to half idle's and the wavefronts broaden
to four times it — bands of light rather than rings — but the ball is
travelling under them at near eight times idle speed, the fastest of
the three by a wide margin. Broad calm shapes moving fast, which is
the opposite of searching's tight shapes churning in place.
The colour comes off the FRINGE rather than the glow: eight times
idle's fringe strength on twenty times its width, with the ring glow
pulled just under idle's and the knee down, so the light gathers in
the spectral edges instead of flooding the rings themselves. The key
light doubles to keep a dome under all that, and the rim sheen is off
almost entirely.
Note the RING SEED is staged here, and it is the one value in this
preset that cannot glide: it sits inside a ceil() in the octave loop,
so it quantizes, and easing it across a state change steps rather than
fades. The move is small — 0.9 to 1.02 — but if the transition pops,
that is what is popping, and the fix is to hold it equal in all three
states rather than to slow it down.
*/
speaking: {
speed: 4.7,
spin: 0.34,
warp: 0.22,
seed: 1.02,
glow: 0.04,
lineSoft: 0.22,
fringe: 0.81,
fringeSoft: 0.058,
ringPhase: 1.03,
exposure: 0.68,
contrast: 0.85,
light: 0.99,
rim: 0.015
}
},
// the rings supply their own rainbow, so the tint only shifts its
// temperature: neutral at rest, cooled while searching, warmed while
// answering
stateColors: {
idle: { tint: "#ffffff", sheen: "#b9d6ff" },
thinking: { tint: "#9db8ff", sheen: "#7ba6ff" },
speaking: { tint: "#ffc492", sheen: "#ffb277" }
}
};
export type Shdr09Props = Omit<ShaderOrbProps, "variant">;
export function Shdr09({ size = 280, ...rest }: Shdr09Props) {
return <ShaderOrb variant={shdr09Orb} size={size} {...rest} />;
}
export default Shdr09;
12. components/ui/shdr-10.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-10 — a lattice of light knitted into the ball's own skin.
Ported from a golfed twigl listing:
for(float z,d,i;i++<4e1;){
vec3 p=z*normalize(FC.rgb*2.-r.xyx);
p=vec3(atan(p.z+=9.,p.x+1.)*2.,.6*p.y+t+t,length(p.xz)-3.);
for(d=1.;d<7.;d++)p+=sin(p.yzx*d+t+.5*i)/d;
z+=d=.4*length(vec4(.3*cos(p)-.3,p.z));
o+=(cos(p.y+i*.4+vec4(6,1,2,0))+1.)/d;}
o=tanh(o*o/6e3);
What it actually is, decoded:
- THE SECOND LINE IS AN UNWRAP. Rewriting the sample as
(angle, height, radius - 3) is the standard tunnel unwrap: it flattens
a CYLINDER of radius three into a strip, so the field can be written
in plain coordinates and still come out wrapped around a pipe.
- THE DENSITY IS A LATTICE ON THAT SURFACE. length of
(.3*cos(p) - .3, p.z) is small only where all three cosines sit at one
— a 3D lattice in unwrapped space — AND p.z is near zero, which is the
shell. Cells of the lattice that land on the shell light up; the rest
is empty. A knitted skin, not a volume.
- EVERY MARCH STEP GETS ITS OWN PHASE. .5*i in the warp and i*.4 in the
colour mean consecutive samples are not looking at the same field, so
the sum comes out as layered gauze rather than as forty copies of one
surface. It is the cheapest volumetric trick in the listing.
- o*o BEFORE the knee is a contrast squarer, not a tone map, the same
move shdr-06 ends on.
Port decisions, each one a documented trap or rule in the README:
- THE ORB IS THE OBJECT, and here that is a ONE TOKEN change: the
unwrap's radius term reads length(p.xz), the distance from an axis,
which makes a pipe. Read length(p) instead — the distance from a
POINT — and the identical field wraps a sphere. This is exactly the
move the README describes for shdr-20, whose sigmoid cliff became
the ball's own shell. Nothing else about the listing had to move.
- THE WRAP MULTIPLIER MUST STAY AN INTEGER. atan jumps by 2*PI across
the seam behind the ball, so the unwrapped angle jumps by 2*PI times
the multiplier; every consumer of it here is a sine or cosine of an
integer multiple, so at integer wrap the jump is a whole number of
periods and the seam is invisible. The listing's 2 is not decorative.
Set a fraction and a hard line opens down the back.
- The listing offsets its axis by one in x, which is a cylinder-axis
offset with no meaning on a sphere. Dropped.
- The golfed listing relies on z, d and i starting at zero;
uninitialised locals are UNDEFINED in GLSL ES 1.0, explicit here.
- No step-length weighting: 1/d is the density, as in shdr-22 and
shdr-18, and multiplying by the step would cancel it exactly.
- The accumulator is divided by the step count before the square, so the
golfed 6e3 knee lands on a number that fits a slider — the same
normalization as shdr-06, and for the same reason.
- Emitted light, so rgb is already premultiplied and alpha comes from
the peak channel (see shdr-31).
---------------------------------------------------------------------------- */
/*
* Step counts are `#define`s: ES 1.0 requires constant loop bounds. STEPS and
* TURB are the listing's i++ < 4e1 and d < 7.
*/
const WEAVE_FRAG = `
#define STEPS 40
#define TURB 6
#define AA 1
// Volume-reactive values, resolved once per fragment in main().
float weaveTurb;
float weaveCell;
float weaveExposure;
vec3 weaveRender(vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
vec3 ro = vec3(0.0, 0.0, uP_camDist);
vec3 rd = normalize(vec3(uv, -uP_focal));
float animTime = uP_speed; // integrated clock: the warp
float scroll = uP_scroll; // integrated clock: the skin climbs
vec3 acc = vec3(0.0);
// transmittance carried front-to-back — the near skin veils the far one
float T = 1.0;
// march only the span the envelope can light, as in shdr-01
float z = max(uP_camDist - uP_envRadius * 1.3, 0.0);
float zEnd = uP_camDist + uP_envRadius * 1.3;
for (int it = 0; it < STEPS; it++) {
float fi = float(it) + 1.0;
vec3 world = ro + rd * z;
/*
The unwrap, with the listing's cylinder swapped for the ball: angle
about the axis, height, and distance from the CENTRE less the shell
radius. uP_wrap wants to stay a whole number — see the header.
*/
float rl = length(world);
vec3 p = vec3(
atan(world.z, world.x) * uP_wrap,
world.y * uP_climb + scroll,
rl - uP_shellR
);
// six octaves of feedback warp, each march step on its own phase
for (int j = 0; j < TURB; j++) {
float dj = float(j) + 1.0;
p += weaveTurb * sin(p.yzx * dj + animTime + uP_layer * fi) / dj;
}
/*
The lattice. Small only where all three cosines sit at one and the
sample is on the shell — so the cells of a 3D lattice in unwrapped
space are cut by the ball's surface, and what is left is a knitted
skin. uP_cell is the listing's .3: the amplitude of the cosine terms
against the shell term, and therefore how much the lattice matters
relative to simply being on the surface.
*/
float d = uP_stepScale * length(vec4(weaveCell * cos(p) - weaveCell, p.z));
d = max(d, uP_envRadius * 0.004);
// colour by unwrapped height, with each step offset again
vec3 w = cos(p.y + fi * uP_hueStep + vec3(6.0, 1.0, 2.0) * uP_spread) + 1.0;
w /= d;
w = min(w, vec3(uP_stepClamp));
// envelope: plateau through the ball, cut 12% past the radius so the
// analytic silhouette in main() still has emission left to cut
float env = smoothstep(uP_envRadius * 1.12, uP_envRadius * uP_envCore, rl);
w = (w + uP_fill) * env;
acc += T * w;
T *= exp(-dot(w, vec3(0.299, 0.587, 0.114)) * uP_scatter);
z += d;
if (T < 0.004 || z > zEnd) break;
}
return acc;
}
void main() {
weaveTurb = uP_turb * (1.0 + 0.4 * uInput);
weaveCell = uP_cell * (1.0 + 0.3 * uInput);
weaveExposure = uP_exposure * (1.0 - 0.3 * uOutput);
vec3 acc = vec3(0.0);
#if AA > 1
for (int mx = 0; mx < AA; mx++) {
for (int my = 0; my < AA; my++) {
vec2 offset = (vec2(float(mx), float(my)) + 0.5) / float(AA) - 0.5;
acc += weaveRender(gl_FragCoord.xy + offset);
}
}
acc /= float(AA * AA);
#else
acc = weaveRender(gl_FragCoord.xy);
#endif
/*
The listing's knee is tanh(o*o/6e3) over an unnormalized sum of forty
steps. Dividing by the step count first pulls the square's scale down
by forty squared, so the same knee lands near four — a number that fits
on a slider. The square is a contrast squarer, not a tone map.
*/
vec3 v = acc / float(STEPS);
vec3 col = tanh3(v * v / max(weaveExposure, 0.0001));
col = pow(clamp(col, 0.0, 1.0), vec3(uP_contrast));
// saturation about luminance, then the tint
float lum = dot(col, vec3(0.299, 0.587, 0.114));
col = mix(vec3(lum), col, uP_saturation);
col *= uC_tint;
// alpha from the brightest channel, not luminance — a deep blue thread
// has low luminance but must not go transparent
float peak = max(col.r, max(col.g, col.b));
float a = clamp(peak * uP_alphaGain, 0.0, 1.0);
// Analytic silhouette — identical construction to shdr-01: exact
// ray-to-centre distance against the radius, colour AND alpha.
vec3 mrd = normalize(vec3(orbUV(), -uP_focal));
float closest = length(cross(vec3(0.0, 0.0, uP_camDist), mrd));
float band = mix(0.35, 0.012, clamp(uP_edge, 0.0, 1.0));
float mask = 1.0 - smoothstep(uP_envRadius * (1.0 - band), uP_envRadius * 1.005, closest);
col *= mask;
a *= mask;
// safety taper at the frame boundary — colour as well as alpha
float r2d = length(orbUV());
float fade = 1.0 - smoothstep(uP_edgeFade, 1.0, r2d);
col *= fade;
a *= fade;
// Emitted light, so rgb is already premultiplied — do NOT scale by alpha
// again (see the same note in shdr-31).
gl_FragColor = vec4(col, a);
}
`;
export const shdr10Orb: OrbVariant = {
key: "shdr-10",
label: "SHDR-10",
note: "a lattice of light knitted into the ball's own skin",
frag: WEAVE_FRAG,
params: [
{ key: "speed", label: "Anim speed", min: 0.015, max: 10, step: 0.05, default: 0.6, integrate: true },
{ key: "scroll", label: "Climb", min: 0, max: 8, step: 0.03, default: 1.2, integrate: true },
{ key: "camDist", label: "Camera distance", min: 1, max: 50, step: 0.3, default: 7 },
{ key: "focal", label: "Lens", min: 0.15, max: 15, step: 0.05, default: 2 },
{ key: "shellR", label: "Shell radius", min: 0.2, max: 20, step: 0.1, default: 2.6 },
{ key: "wrap", label: "Wraps around", min: 1, max: 14, step: 1, default: 8 },
{ key: "climb", label: "Band spacing", min: 0.05, max: 12, step: 0.05, default: 4 },
{ key: "turb", label: "Warp", min: 0, max: 3, step: 0.02, default: 0.35 },
{ key: "layer", label: "Layer offset", min: 0, max: 2, step: 0.01, default: 0.5 },
{ key: "cell", label: "Lattice weight", min: 0, max: 2, step: 0.01, default: 0.45 },
{ key: "stepScale", label: "Step scale", min: 0.02, max: 3, step: 0.005, default: 0.15 },
{ key: "hueStep", label: "Layer hue", min: 0, max: 3, step: 0.01, default: 0.4 },
{ key: "spread", label: "Colour spread", min: 0, max: 3, step: 0.02, default: 1 },
{ key: "envRadius", label: "Envelope radius", min: 0.15, max: 20, step: 0.1, default: 2.9 },
{ key: "envCore", label: "Envelope core", min: 0.3, max: 1.02, step: 0.01, default: 0.92 },
{ key: "fill", label: "Body fill", min: 0, max: 40, step: 0.05, default: 0.1 },
{ key: "stepClamp", label: "Step clamp", min: 5, max: 5000, step: 5, default: 300 },
{ key: "scatter", label: "Diffusion", min: 0, max: 0.2, step: 0.0005, default: 0.004 },
{ key: "exposure", label: "Exposure", min: 0.05, max: 500, step: 0.5, default: 30 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 15, step: 0.05, default: 1.15 },
{ key: "saturation", label: "Saturation", min: 0, max: 4, step: 0.02, default: 1.2 },
{ key: "alphaGain", label: "Alpha gain", min: 0.05, max: 15, step: 0.1, default: 2 },
{ key: "edge", label: "Edge sharpness", min: 0, max: 1, step: 0.01, default: 1 },
{ key: "edgeFade", label: "Halo falloff", min: 0.1, max: 3, step: 0.015, default: 0.98 }
],
colors: [{ key: "tint", label: "Tint", default: "#ffffff" }],
/*
Staged on LATTICE WEIGHT, which decides whether the ball wears a knitted
net or a smooth shell, and on the two clocks, which are integrated and
so change rate without ever jumping phase.
THE LAYER OFFSET IS STAGED ONLY ON A BUDGET, and the budget is
arithmetic rather than taste. It multiplies the SAMPLE INDEX inside the
warp — sin(... + uP_layer * fi), with fi running to forty — so it is a
phase rate across the stack, not an amount: a step of D moves the
deepest sample by 40*D radians while the nearest barely moves. The old
0.5 -> 2 stage was 60 radians, nine and a half turns, and the skin
boiled through every arrangement in between instead of cross-fading.
Hold |D| at or under 2*PI/40, about 0.157, and even the deepest sample
travels less than one full turn, which reads as the layers settling.
Resting sits at 0.5 and searching at 0.65 — 0.15, just inside it — and
answering stays at 0.5, so every transition in the set is within one
turn. Anything wider has to be reached with lattice weight and warp
instead: both are plain amplitudes, and both glide cleanly.
COLOUR SPLIT is staged on the same budget and the same reasoning. It
scales a fixed vec3 inside the hue cosine, so its worst channel moves
six radians per unit; searching's step of 0.55 is about half a turn on
that channel, which crossfades. A step past one unit would not.
Wraps around never moves either: it must stay a whole number or the seam
opens, and a slider gliding through 1.5 would tear the ball open in the
middle of a transition. Band spacing and layer hue are held for the same
class of reason — both are frequencies read off a coordinate.
*/
statePresets: {
// at rest: an open net climbing slowly
idle: {
speed: 0.6,
scroll: 1.2,
turb: 0.35,
cell: 0.45,
exposure: 30,
scatter: 0.004,
alphaGain: 2
},
/*
searching: the net DECOHERES. Three things pull in the same direction.
Lattice weight drops to two thirds of resting's, so the cosine terms
no longer close hard on their cells; the warp runs at nearly two and a
half times resting and churns what is left of them; and the layer
offset steps up by 0.15 — the whole phase budget above, and as far as
the forty samples can be pushed out of agreement without the
transition boiling.
And it is BRIGHT. The knee drops to well under half resting's — near
answering's, so this is no longer the dim state — with saturation
raised half again over resting, contrast pulled back so nothing
crushes, and the colour split opened to 1.55, which spreads the three
channels further apart in phase and is what turns the decohering skin
into full spectrum rather than a blue haze. The tint goes with it:
near white with only a cool cast, where a saturated blue would have
thrown all that colour away again.
Step scale drops to two thirds under all of it, so the ray resolves
finer detail over less depth, and the diffusion stays low enough that
the far side still shows through.
*/
thinking: {
speed: 1.9,
scroll: 0.5,
turb: 0.85,
layer: 0.65,
cell: 0.3,
stepScale: 0.09,
spread: 1.55,
fill: 0.12,
exposure: 13,
scatter: 0.006,
contrast: 1.05,
saturation: 1.7,
alphaGain: 2.6
},
/*
answering: the net SNAPS IN. Lattice weight goes to more than twice
resting's and nearly five times searching's, so the cells close hard
and the skin reads as knitted rope rather than gauze, with the warp
down to a fifth of searching's so nothing blurs it.
And it CLIMBS: the scroll clock runs three and a half times resting's,
the fastest of the three, so the whole net travels up the ball while
holding its shape. The knee drops to a quarter of searching's and the
alpha gain lifts — this is unmistakably the bright state.
*/
speaking: {
speed: 1,
scroll: 4.2,
turb: 0.18,
cell: 0.95,
exposure: 11,
scatter: 0.0015,
contrast: 0.9,
saturation: 1.45,
alphaGain: 2.8
}
},
// the height ramp supplies the colour, so the tint only shifts its
// temperature: neutral at rest, barely cooled while searching — a
// saturated blue there would cancel the colour split that state is built
// on — and warmed while answering
stateColors: {
idle: { tint: "#ffffff" },
thinking: { tint: "#e8f4ff" },
speaking: { tint: "#ffc492" }
}
};
export type Shdr10Props = Omit<ShaderOrbProps, "variant">;
export function Shdr10({ size = 280, ...rest }: Shdr10Props) {
return <ShaderOrb variant={shdr10Orb} size={size} {...rest} />;
}
export default Shdr10;
13. components/ui/shdr-11.tsx
/*
* Deliberately not a `"use client"` module. The directive lives on the runtime
* in `orbkit-core`, which owns the hooks; keeping it off this file lets server
* components read `shdr11Orb` as real data (its param schema drives the docs
* tables and the playground controls) instead of an opaque client reference.
*/
import {
ShaderOrb,
type OrbVariant,
type ShaderOrbProps
} from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-11 — the quantum-orbital orb.
Renders |psi|^2 of a hydrogen-like wave function projected onto the orb's
dome, shaded with rainbow chromatic bands over a dark metallic sphere.
The dome point is rotated around Y (the fake 3D of a flat disc), its spin
axis precesses so the pattern never settles into a visible loop, and a
drifting fbm field warps the 3D domain so the wave function smears and
migrates around the sphere instead of wobbling in place.
---------------------------------------------------------------------------- */
const HYDROGEN_FRAG = `
const float PI = 3.14159265359;
void main() {
vec2 uv = orbUV();
float r2d = length(uv);
float R = uP_radius + uP_swell * uInput;
float mask = smoothstep(0.012, -0.012, r2d - R);
float nr = clamp(r2d / max(R, 0.001), 0.0, 1.0);
float z = sqrt(max(1.0 - nr * nr, 0.0));
// uP_speed and uP_flowSpeed arrive pre-integrated as clocks (see
// OrbParamDef.integrate), so state transitions stay phase-continuous.
// The state volumes reshape the orbital itself: the params set the base,
// input/output excitement bends zoom, radial form, probability and chroma,
// so each state settles into a different interference pattern.
float posScale = uP_posScale * (0.8 + 0.45 * uOutput + 0.2 * uInput);
float radialPow = uP_radialPow * (0.7 + 0.8 * uOutput);
float radialDecay = uP_radialDecay * (1.25 - 0.5 * uOutput);
float probPow = uP_probPow * (1.3 - 0.55 * uOutput);
float probGain = uP_probGain * (0.7 + 0.6 * uOutput + 0.5 * uInput);
float waveFreq = uP_waveFreq * (0.6 + 1.0 * uOutput);
float chromaSpread = uP_chromaSpread * (0.6 + 0.9 * uOutput + 0.5 * uInput);
// dome point rotated around Y — the fake 3D of the flat disc
float animTime = uP_speed; // integrated clock
float cosT = cos(animTime * uP_rotSpeed);
float sinT = sin(animTime * uP_rotSpeed);
vec3 sp = vec3(uv / max(R, 0.001), z) * posScale;
vec3 pos = vec3(sp.x * cosT - sp.z * sinT, sp.y, sp.x * sinT + sp.z * cosT);
// precession: the rotation axis itself drifts, so the pattern never
// settles into a repeating spin
float tilt = sin(animTime * 0.21 + 1.7) * uP_precess;
float cx = cos(tilt), sx = sin(tilt);
pos = vec3(pos.x, pos.y * cx - pos.z * sx, pos.y * sx + pos.z * cx);
// liquid flow: drifting fbm warps the 3D domain, so the wave function
// smears and migrates around the sphere instead of wobbling in place.
// (sampled on pos components — continuous everywhere, no phi seam)
float flowT = uP_flowSpeed; // integrated clock
float fAmp = uP_flowAmp * (0.7 + 0.6 * uOutput + 0.4 * uInput);
vec3 w;
w.x = fbm(pos.yz * uP_flowScale + vec2(flowT * 0.70, -flowT * 0.40));
w.y = fbm(pos.zx * uP_flowScale + vec2(-flowT * 0.55, flowT * 0.62) + 3.7);
w.z = fbm(pos.xy * uP_flowScale + vec2(flowT * 0.50, flowT * 0.85) + 7.1);
pos += (w - 0.5) * fAmp;
float r = length(pos) + 0.001;
float theta = acos(clamp(pos.y / r, -1.0, 1.0));
float phi = atan(pos.z, pos.x);
float a0 = 0.5;
float rho = 2.0 * r / (5.0 * a0);
float radial = pow(rho, radialPow) * exp(-rho / radialDecay);
float angular = pow(sin(theta), 3.0) * cos(phi + animTime * 0.2); // single lobe
float psi = radial * angular;
float probability = psi * psi;
// travelling spiral wave — the modulation moves across the surface instead
// of pulsing in place. The azimuthal harmonic count must be a whole number,
// else sin(phi * f) doesn't line up across the +/-PI wrap and leaves a
// vertical meridian seam. Snap it to the nearest integer.
float waveN = max(1.0, floor(waveFreq + 0.5));
float wavePhase = phi * waveN + theta * 2.5 - animTime * 2.0;
probability *= (0.85 + 0.15 * sin(wavePhase));
// drifting bright patches, like convection cells wandering the surface
float patches = fbm(pos.xy * 1.6 + vec2(flowT * 0.4, -flowT * 0.3));
probability *= 0.65 + 0.7 * patches;
probability = pow(probability, probPow) * probGain;
probability = clamp(probability, 0.0, 1.0);
float fresnel = pow(1.0 - z, 1.5);
// rainbow chromatic aberration
float chromaOffset = phi * 2.0 + theta * 1.5 + animTime * 0.3 + probability * 3.0;
vec3 rainbow;
rainbow.r = sin(chromaOffset) * 0.5 + 0.5;
rainbow.g = sin(chromaOffset + chromaSpread) * 0.5 + 0.5;
rainbow.b = sin(chromaOffset + chromaSpread * 2.0) * 0.5 + 0.5;
rainbow = normalize(rainbow + 0.01) * length(rainbow);
float bandFreq = chromaOffset * 3.0 + fresnel * 2.4;
vec3 chromaticBands;
chromaticBands.r = sin(bandFreq) * 0.5 + 0.5;
chromaticBands.g = sin(bandFreq + 2.094) * 0.5 + 0.5;
chromaticBands.b = sin(bandFreq + 4.189) * 0.5 + 0.5;
vec3 glowColor = mix(rainbow, chromaticBands, 0.12);
glowColor = pow(glowColor, vec3(0.8));
vec3 darkMetal = vec3(uP_metalDark);
vec3 lightMetal = mix(vec3(0.9, 0.92, 0.95), glowColor, 0.7);
float metalGradient = smoothstep(0.0, 1.0, probability * 0.7 + fresnel * 0.3);
vec3 metalColor = mix(darkMetal, lightMetal, metalGradient);
float orbGlow = uP_glow + 0.6 * uOutput;
float totalGlow = (0.25 + fresnel * 0.6 + probability * 0.8) * orbGlow;
float glowAmount = clamp(pow(totalGlow, 0.7), 0.0, 1.0);
vec3 surfaceColor = mix(metalColor, glowColor, glowAmount);
vec3 normal = vec3(uv / max(R, 0.001), z);
float specular = pow(max(dot(normal, normalize(vec3(1.0, 1.0, 2.0))), 0.0), 32.0);
surfaceColor += mix(vec3(1.0), glowColor, 0.6) * specular * 0.4;
float visibility = clamp(probability * 1.2 + fresnel * 0.3 + uP_baseVis + uInput * 0.15, 0.0, 1.0);
float a = mask * visibility;
gl_FragColor = vec4(surfaceColor * a, a);
}
`;
export const shdr11Orb: OrbVariant = {
key: "shdr-11",
label: "SHDR-11",
note: "quantum orbital, rainbow chroma",
frag: HYDROGEN_FRAG,
params: [
{ key: "speed", label: "Anim speed", min: 0.015, max: 10, step: 0.05, default: 0.9, integrate: true },
{ key: "rotSpeed", label: "Rotation speed", min: 0, max: 5, step: 0.05, default: 0.5 },
{ key: "radius", label: "Radius", min: 0.15, max: 3, step: 0.015, default: 0.9 },
{ key: "swell", label: "Input swell", min: 0, max: 1, step: 0.01, default: 0.07 },
{ key: "posScale", label: "Orbital zoom", min: 0.15, max: 10, step: 0.05, default: 0.5 },
{ key: "flowSpeed", label: "Flow speed", min: 0, max: 10, step: 0.05, default: 0.35, integrate: true },
{ key: "flowAmp", label: "Flow amount", min: 0, max: 4, step: 0.05, default: 0.45 },
{ key: "flowScale", label: "Flow scale", min: 0.3, max: 10, step: 0.1, default: 0.3 },
{ key: "precess", label: "Precession", min: 0, max: 4, step: 0.05, default: 0.3 },
{ key: "radialPow", label: "Radial power", min: 0.5, max: 15, step: 0.1, default: 0.5 },
{ key: "radialDecay", label: "Radial decay", min: 0.3, max: 30, step: 0.15, default: 1 },
{ key: "probPow", label: "Probability curve", min: 0.1, max: 3, step: 0.015, default: 0.4 },
{ key: "probGain", label: "Probability gain", min: 0.15, max: 15, step: 0.1, default: 3 },
{ key: "waveFreq", label: "Wave frequency", min: 0, max: 20, step: 0.5, default: 4 },
{ key: "chromaSpread", label: "Chroma spread", min: 0, max: 1.5, step: 0.01, default: 0.18 },
{ key: "glow", label: "Glow", min: 0, max: 5, step: 0.05, default: 0.9 },
{ key: "metalDark", label: "Metal darkness", min: 0, max: 3, step: 0.015, default: 0 },
{ key: "baseVis", label: "Base visibility", min: 0, max: 1.5, step: 0.01, default: 0.12 }
],
colors: [],
statePresets: {
// idle look, tuned by hand — the schema defaults mirror this set
idle: {
speed: 0.9,
rotSpeed: 0.5,
radius: 0.9,
swell: 0.07,
posScale: 0.5,
flowSpeed: 0.35,
flowAmp: 0.45,
flowScale: 0.3,
precess: 0.3,
radialPow: 0.5,
radialDecay: 1,
probPow: 0.4,
probGain: 3,
waveFreq: 4,
chromaSpread: 0.18,
glow: 0.9,
metalDark: 0,
baseVis: 0.12
},
// thinking: wider zoom, heavier flow, tighter shells, wide chroma —
// restless but not loud
thinking: {
speed: 0.9,
rotSpeed: 0.5,
radius: 0.9,
swell: 0.07,
posScale: 0.65,
flowSpeed: 0.35,
flowAmp: 1.1,
flowScale: 0.3,
precess: 0,
radialPow: 0.5,
radialDecay: 1.9,
probPow: 0.4,
probGain: 3,
waveFreq: 4,
chromaSpread: 0.41,
glow: 0.9,
metalDark: 0,
baseVis: 0.12
},
// speaking: fast anim, full zoom, quick fine-grained flow, strong
// precession, bright gain — the loudest, most energetic pattern
speaking: {
speed: 2.45,
rotSpeed: 0.5,
radius: 0.9,
swell: 0.07,
posScale: 1,
flowSpeed: 2.75,
flowAmp: 0.8,
flowScale: 2.2,
precess: 1.3,
radialPow: 0.5,
radialDecay: 1,
probPow: 0.31,
probGain: 4.3,
waveFreq: 4,
chromaSpread: 0.12,
glow: 0.9,
metalDark: 0,
baseVis: 0.12
}
}
};
export type Shdr11Props = Omit<ShaderOrbProps, "variant">;
export function Shdr11({ size = 280, ...rest }: Shdr11Props) {
return <ShaderOrb variant={shdr11Orb} size={size} {...rest} />;
}
export default Shdr11;
14. components/ui/shdr-12.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-12 — a ball built from glossy toy bricks, studs up.
THE ORB IS THE OBJECT: the sphere is assembled from interlocking plastic
bricks the way a brick-built globe is. A DDA march walks a rectilinear
lattice whose cells are one stud wide and one brick tall (bricks are 1.2
stud pitches high, the real proportion); a cell is solid wherever its
centre sits inside the unit sphere.
What sells the toy:
- BRICKWORK BONDING: bricks are 2x4 studs. Each layer alternates its
long axis, and every (layer, row) course staggers by a hashed offset —
so seams never align vertically, exactly like a proper build. A cell
maps to its owning brick analytically; the brick id drives colour,
mold variance, seams and the rebuild blink.
- STUDS: every upward face gets its stud embossed — a perturbed normal
around the stud rim, a lifted highlight on the cap and a contact ring
outside it. The lighting does the work; the silhouette stays brick.
- SEAMS: thin dark joints drawn only on true brick boundaries (the two
axes tangent to the struck face), not around every stud.
- PLASTIC: five tunable brick colours picked per brick — the patch
parameter slides the palette from per-brick confetti to big moulded
colour regions — with a white Blinn specular for the ABS sheen.
- REBUILD: bricks in the outer two courses blink out and back on an
integrated clock, revealing the darker bricks beneath. Idle loses an
occasional brick; THINKING churns the whole shell — the ball visibly
rebuilding itself — and speaking snaps it whole, fast and glossy.
Construction notes:
- The march is BOUNDED by an analytic sphere just past the brick
corners, so empty pixels cost two dot products. No noise runs in the
solid test — interior cells answer with one length() — which makes
this one of the cheapest marched orbs in the library.
- The DDA is anisotropic (cell height differs from pitch): the standard
Amanatides & Woo setup generalizes by using per-axis cell sizes.
- No fwidth, no round, constant loop bound with inner breaks —
GLSL ES 1.0 throughout, as everywhere in this repo.
- Surface-lit and hit-bounded, so alpha IS coverage — premultiplied
output (trivially: hits are opaque, misses are clear).
---------------------------------------------------------------------------- */
const BRICK_FRAG = `
#define STEPS 96
// Per-fragment constants, resolved once in main().
vec3 lgCell; // cell sizes: (stud pitch, brick height, stud pitch)
float lgGap;
// Brick-lookup results (GLSL ES 1.0 has no out-struct ergonomics).
vec3 lgBid; // unique id of the owning brick
float lgOff; // long-axis stagger offset of its course, in studs
float lgOrient; // 0: long axis runs along x, 1: along z
mat2 lgRot(float a) {
float c = cos(a);
float s = sin(a);
return mat2(c, -s, s, c);
}
/*
Which 2x4 brick owns this stud cell? Layers alternate their long axis
and every (layer, row) course staggers by a hashed offset — brickwork
bonding, so vertical seams never stack.
*/
void lgBrick(vec3 cellIdx) {
lgOrient = mod(cellIdx.y, 2.0);
float lc = lgOrient < 0.5 ? cellIdx.x : cellIdx.z;
float sc = lgOrient < 0.5 ? cellIdx.z : cellIdx.x;
float srow = floor(sc / 2.0);
lgOff = floor(hash(vec2(cellIdx.y * 3.17, srow * 7.31)) * 4.0);
lgBid = vec3(floor((lc + lgOff) / 4.0), cellIdx.y, srow + lgOrient * 913.0);
}
/*
The world function: inside the ball, minus bricks currently blinked out
of the outer two courses. Interior cells answer with a single length —
the brick lookup only runs in the shell.
*/
float lgSolid(vec3 cc) {
float r = length(cc);
if (r >= 1.0) return 0.0;
if (r > 1.0 - 2.2 * lgCell.y) {
lgBrick(floor(cc / lgCell));
float blink = fract(hash(lgBid.xy * 0.173 + lgBid.z * 0.089) + uP_rebuild * 0.03);
if (blink < lgGap) return 0.0; // this brick is off the build right now
}
return 1.0;
}
void main() {
// Volume coupling: agent output stokes the sheen and the gain; user
// input brightens the key light.
float glossNow = uP_gloss * (0.7 + 0.9 * uOutput);
float gainNow = uP_gain * (0.92 + 0.25 * uOutput);
float lightNow = uP_light * (1.0 + 0.3 * uInput);
float pitch = 2.0 / clamp(uP_studs, 8.0, 48.0);
lgCell = vec3(pitch, pitch * 1.2, pitch); // real brick proportion
lgGap = clamp(uP_gap, 0.0, 0.9);
float bound = 1.0 + length(lgCell) * 0.5 + 0.001;
vec2 uv = orbUV() / uP_radius;
vec3 ro = vec3(uv * bound, 2.6);
vec3 rd = vec3(0.0, 0.0, -1.0);
// rotate the RAY into object space (inverse tumble) — the lattice stays
// axis-aligned and the studs stay up while the ball turns. Light and
// view rotate along, keeping the sun fixed relative to the viewer.
mat2 tiltM = lgRot(uP_tilt); // positive tilt looks DOWN at the studs
mat2 spinM = lgRot(-uP_spin); // integrated clock
ro.yz = tiltM * ro.yz;
ro.xz = spinM * ro.xz;
rd.yz = tiltM * rd.yz;
rd.xz = spinM * rd.xz;
vec3 Lo = normalize(vec3(-0.5, 0.7, 0.55));
Lo.yz = tiltM * Lo.yz;
Lo.xz = spinM * Lo.xz;
vec3 Vo = vec3(0.0, 0.0, 1.0);
Vo.yz = tiltM * Vo.yz;
Vo.xz = spinM * Vo.xz;
// DDA needs nonzero direction components — nudge, keep the sign
vec3 sgn = vec3(
rd.x >= 0.0 ? 1.0 : -1.0,
rd.y >= 0.0 ? 1.0 : -1.0,
rd.z >= 0.0 ? 1.0 : -1.0
);
rd = normalize(sgn * max(abs(rd), vec3(1.0e-4)));
// analytic bounding sphere: empty pixels exit here
float b = dot(rd, ro);
float c = dot(ro, ro) - bound * bound;
float disc = b * b - c;
if (disc < 0.0) {
gl_FragColor = vec4(0.0);
return;
}
float sq = sqrt(disc);
vec3 p0 = ro + rd * (-b - sq + pitch * 0.001);
float tSpan = 2.0 * sq;
// Amanatides & Woo, anisotropic cells: per-axis sizes throughout
vec3 vp = floor(p0 / lgCell);
vec3 tDelta = lgCell / abs(rd);
vec3 tMax = ((vp + step(vec3(0.0), rd)) * lgCell - p0) / rd;
float hitF = 0.0;
vec3 mask = vec3(0.0, 0.0, 1.0); // first-voxel fallback: face the viewer
float tCur = 0.0;
for (int i = 0; i < STEPS; i++) {
if (lgSolid((vp + 0.5) * lgCell) > 0.5) {
hitF = 1.0;
break;
}
if (tMax.x < tMax.y && tMax.x < tMax.z) {
tCur = tMax.x;
tMax.x += tDelta.x;
vp.x += sgn.x;
mask = vec3(1.0, 0.0, 0.0);
} else if (tMax.y < tMax.z) {
tCur = tMax.y;
tMax.y += tDelta.y;
vp.y += sgn.y;
mask = vec3(0.0, 1.0, 0.0);
} else {
tCur = tMax.z;
tMax.z += tDelta.z;
vp.z += sgn.z;
mask = vec3(0.0, 0.0, 1.0);
}
if (tCur > tSpan) break; // left the bound: miss
}
if (hitF < 0.5) {
gl_FragColor = vec4(0.0);
return;
}
// the hit cell, its brick, and the struck face
vec3 cc = (vp + 0.5) * lgCell;
float r = length(cc);
vec3 dir = cc / max(r, 1.0e-4);
lgBrick(vp);
vec3 n = -mask * sgn;
vec3 hp = p0 + rd * tCur;
/*
Brick colour: a per-brick hash picks one of the five plastic colours.
The patch parameter slides the pick toward a smooth field over the
sphere, so 0 is per-brick confetti and 1 is big moulded colour
regions; the field is range-stretched so all five colours appear.
*/
float cph = hash(lgBid.xy * 1.37 + lgBid.z * 0.91);
float rn = noise(dir.xy * 2.6 + 7.0) * 0.5 + noise(dir.yz * 2.6 + 13.0) * 0.5;
rn = clamp(0.5 + (rn - 0.5) * 2.2, 0.0, 0.999);
float idx = floor(clamp(mix(cph, rn, clamp(uP_patch, 0.0, 1.0)), 0.0, 0.999) * 5.0);
vec3 albedo = idx < 0.5 ? uC_brickA
: (idx < 1.5 ? uC_brickB
: (idx < 2.5 ? uC_brickC
: (idx < 3.5 ? uC_brickD : uC_brickE)));
albedo *= 0.93 + 0.14 * hash(lgBid.xy * 0.53 + lgBid.z * 1.7); // mold variance
/*
Seams: distance to the nearest BRICK boundary along each lattice axis,
from the continuous within-brick coordinates. Only the two axes
tangent to the struck face draw — stud grid lines never do.
*/
vec3 sp = hp / lgCell;
float lcC = lgOrient < 0.5 ? sp.x : sp.z;
float scC = lgOrient < 0.5 ? sp.z : sp.x;
float u4 = fract((lcC + lgOff) / 4.0);
float v2 = fract(scC / 2.0);
float wY = fract(sp.y);
float dL = min(u4, 1.0 - u4) * 4.0 * pitch;
float dS = min(v2, 1.0 - v2) * 2.0 * pitch;
float dY = min(wY, 1.0 - wY) * lgCell.y;
float seamD;
if (mask.y > 0.5) seamD = min(dL, dS);
else if (mask.x > 0.5) seamD = min(dY, lgOrient < 0.5 ? dS : dL);
else seamD = min(dY, lgOrient < 0.5 ? dL : dS);
float seam = (1.0 - smoothstep(0.0, 0.07 * pitch, seamD)) * clamp(uP_seam, 0.0, 1.0);
/*
Studs, embossed the way the real brick photographs: the normal tilts
hard around the stud shoulder so the light wraps it like a cylinder
edge, the cap lifts, a contact shadow falls on the side facing away
from the light, and a faint ring engraved into the cap stands in for
the moulded logo.
*/
vec3 nEff = n;
float studF = 0.0;
float shadowF = 0.0;
float engrave = 0.0;
float studAmt = clamp(uP_stud, 0.0, 1.0);
if (mask.y > 0.5 && n.y > 0.5) {
vec2 cuv = fract(hp.xz / pitch) - 0.5;
float sd = length(cuv);
float rim = smoothstep(0.14, 0.29, sd) * (1.0 - smoothstep(0.29, 0.335, sd));
vec3 tiltN = normalize(vec3(cuv.x, 0.42, cuv.y));
nEff = normalize(mix(n, tiltN, rim * studAmt));
studF = 1.0 - smoothstep(0.285, 0.33, sd);
vec2 lxz = normalize(Lo.xz + vec2(1.0e-5));
float away = clamp(dot(normalize(cuv + vec2(1.0e-5)), -lxz), 0.0, 1.0);
shadowF = smoothstep(0.47, 0.335, sd) * (1.0 - studF) * (0.35 + 0.65 * away);
engrave = smoothstep(0.11, 0.135, sd) * (1.0 - smoothstep(0.155, 0.18, sd)) * studF;
}
// plastic shading: lambert + wrap for roundness + white Blinn sheen,
// dimmed toward the interior so revealed under-bricks read as inside
float lam = clamp(dot(nEff, Lo), 0.0, 1.0);
float wrap = clamp(dot(dir, Lo) * 0.5 + 0.5, 0.0, 1.0);
float depthDim = mix(1.0, 0.55, clamp((1.0 - r) / (3.0 * lgCell.y), 0.0, 1.0));
float shade = (0.34 + 0.42 * wrap * wrap + 0.8 * lam * lightNow) * depthDim;
vec3 col = albedo * shade * (1.0 + 0.1 * studF);
col *= 1.0 - shadowF * 0.38 * studAmt; // stud contact shadow
col *= 1.0 - engrave * 0.14 * studAmt; // moulded logo ring
// chamfered edge: a thin bright bevel line just inside the dark joint,
// catching the light the way the real brick's edges do
float bevel = smoothstep(0.05 * pitch, 0.085 * pitch, seamD)
* (1.0 - smoothstep(0.085 * pitch, 0.16 * pitch, seamD));
col += albedo * bevel * (0.18 + 0.5 * lam) * clamp(uP_seam, 0.0, 1.0);
col *= 1.0 - seam * 0.8; // dark joints
// two-lobe plastic sheen: a sharp hotspot over a broad soft gloss
float ndh = clamp(dot(nEff, normalize(Lo + Vo)), 0.0, 1.0);
float spec = pow(ndh, 48.0) + 0.22 * pow(ndh, 8.0);
col += vec3(1.0) * spec * glossNow * (1.0 - seam) * depthDim;
col *= gainNow;
col = pow(max(col, 0.0), vec3(uP_contrast));
// Surface-lit orb bounded by the hit test: alpha IS coverage, and a hit
// is fully opaque — premultiplied output, trivially (see shdr-28).
gl_FragColor = vec4(col, 1.0);
}
`;
export const shdr12Orb: OrbVariant = {
key: "shdr-12",
label: "SHDR-12",
note: "a ball of glossy toy bricks, studs up — it rebuilds itself while it thinks",
frag: BRICK_FRAG,
params: [
{ key: "spin", label: "Spin", min: 0, max: 5, step: 0.03, default: 0.25, integrate: true },
{ key: "tilt", label: "Tilt", min: 0, max: 4, step: 0.02, default: 0.55 },
{ key: "rebuild", label: "Rebuild rate", min: 0, max: 20, step: 0.1, default: 0.4, integrate: true },
{ key: "gap", label: "Missing bricks", min: 0, max: 0.8, step: 0.01, default: 0.07 },
{ key: "studs", label: "Studs", min: 8, max: 48, step: 1, default: 18 },
{ key: "radius", label: "Radius", min: 0.15, max: 3, step: 0.015, default: 0.95 },
{ key: "patch", label: "Colour patches", min: 0, max: 1, step: 0.01, default: 0.35 },
{ key: "stud", label: "Stud relief", min: 0, max: 1, step: 0.01, default: 0.85 },
{ key: "seam", label: "Seams", min: 0, max: 1, step: 0.01, default: 0.6 },
{ key: "gloss", label: "Gloss", min: 0, max: 3, step: 0.02, default: 1 },
{ key: "light", label: "Key light", min: 0, max: 3, step: 0.015, default: 1 },
{ key: "gain", label: "Gain", min: 0.05, max: 5, step: 0.05, default: 1 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 10, step: 0.05, default: 1 }
],
colors: [
{ key: "brickA", label: "Red", default: "#c4281c" },
{ key: "brickB", label: "Yellow", default: "#f2cd37" },
{ key: "brickC", label: "Blue", default: "#1e5aa8" },
{ key: "brickD", label: "Green", default: "#00852b" },
{ key: "brickE", label: "White", default: "#f4f4f4" }
],
/*
The rebuild blink is the state read:
idle SETTLES lazy tumble, an occasional brick popped off the shell
thinking BUILDS the tumble all but stops while the outer courses
churn — bricks blinking out and back everywhere,
the ball visibly rebuilding itself
speaking SNAPS whole and glossy: the gaps close, the sheen flares,
and the ball turns fast to answer
Two controls are deliberately NOT staged, and both for the same reason:
they quantize, so easing them steps instead of fading. STUDS sets the
grid pitch, and a gliding pitch re-tiles the whole shell — the ball
reads as inflating rather than changing mood. COLOUR PATCHES lands
inside a floor() that picks one of the five brick colours, so easing it
flips bricks between colours one at a time, which reads as a fault
rather than a transition. Everything staged below is either an
amplitude or one of the two integrated clocks, whose rate can change
without their phase ever jumping.
The surface finish carries as much of the read as the blink does:
searching wears full stud relief and hard seams on a matt gloss — every
brick edge visible, an object mid-assembly — and answering flattens the
studs, sinks the seams and flares the sheen, so it resolves into one
moulded piece.
*/
statePresets: {
idle: {
spin: 0.25,
rebuild: 0.4,
gap: 0.07,
gloss: 1,
gain: 1,
light: 1
},
thinking: {
spin: 0.03,
tilt: 0.9,
rebuild: 9,
gap: 0.55,
stud: 1,
seam: 0.95,
gloss: 0.55,
gain: 1.05,
light: 1.15,
contrast: 1.05
},
speaking: {
spin: 1.6,
tilt: 0.42,
rebuild: 0.5,
gap: 0,
stud: 0.6,
seam: 0.3,
gloss: 2.4,
gain: 1.25,
light: 1.35,
contrast: 0.95
}
},
/*
Answering swaps the whole box out. Resting and searching keep the
classic five above — that palette is the joke, and it should be what
the orb looks like most of the time — but the state that snaps whole
and glossy gets a hot set to snap INTO: the same five slots, pushed to
high chroma, so the sheen at gloss 2.4 has something saturated to sit
on rather than a flat primary.
Safe to stage, unlike the patch control that picks between these. Each
brick keeps its slot through the change and only the colour in that
slot eases, so the shell cross-fades where flipping bricks between
slots would pop. Omitting idle and thinking is deliberate: an unlisted
state falls back to the colour defaults, which is exactly the classic
palette.
*/
stateColors: {
speaking: {
brickA: "#ff3b6b",
brickB: "#ffc93c",
brickC: "#21d4fd",
brickD: "#7af5a0",
brickE: "#ffffff"
}
}
};
export type Shdr12Props = Omit<ShaderOrbProps, "variant">;
export function Shdr12({ size = 280, ...rest }: Shdr12Props) {
return <ShaderOrb variant={shdr12Orb} size={size} {...rest} />;
}
export default Shdr12;
15. components/ui/shdr-13.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-13 — a plasma globe: crawling lightning filaments inside a glass ball.
The reference is the physical object: a tesla ball with a hot nucleus,
pink-to-violet streamers writhing out to the glass, and bright flares
where a filament touches the shell.
How the filaments are built, and why they behave like the real thing:
- A curve in 3D is the intersection of two level surfaces. Two independent
trig fields are evaluated on the DIRECTION of each march point
(q = dir * fils, |dir| = 1), so their joint zero set is a set of
directions — extruded radially, those are filaments running from the
nucleus to the glass, never a floating blob. Filament count rides the
angular frequency uP_fils, so a state preset can literally grow more
streamers, and because the field morphs smoothly the new ones split off
from existing ones instead of popping in.
- Each field carries its own additive time phases, so the zero curves
drift, merge, and reconnect — the crawl of a real streamer hunting
across the glass. The whole array also precesses on an integrated spin
clock.
- A radial writhe term bends the direction before sampling, gated by
smoothstep from the centre so every filament stays ROOTED at the
nucleus while its far end wanders on the shell.
- Proximity to the curve is measured in field space (f1^2 + f2^2), and
brightness is its inverse — the same 1/d accumulation as the other
volumetric orbs, so thickness varies naturally along a filament with
the field gradient.
- The march is bounded to the exact ray/sphere chord (entry to exit), and
each step is weighted by its true length, so limb rays integrate short
chords and dim correctly; the tip flare (a smoothstep in r near the
shell) is what lights the glass from inside where streamers land.
- Colour is a radial mix: the inner colour near the nucleus, the arc
colour toward the glass, cores whitened by their own intensity — the
pink-core / violet-tip gradient of a real discharge.
House rules followed from the other orbs: per-step clamp before the
spikes own the frame, front-to-back transmittance, tanh tone map with an
exposure knee, alpha from the brightest channel, and the analytic
silhouette cut from each ray's closest approach — colour AND alpha.
---------------------------------------------------------------------------- */
/*
* Step count is a `#define`: ES 1.0 requires constant loop bounds.
*/
const ION_FRAG = `
#define STEPS 64
// Volume-reactive values, resolved once per fragment in main().
float ionSharp;
float ionWrithe;
float ionCore;
float ionExposure;
float ionRadius;
mat2 ionRot2(float a) {
float c = cos(a);
float s = sin(a);
return mat2(c, -s, s, c);
}
vec3 ionRender(vec2 fragCoord) {
float t = uP_speed; // integrated clock: filament crawl
float spinAng = uP_spin; // integrated clock: array precession
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
vec3 ro = vec3(0.0, 0.0, uP_camDist);
vec3 rd = normalize(vec3(uv, -uP_focal));
// exact ray/sphere chord — the march never leaves the globe, so no
// envelope fade is needed and every step length is meaningful
float proj = dot(-ro, rd);
float b2 = dot(ro, ro) - proj * proj;
float half_ = sqrt(max(ionRadius * ionRadius - b2, 0.0));
float zNear = proj - half_;
float stepLen = 2.0 * half_ / float(STEPS);
// per-pixel jitter of the march start: a filament grazed at a shallow
// angle is crossed periodically by the fixed step grid and renders as a
// dotted chain — the jitter decorrelates neighbouring rays and melts the
// dots into plasma grain
zNear += (hash(fragCoord) - 0.5) * stepLen;
vec3 acc = vec3(0.0);
float T = 1.0;
for (int i = 0; i < STEPS; i++) {
vec3 p = ro + rd * (zNear + (float(i) + 0.5) * stepLen);
// precess the whole filament array; a static tilt keeps the spin axis
// off-vertical so the motion reads in 3D
vec3 pr = p;
pr.xz = ionRot2(spinAng) * pr.xz;
pr.yz = ionRot2(uP_tilt) * pr.yz;
float r = length(pr);
vec3 dir = pr / max(r, 1e-4);
float rr = r / max(ionRadius, 1e-3);
// writhe: bend the sampling direction with radius and time, rooted at
// the nucleus by the smoothstep so filaments stay attached
float wr = ionWrithe * smoothstep(0.0, ionRadius * 0.35, r);
vec3 q = dir * uP_fils;
q += wr * vec3(
sin(r * uP_writheFreq - t * 1.2 + q.y * 1.8),
sin(r * uP_writheFreq * 0.83 + t * 1.0 + q.z * 1.8),
sin(r * uP_writheFreq * 1.19 - t * 0.7 + q.x * 1.8));
// two independent fields over the direction sphere; their joint zero
// set is the filament curves. Time enters as additive phase only.
float f1 = sin(q.x + t * 0.70)
+ sin(q.y * 1.31 - t * 0.50)
+ sin(q.z * 1.13 + t * 0.90);
float f2 = sin(q.y * 1.21 + t * 0.60 + 1.7)
+ sin(q.z * 1.43 - t * 0.80 + 3.1)
+ sin(q.x * 0.87 + t * 0.40 + 5.0);
float d2 = f1 * f1 + f2 * f2;
float g = 1.0 / (d2 * ionSharp + uP_soft);
// flare where a streamer lands on the glass, and the hot nucleus
g *= 1.0 + uP_tipGain * smoothstep(0.55, 0.95, rr);
float core = ionCore / (r * r * 8.0 + 0.05);
// pink near the nucleus, violet-blue at the glass, cores whitened by
// their own intensity
vec3 fCol = mix(uC_inner, uC_arc, smoothstep(0.1, 0.75, rr));
vec3 w = (fCol + vec3(uP_whiten) * g) * g + uC_inner * core + vec3(uP_fill);
w = min(w, vec3(uP_stepClamp));
w *= stepLen; // length-fair: limb chords are short and dim correctly
acc += T * w;
T *= exp(-dot(w, vec3(0.299, 0.587, 0.114)) * uP_scatter);
if (T < 0.004) break;
}
return acc;
}
void main() {
// Louder agent output softens and thickens the arcs and quickens the
// writhe; user input flares the nucleus — the globe answers being spoken
// to the way the real toy answers a fingertip.
ionSharp = uP_sharp * (1.0 - 0.25 * uOutput);
ionWrithe = uP_writhe * (1.0 + 0.6 * uOutput);
ionCore = uP_coreGain * (1.0 + 1.6 * uInput + 0.4 * uOutput);
ionExposure = uP_exposure * (1.0 - 0.35 * uOutput);
ionRadius = uP_envRadius + uP_swell * uInput;
vec3 acc = ionRender(gl_FragCoord.xy);
// tanh tone map with a tunable knee, then the usual finishing chain
vec3 col = tanh3(acc / max(ionExposure, 0.01));
col = pow(clamp(col, 0.0, 1.0), vec3(uP_contrast));
float lum = dot(col, vec3(0.299, 0.587, 0.114));
col = mix(vec3(lum), col, uP_saturation);
col *= uC_tint;
// alpha from the brightest channel — a saturated violet streamer has low
// luminance but must not go transparent
float peak = max(col.r, max(col.g, col.b));
float a = clamp(peak * uP_alphaGain, 0.0, 1.0);
// analytic silhouette, identical construction to shdr-01: exact
// ray-to-centre distance against the radius, colour AND alpha
vec3 mrd = normalize(vec3(orbUV(), -uP_focal));
float closest = length(cross(vec3(0.0, 0.0, uP_camDist), mrd));
float band = mix(0.35, 0.012, clamp(uP_edge, 0.0, 1.0));
float mask = 1.0 - smoothstep(ionRadius * (1.0 - band), ionRadius * 1.005, closest);
col *= mask;
a *= mask;
// Emitted light, so rgb is already premultiplied — do NOT scale by alpha
// again (see the same note in shdr-31).
gl_FragColor = vec4(col, a);
}
`;
export const shdr13Orb: OrbVariant = {
key: "shdr-13",
label: "SHDR-13",
note: "plasma globe: crawling lightning filaments",
frag: ION_FRAG,
params: [
{ key: "speed", label: "Anim speed", min: 0.015, max: 10, step: 0.05, default: 1, integrate: true },
{ key: "spin", label: "Spin rate", min: 0, max: 5, step: 0.03, default: 0.2, integrate: true },
{ key: "camDist", label: "Camera distance", min: 1, max: 50, step: 0.3, default: 7 },
{ key: "focal", label: "Lens", min: 0.15, max: 15, step: 0.1, default: 2.25 },
{ key: "envRadius", label: "Globe radius", min: 0.15, max: 15, step: 0.1, default: 2.6 },
{ key: "swell", label: "Input swell", min: 0, max: 1, step: 0.01, default: 0.15 },
{ key: "tilt", label: "Axis tilt", min: 0, max: 4, step: 0.02, default: 0.4 },
{ key: "fils", label: "Filament density", min: 0.5, max: 12, step: 0.1, default: 6 },
{ key: "writhe", label: "Writhe", min: 0, max: 3, step: 0.02, default: 0.9 },
{ key: "writheFreq", label: "Writhe frequency", min: 0.2, max: 8, step: 0.05, default: 1.6 },
{ key: "sharp", label: "Arc sharpness", min: 0.5, max: 60, step: 0.5, default: 4 },
{ key: "soft", label: "Arc core softness", min: 0.002, max: 0.5, step: 0.002, default: 0.06 },
{ key: "whiten", label: "Core whitening", min: 0, max: 0.2, step: 0.002, default: 0.008 },
{ key: "coreGain", label: "Nucleus glow", min: 0, max: 5, step: 0.05, default: 1.6 },
{ key: "tipGain", label: "Glass flare", min: 0, max: 6, step: 0.05, default: 1.8 },
{ key: "fill", label: "Body haze", min: 0, max: 2, step: 0.01, default: 0.02 },
{ key: "stepClamp", label: "Step clamp", min: 0.3, max: 300, step: 1.5, default: 40 },
{ key: "scatter", label: "Diffusion", min: 0, max: 0.5, step: 0.003, default: 0.012 },
{ key: "exposure", label: "Exposure", min: 0.1, max: 200, step: 0.5, default: 11 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 15, step: 0.1, default: 1 },
{ key: "saturation", label: "Saturation", min: 0, max: 4, step: 0.02, default: 1.2 },
{ key: "alphaGain", label: "Alpha gain", min: 0.05, max: 15, step: 0.1, default: 2.5 },
{ key: "edge", label: "Edge sharpness", min: 0, max: 1, step: 0.01, default: 1 }
],
colors: [
{ key: "inner", label: "Nucleus", default: "#ff70d8" },
{ key: "arc", label: "Arc", default: "#5a5cff" },
{ key: "tint", label: "Tint", default: "#ffffff" }
],
statePresets: {
// a full globe of swooping arcs around a hot nucleus
idle: {
speed: 1,
spin: 0.2,
fils: 6,
writhe: 0.9,
sharp: 4,
soft: 0.06,
whiten: 0.008,
tipGain: 1.8,
coreGain: 1.6,
exposure: 11
},
// hunting: even more filaments, softer and more nebular, restless writhe
thinking: {
speed: 1.8,
spin: 0.45,
fils: 7,
writhe: 1.2,
sharp: 3,
soft: 0.08,
whiten: 0.012,
tipGain: 1.5,
coreGain: 1.2,
exposure: 10
},
// discharge: the densest, brightest state — spiky arcs flaring hard on
// the glass around a blazing core
speaking: {
speed: 2.6,
spin: 0.3,
fils: 8,
writhe: 1.3,
sharp: 3.5,
soft: 0.05,
whiten: 0.012,
tipGain: 2.4,
coreGain: 2,
exposure: 8.5
}
}
};
export type Shdr13Props = Omit<ShaderOrbProps, "variant">;
export function Shdr13({ size = 280, ...rest }: Shdr13Props) {
return <ShaderOrb variant={shdr13Orb} size={size} {...rest} />;
}
export default Shdr13;
16. components/ui/shdr-14.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-14 — demoscene sine-plasma on a rolling dome, quantized to chunky
two-tone pixels.
The one retro renderer in the family, and it animates like one: the
luminance is the CLASSIC demo plasma — three interfering sine waves —
evaluated on the sphere's rotating dome point so the wavefronts roll
around the ball, plus a ripple source that orbits the dome and pushes
expanding rings through the interference. A lambert term keeps the ball
solid and a fresnel rim edges it. All of it collapses into one luminance,
and an 8x8 ORDERED BAYER DITHER snaps that onto a short tone ladder
between two colours, ink and paper. At 2 tone steps it is the classic
1-bit look.
Construction notes:
- The Bayer matrix is generated PROCEDURALLY. GLSL ES 1.0 has no array
initializer lists and no bitwise operators, so the usual lookup-table
and bit-interleave constructions are both unavailable. The fract() form
below produces the exact 2x2 base matrix, and the recursion
M(2n) = M(n)/4 + M(2) builds 8x8 from it.
- The content is sampled at each CELL's centre, not per fragment, so the
dots are crisp squares — including the silhouette, which goes blocky at
the rim on purpose. Averaging per fragment would anti-alias the grid
away and leave gray mush.
- The dither threshold is per screen cell and static: ordered dither does
not crawl. All motion lives in the content underneath it.
- Clocks enter only as additive phase (plasma drift) or through cos/sin
(light orbit) — integrated clocks, phase-safe as everywhere here.
- Surface-lit and mask-bounded, so alpha IS coverage — premultiplied
output, as in shdr-28.
---------------------------------------------------------------------------- */
const DITHER_FRAG = `
// 2x2 Bayer base: floor/fract only. (0,0)=0, (1,0)=.5, (0,1)=.75, (1,1)=.25
// — the 0,2,3,1 ordering over 4.
float bayer2(vec2 a) {
a = floor(a);
return fract(a.x / 2.0 + a.y * a.y * 0.75);
}
// 8x8 by recursion: M8 = M2(a/4)/16 + M2(a/2)/4 + M2(a). No arrays, no
// bitwise — neither exists in GLSL ES 1.0.
float bayer8(vec2 a) {
return bayer2(a * 0.25) * 0.0625 + bayer2(a * 0.5) * 0.25 + bayer2(a);
}
void main() {
// Volume coupling: user input deepens the waves, agent output brightens
// the whole tone ladder — the dot field visibly blooms while it speaks.
float plasmaAmt = uP_plasma * (1.0 + 0.4 * uInput);
float gainNow = uP_gain * (0.85 + 0.5 * uOutput);
/*
Chunky pixel grid, RESOLUTION-RELATIVE: uP_cells is how many cells span
the canvas, so a 190px gallery card and a 420px playground orb show the
same composition — the same wave resolved by the same number of dots.
Sized in device pixels instead, small canvases collapse to a few dozen
blotches. All content below samples at the cell centre so every dot is
one flat square.
*/
float cellPx = max(min(uRes.x, uRes.y) / max(uP_cells, 8.0), 1.0);
vec2 pix = floor(gl_FragCoord.xy / cellPx);
vec2 cellCentre = (pix + 0.5) * cellPx;
vec2 suv = (2.0 * cellCentre - uRes) / min(uRes.x, uRes.y);
vec2 uv = suv / uP_radius;
float r2 = dot(uv, uv);
// blocky silhouette — cut on the cell grid, deliberately not smoothed
float mask = 1.0 - step(1.0, r2);
float z = sqrt(max(1.0 - r2, 0.0));
vec3 n = vec3(uv, z);
/*
The plasma is evaluated in a ROTATING frame: the dome point spins about
Y on its own integrated clock, so the wavefronts roll around the ball
instead of sliding across a flat disc. The light stays screen-fixed —
the form shading holds still while the pattern travels over it.
*/
float rot = uP_spin; // integrated clock
float cr = cos(rot);
float sr = sin(rot);
vec3 sp = vec3(n.x * cr - n.z * sr, n.y, n.x * sr + n.z * cr);
float t = uP_speed; // integrated clock
// the classic demoscene plasma: three interfering sine waves, each on its
// own direction and rate
float f = uP_scale;
float v = sin(sp.x * f * 3.1 + t)
+ sin((sp.y * 0.85 + sp.z * 0.4) * f * 3.6 - t * 1.3)
+ sin((sp.x + sp.y + sp.z) * f * 2.2 + t * 0.7);
// a ripple source orbiting the dome — expanding rings pushed through the
// interference; the clock enters only as additive phase
vec2 src = 0.55 * vec2(cos(t * 0.5), sin(t * 0.5));
v += sin(length(uv - src) * f * 5.0 - t * 2.2);
v *= 0.25; // four unit waves back to -1..1
float lambert = clamp(dot(n, normalize(vec3(-0.45, 0.55, 0.7))), 0.0, 1.0);
float fres = pow(1.0 - z, 2.0);
// waves modulated by the dome shading, so the ball stays a ball under
// the rolling pattern; everything collapses into one luminance
float lum = (0.5 + 0.5 * v * plasmaAmt) * (0.3 + uP_light * lambert)
+ uP_rim * fres;
lum = pow(clamp(lum * gainNow, 0.0, 1.0), uP_contrast);
// ordered dither onto the tone ladder — levels 2 is the classic 1-bit
// look, higher values keep the grain but add mid-tones
float steps = max(uP_levels - 1.0, 1.0);
float q = clamp(floor(lum * steps + bayer8(pix)) / steps, 0.0, 1.0);
vec3 col = mix(uC_ink, uC_paper, q);
// Surface-lit orb bounded by a mask: alpha IS coverage, so premultiply —
// the opposite convention from the emissive orbs (see shdr-31).
float a = mask;
gl_FragColor = vec4(col * a, a);
}
`;
export const shdr14Orb: OrbVariant = {
key: "shdr-14",
label: "SHDR-14",
note: "a lit plasma dome quantized to chunky two-tone pixels",
frag: DITHER_FRAG,
params: [
{ key: "speed", label: "Wave speed", min: 0.015, max: 10, step: 0.05, default: 0.5, integrate: true },
{ key: "spin", label: "Roll", min: 0, max: 5, step: 0.03, default: 0.15, integrate: true },
{ key: "radius", label: "Radius", min: 0.15, max: 3, step: 0.015, default: 0.9 },
{ key: "cells", label: "Grid cells", min: 32, max: 320, step: 2, default: 140 },
{ key: "levels", label: "Tone steps", min: 2, max: 8, step: 1, default: 3 },
{ key: "scale", label: "Wave scale", min: 0.3, max: 12, step: 0.1, default: 1.5 },
{ key: "plasma", label: "Wave amount", min: 0, max: 3, step: 0.015, default: 0.9 },
{ key: "light", label: "Key light", min: 0, max: 3, step: 0.015, default: 0.9 },
{ key: "rim", label: "Rim light", min: 0, max: 3, step: 0.015, default: 0.35 },
{ key: "gain", label: "Brightness", min: 0.05, max: 5, step: 0.05, default: 1 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 10, step: 0.05, default: 1.1 }
],
colors: [
{ key: "ink", label: "Ink", default: "#101426" },
{ key: "paper", label: "Paper", default: "#cfe6ff" }
],
/*
Staged in the family language: thinking churns the plasma in place while
the light freezes, speaking sweeps the light fast and brightens the
ladder. `pixel` and `levels` never move between states — both quantize,
and a gliding quantizer pops instead of fading.
*/
statePresets: {
// calm: waves rolling slowly, dome barely turning
idle: {
speed: 0.5,
spin: 0.15,
plasma: 0.9,
gain: 1,
contrast: 1.1
},
// computing: the interference races IN PLACE — wave clock at three
// times idle, deeper waves — while the dome stops turning
thinking: {
speed: 1.6,
spin: 0.05,
plasma: 1.15,
gain: 0.95,
contrast: 1.15
},
// answering: the whole dome rolls fast and the tones bloom bright
speaking: {
speed: 1.3,
spin: 0.8,
plasma: 1,
gain: 1.3,
contrast: 1.05
}
},
// ink/paper carry the at-a-glance read: cool print at rest, violet-blue
// while computing, warm amber while answering
stateColors: {
idle: { ink: "#101426", paper: "#cfe6ff" },
thinking: { ink: "#140f38", paper: "#a9b9ff" },
speaking: { ink: "#2a1410", paper: "#ffd9a4" }
}
};
export type Shdr14Props = Omit<ShaderOrbProps, "variant">;
export function Shdr14({ size = 280, ...rest }: Shdr14Props) {
return <ShaderOrb variant={shdr14Orb} size={size} {...rest} />;
}
export default Shdr14;
17. components/ui/shdr-15.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-15 — an iridescent particle-track web, worn as the ball's own skin.
Ported from a golfed twigl listing:
for(float i,z,d,s;i++<1e1;o+=(cos(z/.03+t+vec4(0,2,3,0))+1.)/d/s){
vec3 p=z*normalize(FC.rgb*2.-r.xyy),
a=normalize(cos(vec3(7,1,0)+t-s));
p.z+=9.,a=a*dot(a,p)-cross(a,p);
for(d=1.;d++<9.;)a+=sin(a*d+t).yzx/d;
z+=d=.03*abs(sin(s=length(a)));}
o=tanh(o/3e3);
What it actually is, decoded:
- TEN steps, each at most 0.03 long: the whole march is a MICRO-SLAB a
third of a unit thick. This is not a volume, it is ten nested layers
of one interference pattern.
- d = .03*abs(sin(length(a))) sticks the march wherever the turbulent
field magnitude sits on a multiple of pi — concentric shells in field
space. The 1/d weight genuinely reaches infinity there; tanh eats it,
and those near-zeros ARE the bright web cores.
- a*dot(a,p) - cross(a,p) is the minus-90-degree twin of shdr-22'
exact Rodrigues rotation. The axis cos((7,1,0)+t-s) carries FEEDBACK:
s is last step's field magnitude, so every layer takes a differently
jittered axis and the ten layers interfere.
- cos(z/.03 + t + (0,2,3)) cycles the palette once per layer or so —
the iridescent banding.
- p.z += 9 puts the camera OUTSIDE, like shdr-22.
Port decisions, each one a documented trap or rule in the README:
- THE ORB IS THE OBJECT: the micro-slab is anchored to the sphere
ANALYTICALLY — each ray starts its ten tiny steps at its own
sphere-entry point, so the slab follows the ball's curve and the web
becomes the orb's actual skin, wrapping the limb for free. The depth
hue uses (z - entry), turning layer banding into skin iridescence.
- The golfed listing relies on i, z, d, s starting at zero —
uninitialised locals are UNDEFINED in GLSL ES 1.0, explicit here.
- The 1/d spike is clamped and the clamped weight is normalized back to
family units, exactly as in shdr-22 (the golfed knee here is 3e3)
— so stepClamp stays a pure dynamic-range knob.
- Clocks enter only as additive phase; the axis gets its own integrated
clock so its wander rate tunes without jumping the web.
---------------------------------------------------------------------------- */
/*
* Step counts are `#define`s: ES 1.0 requires constant loop bounds.
* TURB is 8 to match the original's octaves (d = 2..9).
*/
const MUONS_FRAG = `
#define STEPS 10
#define TURB 8
#define AA 1
// Volume-reactive values, resolved once per fragment in main().
float muonsTurb;
float muonsExposure;
vec3 muonsRender(vec2 fragCoord) {
float animTime = uP_speed; // integrated clock: weave + hue phase
float wander = uP_wander; // integrated clock: axis drift
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
vec3 ro = vec3(0.0, 0.0, uP_camDist);
vec3 rd = normalize(vec3(uv, -uP_focal));
/*
Anchor the micro-slab to the ball: intersect the ray with the shell
analytically and start the ten steps AT the entry point, so the slab
hugs the sphere's curve. Rays that miss fall back to their closest
approach — the envelope and silhouette cut them anyway.
*/
float proj = dot(-ro, rd);
float b2 = dot(ro, ro) - proj * proj;
float R = uP_envRadius * 0.96;
float entry = proj - sqrt(max(R * R - b2, 0.0));
vec3 acc = vec3(0.0);
float T = 1.0;
float z = entry;
float s = 0.0;
for (int it = 0; it < STEPS; it++) {
vec3 p = ro + rd * z;
// shell points scaled into field space — the original worked around
// magnitude 9, and the ring density rides on that magnitude
vec3 q = p * uP_fieldScale;
// the per-layer axis, with the original's s feedback — each of the
// ten layers takes a differently jittered axis
vec3 axis = normalize(cos(vec3(7.0, 1.0, 0.0) + wander - s));
// the minus-90-degree Rodrigues twin of shdr-22
vec3 a = axis * dot(axis, q) - cross(axis, q);
for (int j = 0; j < TURB; j++) {
float dj = float(j) + 2.0;
a += muonsTurb * sin(a * dj + animTime).yzx / dj;
}
// the shells: the march sticks where the field magnitude sits on a
// multiple of pi, and 1/d blows up — that is the web
s = length(a);
float d = uP_stepScale * abs(sin(s));
d = max(d, 1e-5);
z += d;
/*
Layer-cycled palette, with the depth measured from the ENTRY point
so the banding follows the ball's skin. Clamp then normalize to
family units, as in shdr-22 — the raw spikes run to 1/1e-5.
*/
vec3 w = (cos((z - entry) / max(uP_stepScale, 1e-3) + animTime + vec3(0.0, 2.0, 3.0) * uP_disperse) + 1.0)
/ d / max(s, 0.5);
w = min(w, vec3(uP_stepClamp));
w *= 20.0 / max(uP_stepClamp, 1.0);
// envelope: plateau through the ball, cut 12% past the radius so the
// analytic silhouette in main() still has emission left to cut
float env = smoothstep(uP_envRadius * 1.12, uP_envRadius * uP_envCore, length(p));
w = (w + uP_fill) * env;
acc += T * w;
T *= exp(-dot(w, vec3(0.299, 0.587, 0.114)) * uP_scatter);
if (T < 0.004) break;
}
return acc;
}
void main() {
muonsTurb = uP_turb * (1.0 + 0.5 * uInput);
muonsExposure = uP_exposure * (1.0 - 0.35 * uOutput);
vec3 acc = vec3(0.0);
#if AA > 1
for (int mx = 0; mx < AA; mx++) {
for (int my = 0; my < AA; my++) {
vec2 offset = vec2(float(mx), float(my)) / float(AA) - 0.5;
acc += muonsRender(gl_FragCoord.xy + offset);
}
}
acc /= float(AA * AA);
#else
acc = muonsRender(gl_FragCoord.xy);
#endif
// tanh tone map per channel — the golfed /3e3 knee is a tunable here
vec3 col = tanh3(acc / max(muonsExposure, 1.0));
col = pow(clamp(col, 0.0, 1.0), vec3(uP_contrast));
// saturation about luminance, then the tint
float lum = dot(col, vec3(0.299, 0.587, 0.114));
col = mix(vec3(lum), col, uP_saturation);
col *= uC_tint;
// alpha from the brightest channel, not luminance — a saturated violet
// thread has low luminance but must not go transparent
float peak = max(col.r, max(col.g, col.b));
float a = clamp(peak * uP_alphaGain, 0.0, 1.0);
// Analytic silhouette — identical construction to shdr-01: exact
// ray-to-centre distance against the radius, colour AND alpha.
vec3 mrd = normalize(vec3(orbUV(), -uP_focal));
float closest = length(cross(vec3(0.0, 0.0, uP_camDist), mrd));
float band = mix(0.35, 0.012, clamp(uP_edge, 0.0, 1.0));
float mask = 1.0 - smoothstep(uP_envRadius * (1.0 - band), uP_envRadius * 1.005, closest);
col *= mask;
a *= mask;
// safety taper at the frame boundary — colour as well as alpha
float r2d = length(orbUV());
float fade = 1.0 - smoothstep(uP_edgeFade, 1.0, r2d);
col *= fade;
a *= fade;
// Emitted light, so rgb is already premultiplied — do NOT scale by alpha
// again (see the same note in shdr-31).
gl_FragColor = vec4(col, a);
}
`;
export const shdr15Orb: OrbVariant = {
key: "shdr-15",
label: "SHDR-15",
note: "an iridescent particle-track web worn as the ball's skin",
frag: MUONS_FRAG,
params: [
{ key: "speed", label: "Anim speed", min: 0.015, max: 10, step: 0.05, default: 0.5, integrate: true },
{ key: "wander", label: "Axis wander", min: 0, max: 3, step: 0.015, default: 0.15, integrate: true },
{ key: "camDist", label: "Camera distance", min: 1, max: 50, step: 0.3, default: 7 },
{ key: "focal", label: "Lens", min: 0.15, max: 15, step: 0.1, default: 2.25 },
{ key: "fieldScale", label: "Web density", min: 1, max: 20, step: 0.1, default: 2.4 },
{ key: "turb", label: "Weave", min: 0, max: 5, step: 0.03, default: 0.8 },
{ key: "stepScale", label: "Skin depth", min: 0.0015, max: 0.4, step: 0.005, default: 0.015 },
{ key: "disperse", label: "Dispersion", min: 0, max: 5, step: 0.03, default: 1 },
{ key: "envRadius", label: "Envelope radius", min: 0.15, max: 15, step: 0.1, default: 2.6 },
{ key: "envCore", label: "Envelope core", min: 0.3, max: 1.02, step: 0.01, default: 1 },
{ key: "fill", label: "Body fill", min: 0, max: 100, step: 0.3, default: 0.3 },
{ key: "stepClamp", label: "Step clamp", min: 3, max: 5000, step: 30, default: 150 },
{ key: "scatter", label: "Diffusion", min: 0, max: 0.5, step: 0.003, default: 0.01 },
{ key: "exposure", label: "Exposure", min: 1.5, max: 1500, step: 10, default: 50 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 15, step: 0.1, default: 1.35 },
{ key: "saturation", label: "Saturation", min: 0, max: 4, step: 0.02, default: 1.35 },
{ key: "alphaGain", label: "Alpha gain", min: 0.05, max: 15, step: 0.1, default: 2 },
{ key: "edge", label: "Edge sharpness", min: 0, max: 1, step: 0.01, default: 1 },
{ key: "edgeFade", label: "Halo falloff", min: 0.1, max: 3, step: 0.015, default: 0.98 }
],
colors: [{ key: "tint", label: "Tint", default: "#ffffff" }],
/*
Staged on the two integrated clocks, as across the family: thinking
sends the AXIS hunting (the web continuously reweaves in place) while
speaking is speed-led (the layer-cycled iridescence shimmers fast and
bright). turb and disperse are amplitudes/phases — everything glides.
*/
statePresets: {
// calm: slow weave, near-still axis
idle: {
speed: 0.4,
wander: 0.12,
turb: 0.75,
disperse: 1,
exposure: 72,
scatter: 0.01,
alphaGain: 2
},
// reweaving: the axis hunts at six times idle and the weave deepens —
// the web knits and unknits in place, spectrum pulled tighter
thinking: {
speed: 1,
wander: 0.7,
turb: 0.95,
disperse: 0.8,
exposure: 66,
scatter: 0.0095,
alphaGain: 2.1
},
// answering: fast iridescent shimmer, wide spectrum, hot threads
speaking: {
speed: 2,
wander: 0.3,
turb: 1.1,
disperse: 1.6,
exposure: 46,
scatter: 0.0075,
alphaGain: 2.5
}
}
};
export type Shdr15Props = Omit<ShaderOrbProps, "variant">;
export function Shdr15({ size = 280, ...rest }: Shdr15Props) {
return <ShaderOrb variant={shdr15Orb} size={size} {...rest} />;
}
export default Shdr15;
18. components/ui/shdr-16.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-16 — sunlight through water: a caustic net crawling over the ball,
split into colour at its edges, surging with the agent's voice.
Not a port. Built from the README's own rule for surface detail — an
isoline through a warped field — pushed until it reads as the thing it
is imitating: the bright branching network a pool throws on its floor.
What it is:
- TWO FAMILIES OF LINES, CROSSED. 1 - abs(sin(q)) is one on a crest and
zero between, on each axis; raised to a power it is a thin line or a
broad wash, and the product of the two families is added back so the
crossings burn hotter than the lines. That product is what makes it
caustic rather than grid: real caustics are brightest where wavefronts
focus together, and the crossings are exactly those foci. The sum is
normalised to one at a crossing, on purpose: past the tone knee an
unbounded sum clips every channel and the sun colour goes white, so
gain would only ever buy you washout. Bounded, gold stays gold at the
hottest focus.
- THE WATER IS A FOLD. Before the lines are read, the plane is folded on
its own sines three times at rising frequency, each octave on its own
clock rate. That is refraction through a rippling surface, stated as a
domain warp: the grid does not scroll, it is bent, and the lines branch
and pinch where the bends pile up.
- CHROMATIC SPLIT ON TIME, NOT SPACE. The net is read three times, one
per channel, each at a slightly different moment of the fold. Where a
line is moving its three readings disagree; where it holds still they
agree. The light itself is the net's LUMINANCE under the sun colour,
and the disagreement is split off as a zero-mean residual added back
on its own amplitude — so the rainbow is a fringe on moving edges,
never a tint on the whole net, and the sun colour survives it. That is
how dispersion through water actually looks: white light, coloured
edges.
- THE SURGE is a round trip on an integrated clock through cos — it
eases through both ends and never wraps, the shdr-31 construction —
and it drives gain and ripple depth together, so the water brightens
as it deepens. Depth is one amplitude, so a state can have all of it,
none, or a flicker of it.
Design decisions, each one a rule in the README:
- THE ORB IS THE OBJECT. The net lives on the sphere's own surface
direction, not on a disc cut out of a plane. But the README's move for
flat fields — a stereographic projection of the dome — has a POLE, and
a ball that turns indefinitely carries whatever texture is behind it
through that pole, where the map stretches it toward infinity. The
aurora hid an atan seam by crossfading two wrappings; this uses
TRIPLANAR mapping instead, which has no seam and no pole: the plane
field is read on the three axis planes of the surface direction and
blended by how squarely each faces it. Three reads per channel, nine in
all — the field is a handful of sines, so it is cheap.
- EVERY INTERNAL FREQUENCY IS A LITERAL. The fold frequencies and the
per-octave clock rates are written into the shader, so the net's
spacing is ONE control — uP_scale — and it is the only spatial
frequency in the orb. It is never staged. Everything a state does
touch is an amplitude or an integrated clock, which is what lets all
three states cross-fade with nothing racing across the surface.
- pow() only ever sees a base in [0, 1] here — 1 - abs(sin) — so the
edge exponent is safe to sweep. The tone knee is applied after a max()
for the same reason.
- Surface-lit and mask-bounded, so alpha IS coverage: premultiplied
output, the opposite convention from the emissive orbs (see shdr-31).
---------------------------------------------------------------------------- */
const CAUSTIC_FRAG = `
// Volume- and surge-reactive values, resolved once per fragment in main().
float causticWarp;
/*
The water. The plane is folded on its own sines three times, each octave
at a literal frequency and on its own share of the clock, so the ripples
refract the net rather than scroll it. Amplitude is the one control.
*/
vec2 fold(vec2 p, float t) {
p += causticWarp * sin(p.yx * 1.31 + vec2( t * 0.90, -t * 0.70));
p += causticWarp * 0.60 * sin(p.yx * 2.17 + vec2(-t * 1.30, t * 1.10));
p += causticWarp * 0.35 * sin(p.yx * 3.73 + vec2( t * 1.90, t * 1.60));
return p;
}
/*
The light. Two crossed families of crest lines, sharpened by the edge
exponent — the base is 1 - abs(sin), always in [0, 1], so pow is defined —
with their product added back so the crossings, where wavefronts focus,
burn hotter than the lines between them.
*/
float net(vec2 p, float t) {
vec2 q = fold(p, t);
vec2 s = 1.0 - abs(sin(q));
vec2 l = pow(s, vec2(uP_edge));
// Normalised to [0, 1]: the sum peaks at four on a crossing, and left
// unbounded it drove the tone knee into clipping all three channels,
// which turns any sun colour white. Bounded, the sun colour survives
// the knee at the foci and gain is a real brightness rather than a
// race to white.
return (l.x + l.y + 2.0 * l.x * l.y) * 0.25;
}
/*
Triplanar: the plane field read on the three axis planes of the surface
direction and blended by the fourth power of each component, so each
plane only shows where it faces squarely. No pole and no seam — the ball
can turn forever.
*/
float netOn(vec3 sp, float t) {
vec3 w = sp * sp;
w *= w;
w /= (w.x + w.y + w.z);
float k = uP_scale;
return w.x * net(sp.yz * k, t) + w.y * net(sp.zx * k, t) + w.z * net(sp.xy * k, t);
}
void main() {
vec2 uv = orbUV();
float rd = length(uv);
float R = uP_radius;
float mask = smoothstep(0.012, -0.012, rd - R);
if (mask <= 0.0) {
gl_FragColor = vec4(0.0);
return;
}
vec2 pl = uv / R;
float z = sqrt(max(1.0 - dot(pl, pl), 0.0));
vec3 n = vec3(pl, z);
// the ball turns about Y on its own integrated clock
float cr = cos(uP_spin);
float sr = sin(uP_spin);
vec3 sp = vec3(n.x * cr - n.z * sr, n.y, n.x * sr + n.z * cr);
float t = uP_flow; // integrated clock: the water
/*
The surge: a round trip on an integrated clock through cos, so it eases
through both ends and never wraps. It lifts the gain and deepens the
ripple together — brighter as the water heaves — and uP_swell is how
much of that a state takes.
Volume coupling in the family language: the agent's voice brightens the
light, the user's deepens the water.
*/
float surge = 0.5 - 0.5 * cos(uP_swellRate);
float gainNow = uP_gain * mix(1.0, 0.55 + 0.9 * surge, uP_swell) * (0.8 + 0.5 * uOutput);
causticWarp = uP_warp * mix(1.0, 0.8 + 0.4 * surge, uP_swell) * (1.0 + 0.35 * uInput);
/*
Three moments of the fold, one per channel. The LIGHT is the net's
luminance under the sun colour, so gold is gold at the foci; the
per-channel disagreement is split off as a zero-mean residual and added
back scaled by uP_split, so the rainbow is a fringe that rides on the
edges where they move and vanishes where they hold — never a tint on
the whole net. Read once with the offset baked in and once without
would cost the same three evaluations, so the offset is constant and
the split is a plain amplitude on the residual: safe to stage.
*/
float ds = 0.09;
vec3 c = vec3(netOn(sp, t + ds), netOn(sp, t), netOn(sp, t - ds));
float cLum = dot(c, vec3(1.0 / 3.0));
vec3 fringe = (c - cLum) * uP_split;
float lambert = clamp(dot(n, normalize(vec3(-0.45, 0.55, 0.7))), 0.0, 1.0);
float fres = pow(1.0 - z, 2.5);
// the floor of the pool, then the light thrown on it — dimmer round the
// limb, where the floor tilts away from the sun
vec3 col = uC_deep * (0.35 + 0.65 * uP_light * lambert);
col += (uC_sun * cLum + fringe) * gainNow * (0.55 + 0.45 * lambert);
col += uC_sheen * uP_rim * fres;
col = pow(max(col, vec3(0.0)), vec3(uP_contrast));
col = tanh3(col);
// Surface orb bounded by a mask: alpha IS coverage, so premultiply — the
// opposite convention from the emissive orbs (see shdr-31).
float a = mask;
gl_FragColor = vec4(max(col, vec3(0.0)) * a, a);
}
`;
export const shdr16Orb: OrbVariant = {
key: "shdr-16",
label: "SHDR-16",
note: "sunlight through water — a caustic net crawling over the ball, fringing into colour where it moves",
frag: CAUSTIC_FRAG,
params: [
{ key: "flow", label: "Water flow", min: 0.015, max: 10, step: 0.05, default: 0.9, integrate: true },
{ key: "spin", label: "Turn", min: 0, max: 5, step: 0.03, default: 0.08, integrate: true },
{ key: "swellRate", label: "Surge rate", min: 0, max: 8, step: 0.05, default: 0.6, integrate: true },
{ key: "radius", label: "Radius", min: 0.15, max: 3, step: 0.015, default: 0.9 },
{ key: "scale", label: "Net scale", min: 3, max: 30, step: 0.1, default: 9 },
{ key: "warp", label: "Ripple depth", min: 0, max: 3, step: 0.02, default: 0.8 },
{ key: "edge", label: "Line sharpness", min: 0.5, max: 10, step: 0.05, default: 2.2 },
{ key: "split", label: "Colour fringe", min: 0, max: 3, step: 0.02, default: 0.6 },
{ key: "swell", label: "Surge depth", min: 0, max: 1, step: 0.01, default: 0.15 },
{ key: "gain", label: "Sun power", min: 0.05, max: 6, step: 0.05, default: 1.6 },
{ key: "contrast", label: "Tone knee", min: 0.15, max: 6, step: 0.05, default: 1.1 },
{ key: "light", label: "Floor light", min: 0, max: 3, step: 0.015, default: 0.9 },
{ key: "rim", label: "Rim sheen", min: 0, max: 3, step: 0.015, default: 0.6 }
],
// the pool floor, the light thrown on it, and the wet gloss at the limb
colors: [
{ key: "deep", label: "Water", default: "#0b2f6e" },
{ key: "sun", label: "Caustic light", default: "#7ff6ff" },
{ key: "sheen", label: "Sheen", default: "#bfe8ff" }
],
/*
Staged on the three integrated clocks and on amplitudes only — nothing
a state touches is a spatial frequency, so every transition cross-fades
with nothing racing across the surface. NET SCALE is the one frequency
in the orb and it is never staged: it multiplies the surface direction
before the fold, so gliding it would sweep the whole net through every
spacing in between. Line sharpness is a power exponent on a base in
[0, 1], an amplitude, and safe at any span.
The read is in the water:
idle a slow pool, lit HARD. Gentle ripple, moderate lines, the
faintest surge, the ball barely turning — but the sun at
full power on a firmer knee, so the caustics burn white
over dark red water.
thinking NERVOUS water. The flow runs at three and a half times
resting on a ripple nearly double rest's, and the lines
go broad under it — a coarse, fast, restless net. The
surge is shallow but quick, a flicker rather than a heave,
and the ball all but freezes so the motion is in the
light. The sun is dropped to half rest's power with the
key light and rim pulled down: white on a darker red,
dimmer than rest.
speaking the water HEAVES, fast. Full surge depth on a rate five
times rest's, so the light pumps in quick breaths, on the
deepest ripple and the broadest lines — sheets of light
rather than threads — with the colour split wide so every
edge fringes, and the ball plainly turning beneath it. The
rim is all but cut so the light is the sun alone. Warm:
gold on brick red.
*/
statePresets: {
idle: {
flow: 0.92,
spin: 0.09,
swellRate: 0.6,
warp: 0.8,
edge: 3,
split: 0.6,
swell: 0.15,
gain: 6,
contrast: 1.3,
light: 0.9,
rim: 0.6
},
thinking: {
flow: 3.22,
spin: 0.03,
swellRate: 2.4,
warp: 1.52,
edge: 2.8,
split: 0.36,
swell: 0.26,
gain: 3.35,
contrast: 1.15,
light: 0.525,
rim: 0.21
},
speaking: {
flow: 1.8,
spin: 0.7,
swellRate: 5.4,
warp: 1.7,
edge: 1.7,
split: 1.1,
swell: 1,
gain: 3.6,
contrast: 0.85,
light: 0.96,
rim: 0.18
}
},
// aqua light on dark red water at rest, pure white on a darker red while
// searching, gold on brick-red water while answering
stateColors: {
idle: {
deep: "#6f0b0b",
sun: "#7ff6ff",
sheen: "#bfe8ff"
},
thinking: {
deep: "#3a0808",
sun: "#ffffff",
sheen: "#ffffff"
},
speaking: {
deep: "#8d2525",
sun: "#ffb914",
sheen: "#ffb3c6"
}
}
};
export type Shdr16Props = Omit<ShaderOrbProps, "variant">;
export function Shdr16({ size = 280, ...rest }: Shdr16Props) {
return <ShaderOrb variant={shdr16Orb} size={size} {...rest} />;
}
export default Shdr16;
19. components/ui/shdr-17.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-17 — a grainy, hyper-saturated storm raging on the ball.
The weather is the classic two-level DOMAIN WARP: the storm field is fbm
sampled where a previous fbm said to look, so the clouds tear and curl
instead of just scrolling. It lives on a stereographic wrap of a rotating
dome, with JOVIAN BAND SHEAR on top — latitude rings flowing past each
other at different speeds, banded like a gas giant, because sp.y is
invariant under the dome's Y-roll the bands stay horizontal while the
weather rolls beneath them.
Colour is where it goes loud: a four-stop gradient (deep → low → mid → hot)
climbs the storm field, and an iridescent cosine rainbow keyed to the same
field is multiplied over it, so every pressure level of the storm carries
its own hue and the whole ball cycles through many colours at once.
The GRAIN is the signature. Two taps of animated white noise, refreshed on
the ambient clock at about 24 fps — cinema rate: fast enough to read as
film flicker rather than stutter, and deliberately NOT the integrated
clock, because the flicker rate should not follow the agent state:
FIELD GRAIN folded into the storm field BEFORE the gradient, so the
colour stops dither into speckle instead of smooth bands —
the risograph read.
FILM GRAIN multiplied over the final colour, plain photographic noise.
Lightning: a hashed gate per flash interval with an exponential decay —
most intervals stay dark, some flash — and the gate opens wide with agent
output, so a speaking orb strobes its high-pressure cells.
Surface-lit and mask-bounded, so alpha IS coverage — premultiplied output,
as in shdr-14.
---------------------------------------------------------------------------- */
const TEMPEST_FRAG = `
const float PI = 3.14159265359;
// Animated white noise, one tap per grain cell per grain frame. The seed
// decorrelates the two taps so field grain and film grain never line up.
float grainNoise(vec2 gpix, float frame, float seed) {
return hash(gpix + vec2(frame * 13.71 + seed, frame * 7.37 - seed));
}
void main() {
// Volume coupling: user input churns the warp harder, agent output
// brightens the field — the lightning gate opens separately below.
float warpNow = uP_warp * (1.0 + 0.55 * uInput);
float gainNow = uP_gain * (0.85 + 0.45 * uOutput);
vec2 uv = orbUV();
float rd = length(uv);
float R = uP_radius;
float mask = smoothstep(0.012, -0.012, rd - R);
// The storm below costs five fbm evaluations per fragment — skip all of it
// outside the silhouette instead of computing weather for transparent sky.
if (mask <= 0.0) {
gl_FragColor = vec4(0.0);
return;
}
vec2 pl = uv / R;
float r2 = dot(pl, pl);
float z = sqrt(max(1.0 - r2, 0.0));
vec3 n = vec3(pl, z);
// roll the dome about Y on its own integrated clock
float cr = cos(uP_spin);
float sr = sin(uP_spin);
vec3 sp = vec3(n.x * cr - n.z * sr, n.y, n.x * sr + n.z * cr);
float t = uP_speed; // integrated clock
// stereographic wrap: the weather travels around the ball and compresses
// toward the limb instead of sliding across a flat disc
vec2 st = sp.xy / (1.3 + sp.z) * uP_scale;
/*
Jovian band flow: a uniform stream plus a BOUNDED traveling wave of
shear, so latitude rings appear to slip past each other. The obvious
construction — t * sin(latitude) — accumulates the differential forever
and rakes the field into hairline streaks within seconds of the random
mount phase; the wave form keeps the shear amplitude fixed while its
phase travels. sp.y is untouched by the Y-roll, so the bands hold
horizontal while the dome turns underneath them.
*/
st.x -= t * 0.3;
st.x += uP_shear * sin(sp.y * uP_bands - t * 0.45);
// two-level domain warp, the storm-cloud construction: q says where to
// look, w says where q said to look, the field reads there
vec2 q = vec2(
fbm(st + vec2(0.0, t * 0.35)),
fbm(st + vec2(5.2, 1.3) - vec2(t * 0.28, 0.0))
);
vec2 w = vec2(
fbm(st + warpNow * q + vec2(1.7, 9.2) + vec2(t * 0.12, 0.0)),
fbm(st + warpNow * q + vec2(8.3, 2.8) - vec2(0.0, t * 0.1))
);
float f = fbm(st + uP_churn * w);
/*
Grain tap 1: speckle folded into the FIELD itself, before the gradient,
so the colour stops below dither into grain instead of smooth bands.
Refreshed on the ambient clock — the flicker rate stays constant across
states on purpose (see the header note).
*/
vec2 gpix = floor(gl_FragCoord.xy / max(uP_grainSize, 1.0));
float frame = floor(uTime * 48.0);
float g1 = grainNoise(gpix, frame, 3.1);
f += (g1 - 0.5) * uP_grain;
f = pow(clamp(f * gainNow, 0.0, 1.0), uP_contrast);
// four-stop palette climbing the storm field
vec3 col = mix(uC_deep, uC_low, smoothstep(0.05, 0.35, f));
col = mix(col, uC_mid, smoothstep(0.35, 0.62, f));
col = mix(col, uC_hot, smoothstep(0.62, 0.88, f));
// iridescent shimmer: a cosine rainbow keyed to the field AND to the warp
// vector — q varies at storm-cell scale, so the rainbow lands as coherent
// coloured weather cells instead of hue noise that optically averages to
// grey — multiplied in so it bends hues without erasing the palette
vec3 shimmer = 0.5 + 0.5 * cos(2.0 * PI * (f * 0.9 + q.x * 1.1 + t * 0.06 + vec3(0.0, 0.33, 0.67)));
col = mix(col, col * (0.35 + 1.9 * shimmer), uP_rainbow);
/*
Lightning: one hashed gate per flash interval with an exponential decay,
so most intervals stay dark and some strike. Agent output opens the gate
— an idle orb flickers occasionally, a speaking one strobes. The strike
lands hardest on the high-pressure cells of the field.
*/
float ft = t * uP_flashRate;
float gate = step(1.0 - (0.1 + 0.5 * uOutput), hash(vec2(floor(ft), 7.7)));
float flashEnv = gate * exp(-fract(ft) * 6.0);
// squared so the strike stays inside the storm cells — a linear weight
// tints the whole ball and reads as the canvas strobing, not as weather
float high = smoothstep(0.55, 0.95, f);
col += uC_flash * (flashEnv * uP_flash) * (0.06 + 0.94 * high * high);
// dome shading keeps the ball a ball under the weather
float lambert = clamp(dot(n, normalize(vec3(-0.45, 0.55, 0.7))), 0.0, 1.0);
col *= 0.35 + uP_light * lambert;
float fres = pow(1.0 - z, 2.5);
col += uC_flash * uP_rim * fres * (0.4 + 0.35 * flashEnv);
// grain tap 2: plain film grain over the final colour
float g2 = grainNoise(gpix, frame, 27.9);
col *= 1.0 + (g2 - 0.5) * uP_filmGrain;
// Surface orb bounded by a mask: alpha IS coverage, so premultiply — the
// opposite convention from the emissive orbs (see shdr-31).
float a = mask;
gl_FragColor = vec4(max(col, vec3(0.0)) * a, a);
}
`;
export const shdr17Orb: OrbVariant = {
key: "shdr-17",
label: "SHDR-17",
note: "a grainy many-coloured storm with band shear and lightning",
frag: TEMPEST_FRAG,
params: [
{ key: "speed", label: "Storm speed", min: 0.015, max: 10, step: 0.05, default: 0.9, integrate: true },
{ key: "spin", label: "Roll", min: 0, max: 5, step: 0.03, default: 0.12, integrate: true },
{ key: "radius", label: "Radius", min: 0.15, max: 3, step: 0.015, default: 0.9 },
{ key: "scale", label: "Weather scale", min: 0.3, max: 12, step: 0.1, default: 2.4 },
{ key: "bands", label: "Band count", min: 0, max: 20, step: 0.1, default: 6 },
{ key: "shear", label: "Band shear", min: 0, max: 5, step: 0.03, default: 1.1 },
{ key: "warp", label: "Warp", min: 0, max: 8, step: 0.05, default: 2.2 },
{ key: "churn", label: "Churn", min: 0, max: 8, step: 0.05, default: 1.4 },
{ key: "gain", label: "Brightness", min: 0.05, max: 5, step: 0.05, default: 1.15 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 10, step: 0.05, default: 1.35 },
{ key: "grain", label: "Field grain", min: 0, max: 2, step: 0.01, default: 0.4 },
{ key: "filmGrain", label: "Film grain", min: 0, max: 2, step: 0.01, default: 0.35 },
{ key: "grainSize", label: "Grain size", min: 1, max: 8, step: 1, default: 2 },
{ key: "rainbow", label: "Iridescence", min: 0, max: 2, step: 0.01, default: 0.65 },
{ key: "flashRate", label: "Flash rate", min: 0, max: 10, step: 0.05, default: 1.6 },
{ key: "flash", label: "Flash power", min: 0, max: 5, step: 0.03, default: 1.2 },
{ key: "light", label: "Key light", min: 0, max: 3, step: 0.015, default: 0.85 },
{ key: "rim", label: "Rim light", min: 0, max: 3, step: 0.015, default: 0.5 }
],
/*
* Five stops: four climbing the storm field plus the lightning colour.
* The iridescence param multiplies a rainbow over all of them, so the
* palette here sets the mood and the shimmer supplies the extra hues.
*/
colors: [
{ key: "deep", label: "Deep", default: "#2a0f4e" },
{ key: "low", label: "Low pressure", default: "#0fd0c3" },
{ key: "mid", label: "Mid pressure", default: "#ff5e9d" },
{ key: "hot", label: "High pressure", default: "#ffd166" },
{ key: "flash", label: "Lightning", default: "#eaf4ff" }
],
/*
Staged in the family language. Grain and grain size never move between
states — grain is a quantizer, and a gliding quantizer pops instead of
fading (same rule as the dither orb's cell grid).
*/
statePresets: {
// brooding: bands drifting, the odd distant flicker
idle: {
speed: 0.9,
shear: 1.1,
warp: 2.2,
churn: 1.4,
flash: 0.7,
gain: 1.15,
contrast: 1.35
},
// computing: the storm churns IN PLACE — clock at twice idle, deeper
// warp, bands almost stalled, lightning held back
thinking: {
speed: 2.2,
shear: 0.6,
warp: 3.4,
churn: 2.1,
flash: 0.6,
gain: 1.05,
contrast: 1.5
},
// answering: bands race, the field blooms bright, lightning strobes
speaking: {
speed: 1.6,
shear: 2.2,
warp: 2.6,
churn: 1.6,
flash: 2.6,
gain: 1.45,
contrast: 1.2
}
},
// teal-magenta-amber carnival at rest, cold indigo-cyan while computing,
// hot magma while answering
stateColors: {
idle: {
deep: "#2a0f4e",
low: "#0fd0c3",
mid: "#ff5e9d",
hot: "#ffd166",
flash: "#eaf4ff"
},
thinking: {
deep: "#0d1440",
low: "#4c4cf0",
mid: "#9d4ce0",
hot: "#4ce0ff",
flash: "#d5e5ff"
},
speaking: {
deep: "#3a0f1e",
low: "#ff6a3d",
mid: "#ff2e88",
hot: "#ffd23f",
flash: "#fff3e0"
}
}
};
export type Shdr17Props = Omit<ShaderOrbProps, "variant">;
export function Shdr17({ size = 280, ...rest }: Shdr17Props) {
return <ShaderOrb variant={shdr17Orb} size={size} {...rest} />;
}
export default Shdr17;
20. components/ui/shdr-18.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-18 — a crystal folded out of one eighth of space, tumbling.
Ported from a golfed twigl listing:
for(float i,z,d;i++<5e1;o+=(cos(.2*z+vec4(0,2,3,0))+1.)/d/z)
{vec3 p=z*normalize(FC.rgb*2.-r.xyy),a=normalize(cos(vec3(0,2,4)+t));
p.z+=4.,a=abs(a*dot(a,p)-cross(a,p));
z+=d=.3*length(cos(max(a,a.yzx)));}
o=tanh(o/1e2);
What it actually is, decoded:
- THE FOLD IS THE WHOLE ORB. abs() on a 3-vector reflects all eight
octants into one, so whatever is drawn in that eighth appears seven
more times, mirrored — the field is forced into the symmetry of a
crystal without a single explicit mirror plane being written down.
- max(a, a.yzx) FOLDS AGAIN, on the diagonals. Component-wise max
against the vector's own rolled swizzle creases the field wherever
two components are equal, which are exactly the diagonal planes of
the cube. Between them and the octant fold, the field carries the
full symmetry of an octahedron.
- THE ROTATION IS EXACT AND NEGATIVE. a*dot(a,p) - cross(a,p) with unit
a is Rodrigues at exactly MINUS 90 degrees — shdr-22 decodes the
same construction with a plus. It matters here beyond handedness:
the fold happens AFTER the rotation, so the mirror planes are carried
around by the axis, and the axis wanders on the clock. The crystal
tumbles; it is not a static mandala with a moving texture.
- The step and the density are one quantity again, length of a cosine
of the folded point — periodic, so the crystal repeats through space,
and small wherever the cosines null together, which lights the cell
walls.
- cos(.2*z + vec4(0,2,3,0)) + 1 tints by DEPTH ALONG THE RAY, with the
channels far enough apart to run most of a hue wheel, and the 1/z
brightens what is near.
Port decisions, each one a documented trap or rule in the README:
- THE ORB IS THE OBJECT: the crystal is periodic and fills space, so it
is bounded by the family's envelope and analytic silhouette. That also
disposes of the listing's brightest region, which is the 1/z bloom
sitting on the lens at z near zero — outside the envelope, cut. What
survives of 1/z inside the ball is a gentle front-to-back shading,
which is the useful half of it.
- The golfed rotation IS orthonormal here, against the README's standing
warning — see above. Left as written.
- The listing relies on i, z and d starting at zero; uninitialised
locals are UNDEFINED in GLSL ES 1.0, explicit here.
- Accumulation is weighted by the step length, as the README requires:
the step collapses on the cell walls, so an unweighted sum piles up
hundreds of samples exactly where the field is already brightest.
- This orb does NOT carry the clamp-normalization line its siblings do
(shdr-22, shdr-07, orb-nova). Theirs exists because their raw
step weights run into the hundreds, so the clamp would leak into total
energy; here the 1/d spike is bounded by the step FLOOR long before
the clamp sees it, the raw weights are order one, and normalizing
would only darken everything by a constant.
- Emitted light, so rgb is already premultiplied and alpha comes from
the peak channel (see shdr-31).
---------------------------------------------------------------------------- */
/*
* Step counts are `#define`s: ES 1.0 requires constant loop bounds. STEPS is
* the listing's i++ < 5e1.
*/
const OCTANT_FRAG = `
#define STEPS 50
#define AA 1
// Volume-reactive values, resolved once per fragment in main().
float octantFold;
float octantFreq;
float octantExposure;
vec3 octantRender(vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
vec3 ro = vec3(0.0, 0.0, uP_camDist);
vec3 rd = normalize(vec3(uv, -uP_focal));
float wander = uP_wander; // integrated clock: the axis, and with it the
// mirror planes, tumble
/*
The wandering axis. Unit by construction, which is what makes the
rotation below an exact one — and the three phases are far enough
apart that the cosines can never null together, so the normalize is
safe without a guard.
*/
vec3 axis = normalize(cos(wander + vec3(0.0, 2.0, 4.0)));
vec3 acc = vec3(0.0);
// transmittance carried front-to-back — near cells veil far ones
float T = 1.0;
// march only the span the envelope can light, as in shdr-01
float z = max(uP_camDist - uP_envRadius * 1.3, 0.0);
float zEnd = uP_camDist + uP_envRadius * 1.3;
for (int it = 0; it < STEPS; it++) {
vec3 p = ro + rd * z;
// the exact minus-90-degree rotation about the wandering axis
vec3 a = dot(axis, p) * axis - cross(axis, p);
/*
The two folds, both blendable. uP_fold reflects the octants together
and uP_crease creases the diagonals; at zero the crystal dissolves
back into an ordinary periodic field, which is worth being able to
see, because the symmetry is doing more work here than the field is.
*/
a = mix(a, abs(a), octantFold);
a = mix(a, max(a, a.yzx), uP_crease);
// step and density in one quantity, as in the listing
float d = uP_stepScale * length(cos(a * octantFreq));
d = max(d, uP_envRadius * 0.004);
/*
Depth as hue, near as bright.
The hue is keyed to depth measured from where the envelope BEGINS,
not from the camera. The listing marches from the lens out to twenty
units and cycles its ramp several times over that; bounded to the
ball, the same .2 slope covers barely a quarter turn of the wheel —
and the quarter it covers has both green and blue sitting at the
bottom of their cosines, so the first build of this orb came out a
flat dark red. Anchored to the ball, the ramp spans it, and moving
the camera no longer repaints the crystal.
No step-length weighting here, unlike orb-nova and against the
README's usual rule — because 1/d IS this shader's density, not an
artefact of sphere tracing. Multiply it by the step and the two
cancel exactly, leaving a flat sum with every trace of the cell walls
gone. The clamp does the job the step weight would have, which is
also what shdr-22 does with the same construction.
*/
float zRel = z - (uP_camDist - uP_envRadius);
vec3 w = cos(uP_hue * zRel + vec3(0.0, 2.0, 3.0) * uP_spread) + 1.0;
w /= d * max(z, 0.05);
w = min(w, vec3(uP_stepClamp));
// envelope: plateau through the ball, cut 12% past the radius so the
// analytic silhouette in main() still has emission left to cut
float env = smoothstep(uP_envRadius * 1.12, uP_envRadius * uP_envCore, length(p));
w = (w + uP_fill) * env;
acc += T * w;
T *= exp(-dot(w, vec3(0.299, 0.587, 0.114)) * uP_scatter);
z += d;
if (T < 0.004 || z > zEnd) break;
}
return acc;
}
void main() {
octantFold = clamp(uP_fold * (1.0 + 0.3 * uInput), 0.0, 1.0);
octantFreq = uP_freq * (1.0 + 0.25 * uInput);
octantExposure = uP_exposure * (1.0 - 0.35 * uOutput);
vec3 acc = vec3(0.0);
#if AA > 1
for (int mx = 0; mx < AA; mx++) {
for (int my = 0; my < AA; my++) {
vec2 offset = (vec2(float(mx), float(my)) + 0.5) / float(AA) - 0.5;
acc += octantRender(gl_FragCoord.xy + offset);
}
}
acc /= float(AA * AA);
#else
acc = octantRender(gl_FragCoord.xy);
#endif
// tanh tone map per channel — the envelope and transmittance change the
// accumulator's scale, so the golfed /1e2 knee is a tunable here
vec3 col = tanh3(acc / max(octantExposure, 0.01));
col = pow(clamp(col, 0.0, 1.0), vec3(uP_contrast));
// saturation about luminance, then the tint
float lum = dot(col, vec3(0.299, 0.587, 0.114));
col = mix(vec3(lum), col, uP_saturation);
col *= uC_tint;
// alpha from the brightest channel, not luminance — a deep blue cell
// has low luminance but must not go transparent
float peak = max(col.r, max(col.g, col.b));
float a = clamp(peak * uP_alphaGain, 0.0, 1.0);
// Analytic silhouette — identical construction to shdr-01: exact
// ray-to-centre distance against the radius, colour AND alpha.
vec3 mrd = normalize(vec3(orbUV(), -uP_focal));
float closest = length(cross(vec3(0.0, 0.0, uP_camDist), mrd));
float band = mix(0.35, 0.012, clamp(uP_edge, 0.0, 1.0));
float mask = 1.0 - smoothstep(uP_envRadius * (1.0 - band), uP_envRadius * 1.005, closest);
col *= mask;
a *= mask;
// safety taper at the frame boundary — colour as well as alpha
float r2d = length(orbUV());
float fade = 1.0 - smoothstep(uP_edgeFade, 1.0, r2d);
col *= fade;
a *= fade;
// Emitted light, so rgb is already premultiplied — do NOT scale by alpha
// again (see the same note in shdr-31).
gl_FragColor = vec4(col, a);
}
`;
export const shdr18Orb: OrbVariant = {
key: "shdr-18",
label: "SHDR-18",
note: "a crystal folded out of one eighth of space, tumbling",
frag: OCTANT_FRAG,
params: [
{ key: "wander", label: "Tumble", min: 0, max: 5, step: 0.02, default: 0.5, integrate: true },
{ key: "camDist", label: "Camera distance", min: 1, max: 50, step: 0.3, default: 4 },
{ key: "focal", label: "Lens", min: 0.15, max: 15, step: 0.05, default: 1.5 },
{ key: "fold", label: "Octant fold", min: 0, max: 1, step: 0.01, default: 1 },
{ key: "crease", label: "Diagonal crease", min: 0, max: 1, step: 0.01, default: 1 },
{ key: "freq", label: "Crystal frequency", min: 0.1, max: 20, step: 0.05, default: 5 },
{ key: "stepScale", label: "Step scale", min: 0.02, max: 2, step: 0.01, default: 0.3 },
{ key: "hue", label: "Depth hue", min: 0, max: 4, step: 0.01, default: 1.5 },
{ key: "spread", label: "Colour spread", min: 0, max: 3, step: 0.02, default: 1 },
{ key: "envRadius", label: "Envelope radius", min: 0.15, max: 15, step: 0.1, default: 2.1 },
{ key: "envCore", label: "Envelope core", min: 0.3, max: 1.02, step: 0.01, default: 0.9 },
{ key: "fill", label: "Body fill", min: 0, max: 20, step: 0.01, default: 0.02 },
{ key: "stepClamp", label: "Step clamp", min: 1, max: 2000, step: 1, default: 40 },
{ key: "scatter", label: "Diffusion", min: 0, max: 0.2, step: 0.001, default: 0.004 },
{ key: "exposure", label: "Exposure", min: 0.2, max: 500, step: 0.5, default: 18 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 15, step: 0.05, default: 1.2 },
{ key: "saturation", label: "Saturation", min: 0, max: 4, step: 0.02, default: 1.25 },
{ key: "alphaGain", label: "Alpha gain", min: 0.05, max: 15, step: 0.1, default: 2 },
{ key: "edge", label: "Edge sharpness", min: 0, max: 1, step: 0.01, default: 1 },
{ key: "edgeFade", label: "Halo falloff", min: 0.1, max: 3, step: 0.015, default: 0.98 }
],
colors: [{ key: "tint", label: "Tint", default: "#ffffff" }],
/*
Staged on the two folds, which is the only orb here where SYMMETRY is
the mood: a crystal at rest, the mirrors relaxing open while it works,
and locked hard shut while it answers. The tumble carries the tempo.
*/
statePresets: {
// at rest: fully folded, turning slowly — a still crystal
idle: {
wander: 0.5,
fold: 1,
crease: 1,
freq: 5,
exposure: 18,
scatter: 0.004,
alphaGain: 2
},
/*
searching: the crystal is pushed BACK and the mirrors loosen a hair.
The lens nearly doubles so the fold sits deeper in the frame, the
octant fold slips just under one — enough for the field to drift out
of register without dissolving — on a tumble twice idle and a
slightly coarser cell. The step clamp is thrown wide open and the
diffusion raised fivefold, so the march runs long and the light
fogs: the brightest state, but hazed rather than sharp.
*/
thinking: {
wander: 1.08,
focal: 2.7,
fold: 0.91,
crease: 1,
freq: 4.1,
stepClamp: 1200,
scatter: 0.02,
exposure: 26,
alphaGain: 2
},
/*
answering: the mirrors LOCK SHUT again and the crystal goes finer
than idle, marched on a step nearly twice as long so the cell walls
read as crisp lines rather than fog. The envelope core drops to half,
which hollows the ball and leaves the crystal floating in it; the
depth hue runs faster and the saturation is pushed hard, at less than
half the idle knee — the sharpest, most coloured state.
*/
speaking: {
wander: 0.7,
fold: 1,
crease: 1,
freq: 6.05,
stepScale: 0.51,
hue: 2,
envCore: 0.49,
exposure: 8,
scatter: 0.002,
saturation: 2,
alphaGain: 2.7
}
},
// the depth ramp supplies the colour, so the tint only shifts its
// temperature: neutral at rest, cooled while searching, warmed while
// answering
stateColors: {
idle: { tint: "#ffffff" },
thinking: { tint: "#9db8ff" },
speaking: { tint: "#ffc492" }
}
};
export type Shdr18Props = Omit<ShaderOrbProps, "variant">;
export function Shdr18({ size = 280, ...rest }: Shdr18Props) {
return <ShaderOrb variant={shdr18Orb} size={size} {...rest} />;
}
export default Shdr18;
21. components/ui/shdr-19.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-19 — beads swelling and shrinking in their cells, packed over the ball.
Ported from a one-line twigl listing:
vec2 p=FC.xy/8e1,v;
for(int i;i++<9;o=max(o,(dot(cos(v-t),sin(v.yx*.62+t))/6.+.2
-length(p+v+cos(v.yx+t)*.4-1.))*5e1))
v=vec2(i%3,i/3)-ceil(p);
What it actually is, decoded:
- IT IS A CELLULAR FIELD, written as compactly as one can be. Nine
iterations walk the 3x3 neighbourhood of the fragment's cell; each
neighbour gets a jittered feature point and a radius, and max() over
the nine takes the union of the discs. Everything else is the two
expressions that generate radius and jitter.
- THE RADIUS IS A DOT OF A COSINE AGAINST A SINE of the cell index, one
of them swizzled and detuned by .62 so the two never fall into step.
That is the entire random number generator, and because both terms
carry the clock, every bead swells and shrinks on its own phase.
Radii run from about -.13 to .53, so cells with a negative radius
simply have no bead — the field thins and fills on its own.
- THE x50 IS AN EDGE RAMP, not a brightness. A signed distance scaled
that hard and clamped is a hard-edged disc with a pixel of feather,
which is why beads read as objects with rims rather than as blobs.
- The listing writes a scalar into a vec4, so its output is greyscale:
white discs on black.
Two golf bugs, both fixed here, both worth knowing:
- THE CELL IDENTITY IS NOT STABLE. The hash input is
v = neighbour offset - ceil(p), which for a fixed absolute cell C
works out to C - 2*ceil(p): it depends on WHICH FRAGMENT IS LOOKING.
So a bead's radius and jitter change across every cell boundary and
the discs are chopped up on the grid. Keyed on the absolute cell
index instead, as here, a bead is one bead and crosses borders whole.
- THE NEIGHBOURHOOD IS OFF BY ONE. i runs 1..9 rather than 0..8, so
vec2(i%3, i/3) covers (0,-1) through (-1,2) after centring — nine
cells, but not the nine that surround you. The corner at (-1,-1) is
missing, and beads there are clipped. Walked properly here.
Orb decisions, each one a rule in the README:
- THE ORB IS THE OBJECT: a flat 2D field, so it is sampled through a
stereographic projection of the dome — the packing compresses toward
the limb the way beads on a real sphere would — and motion is
projection-safe 2D, as in shdr-08.
- FLAT WHITE DISCS ON BLACK IS THE SUBJECT, and the defaults ship that
way: the listing writes a scalar into a vec4 and stops. What the ball
adds is the wrap, a little dome shading and the sheen — enough to be
a sphere, not enough to stop being a dot screen.
- uP_bead is an option, off by default. Since the winning cell is
already known, its own distance and radius give a hemisphere for
free — sqrt(1 - (dist/radius)^2) is a height, and that is a normal to
light — so the same field can be read as packed glass instead of
printed circles. It is a different orb, so it is a slider rather than
a default.
- Bead size and wander are kept low enough that discs stay separate.
They are drawn with max() of (radius - distance), so two that overlap
do not blend — the larger simply wins and bites a straight edge out
of the smaller. Whole discs with gaps between them is the look; a
field of clipped crescents is what happens when they are pushed too
big or shaken too far.
- The golfed listing relies on i starting at zero; uninitialised locals
are UNDEFINED in GLSL ES 1.0, explicit here.
- Surface-lit and mask-bounded, so alpha IS coverage — premultiplied
output, as in shdr-17.
---------------------------------------------------------------------------- */
/*
* AA is a `#define`: ES 1.0 requires constant loop bounds. The bead rims are
* about a pixel wide by construction, so they need it.
*/
const FOAM_FRAG = `
#define AA 2
// Volume-reactive values, resolved once per fragment in main().
float foamGrow;
float foamJitter;
float foamGain;
vec3 foamRender(vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
float R = max(uP_radius, 0.001);
// the dome: the front hemisphere of a unit ball, in screen space
vec2 pl = uv / R;
float z = sqrt(max(1.0 - dot(pl, pl), 0.0));
float t = uP_speed; // integrated clock
// stereographic wrap of the unrotated dome, as in shdr-08
vec2 p = pl / (z + 1.0 + uP_bulge) * uP_scale;
// projection-safe 2D motion: the packing turns and drifts
float sw = uP_swirl; // integrated clock
p = mat2(cos(sw), -sin(sw), sin(sw), cos(sw)) * p;
p += vec2(uP_slide, uP_slide * 0.7); // integrated clock
vec2 cell = ceil(p);
vec2 f = p - cell; // in (-1, 0], the fragment's place in its own cell
/*
The 3x3 walk, keyed on the ABSOLUTE cell index so a bead is one bead —
see the header. Tracking the winner as well as the max costs nothing
and is what makes the shading below possible.
*/
float cover = 0.0;
float bestRel = 1e9;
vec2 bestDelta = vec2(0.0);
float bestRad = 1.0;
vec2 bestId = vec2(0.0);
for (int gy = -1; gy <= 1; gy++) {
for (int gx = -1; gx <= 1; gx++) {
vec2 g = vec2(float(gx), float(gy));
vec2 id = cell + g;
/*
The listing's generator: a dot of a cosine against a detuned,
swizzled sine of the cell index, both carrying the clock. Mind the
range — a dot of two 2-vectors of unit-bounded components spans
FOUR, not two, so the listing's /6 puts radii within a third of a
cell of the mean. Read it as half that and the largest discs
overlap their neighbours, which is what turns a dot screen into a
litter of merged blobs.
*/
float rad = dot(cos(id - t), sin(id.yx * uP_skew + t)) * uP_vary + foamGrow;
/*
Fragment to feature point, and the signs matter more than they
look. The disc labelled id sits at id + jitter in absolute
coordinates, so delta = p - (id + jitter) = f - g - jitter. Write
it as f + g and the disc's IDENTITY and its POSITION end up using
opposite offsets: every fragment then draws disc id in a different
place, and the field comes out as clumps of half-agreeing circles
rather than circles.
*/
vec2 jit = cos(id.yx + t) * foamJitter;
vec2 delta = f - g - jit;
float dist = length(delta);
/*
The union is taken over COVERAGE, not over the signed distance the
listing maxes. Those differ exactly where two discs overlap: max of
(radius - distance) hands the whole overlap to whichever disc wins
and bites a straight edge out of the other, so the field comes out
a litter of crescents and pinwheels. Max of the clamped coverage
keeps every disc whole and merely lets overlapping ones merge.
The x50 ramp is the listing's, and it is an edge width rather than
a brightness: a signed distance scaled that hard and clamped is a
hard-edged disc with about a pixel of feather.
*/
cover = max(cover, clamp((rad - dist) * uP_edge, 0.0, 1.0));
/*
The winner is tracked separately, by RELATIVE depth rather than
absolute — which disc this fragment is furthest inside, in units of
that disc's own radius. Only the optional bead shading reads it,
and relative depth is what keeps a small disc from being shaded as
though it were the large one beside it.
*/
float rel = dist / max(rad, 1e-4);
if (rel < bestRel) {
bestRel = rel;
bestDelta = delta;
bestRad = rad;
bestId = id;
}
}
}
/*
The bead. The winning cell's own distance and radius give the height of
a hemisphere over the disc, and that is a normal — so the flat decal
becomes a lit piece of glass without a second field being evaluated.
*/
float rr = max(bestRad, 1e-4);
float dome = clamp(1.0 - dot(bestDelta, bestDelta) / (rr * rr), 0.0, 1.0);
vec3 bn = normalize(vec3(bestDelta / rr, sqrt(dome) + 0.001));
vec3 key = normalize(vec3(-0.45, 0.55, 0.72));
float beadLam = clamp(dot(bn, key), 0.0, 1.0);
// per-disc colour, hashed on the cell index — near-flat at the defaults,
// which is what keeps the field reading as a dot screen
vec3 beadCol = mix(uC_low, uC_high, hash(bestId + 0.5));
// uP_bead at 0 leaves the listing's flat disc, which is the default
float shade = mix(1.0, 0.45 + 0.85 * beadLam, uP_bead);
vec3 col = beadCol * shade * foamGain * cover;
// a dark body under the packing, so the gaps read as the ball rather
// than as holes in it
col += uC_body * uP_floorLevel;
col = pow(max(col, vec3(0.0)), vec3(uP_contrast));
float lum = dot(col, vec3(0.299, 0.587, 0.114));
col = mix(vec3(lum), col, uP_saturation);
// dome shading keeps the ball a ball under the packing
vec3 n = vec3(pl, z);
float lambert = clamp(dot(n, key), 0.0, 1.0);
col *= 0.55 + uP_light * lambert;
float fres = 1.0 - z;
fres = fres * fres * fres;
col += uC_sheen * uP_rim * fres;
return col;
}
void main() {
// Volume coupling: the user's voice shakes the beads off their centres,
// the agent's swells them and brightens the packing.
foamGrow = uP_grow * (1.0 + 0.35 * uOutput);
foamJitter = uP_jitter * (1.0 + 0.5 * uInput);
foamGain = uP_gain * (0.85 + 0.4 * uOutput);
vec2 uv = orbUV();
float mask = smoothstep(0.012, -0.012, length(uv) - max(uP_radius, 0.001));
if (mask <= 0.0) {
gl_FragColor = vec4(0.0);
return;
}
vec3 col = vec3(0.0);
#if AA > 1
for (int mx = 0; mx < AA; mx++) {
for (int my = 0; my < AA; my++) {
vec2 off = (vec2(float(mx), float(my)) + 0.5) / float(AA) - 0.5;
col += foamRender(gl_FragCoord.xy + off);
}
}
col /= float(AA * AA);
#else
col = foamRender(gl_FragCoord.xy);
#endif
// Surface orb bounded by a mask: alpha IS coverage, so premultiply — the
// opposite convention from the emissive orbs (see shdr-31).
float a = mask;
gl_FragColor = vec4(max(col, vec3(0.0)) * a, a);
}
`;
export const shdr19Orb: OrbVariant = {
key: "shdr-19",
label: "SHDR-19",
note: "beads swelling and shrinking in their cells, packed over the ball",
frag: FOAM_FRAG,
params: [
{ key: "speed", label: "Anim speed", min: 0.015, max: 10, step: 0.05, default: 0.5, integrate: true },
{ key: "swirl", label: "Swirl", min: 0, max: 3, step: 0.015, default: 0.05, integrate: true },
{ key: "slide", label: "Drift", min: 0, max: 4, step: 0.02, default: 0.1, integrate: true },
{ key: "radius", label: "Radius", min: 0.15, max: 3, step: 0.015, default: 0.9 },
{ key: "scale", label: "Packing scale", min: 0.3, max: 30, step: 0.1, default: 10 },
{ key: "bulge", label: "Dome bulge", min: 0, max: 4, step: 0.02, default: 0.3 },
{ key: "grow", label: "Dot size", min: -0.2, max: 1.2, step: 0.005, default: 0.185 },
{ key: "vary", label: "Size variation", min: 0, max: 0.4, step: 0.005, default: 0.13 },
{ key: "skew", label: "Generator detune", min: 0, max: 3, step: 0.01, default: 0.62 },
{ key: "jitter", label: "Dot wander", min: 0, max: 1.5, step: 0.01, default: 0.1 },
{ key: "edge", label: "Rim hardness", min: 1, max: 200, step: 1, default: 50 },
{ key: "bead", label: "Bead shading", min: 0, max: 1, step: 0.01, default: 0 },
{ key: "gain", label: "Brightness", min: 0.05, max: 4, step: 0.02, default: 1 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 6, step: 0.05, default: 1 },
{ key: "saturation", label: "Saturation", min: 0, max: 4, step: 0.02, default: 1.1 },
{ key: "floorLevel", label: "Body fill", min: 0, max: 2, step: 0.01, default: 0.06 },
{ key: "light", label: "Key light", min: 0, max: 3, step: 0.015, default: 0.3 },
{ key: "rim", label: "Rim sheen", min: 0, max: 3, step: 0.015, default: 0.35 }
],
/*
* Four stops: the two ends of the per-bead hash, the body the packing
* sits on, and the glass.
*/
colors: [
{ key: "low", label: "Dot", default: "#ffffff" },
{ key: "high", label: "Dot accent", default: "#eef5ff" },
{ key: "body", label: "Body", default: "#05070c" },
{ key: "sheen", label: "Sheen", default: "#9dbfe4" }
],
/*
Staged on BEAD SIZE, which decides whether the ball is a scatter of
separate beads or a packed foam, and on wander, which decides how far
each one strays from its cell. Packing scale never moves between
states — it sets the bead count, and a gliding count reads as the ball
inflating rather than as a change of mood.
*/
statePresets: {
/*
at rest: a sparse, restless screen. The dots sit small with a wide
size spread and wander most of a cell, on a dome flattened almost to
a disc, so the halftone reads as grain rather than pattern — pushed
bright and hard-contrasted so the few dots that land carry.
*/
idle: {
speed: 0.52,
swirl: 0.045,
slide: 0.1,
bulge: 0.08,
grow: 0.12,
vary: 0.19,
skew: 0.63,
jitter: 0.74,
edge: 51,
gain: 2.28,
contrast: 2.6,
rim: 0.345
},
/*
searching: the screen is set MOVING. The clock runs five times idle,
the swirl and slide both open up an order of magnitude, and the
generator detunes near double, so the dots stream across the ball
rather than sit on it. They swell a little and wander half as far as
idle, under a softer contrast and a stronger key light — a flatter,
brighter, busier screen.
*/
thinking: {
speed: 2.55,
swirl: 0.57,
slide: 0.84,
bulge: 0.14,
grow: 0.19,
vary: 0.165,
skew: 1.13,
jitter: 0.37,
gain: 1.04,
contrast: 0.55,
light: 0.585,
rim: 0.24
},
/*
answering: the screen goes HARD. The generator detune drops to zero,
so every dot's size runs on the clock alone and the whole screen
pulses in step; the size spread opens to its widest and the rim
hardness nearly triples, so the dots read as punched holes rather
than beads. The dome rises, the dots settle to a quarter of idle's
wander, and the body fill is cut — pure white dots on black, at the
thinking tempo and a harder contrast still.
*/
speaking: {
speed: 2.85,
swirl: 0.585,
slide: 0.55,
bulge: 0.28,
grow: 0.22,
vary: 0.365,
skew: 0,
jitter: 0.17,
edge: 141,
gain: 1.35,
contrast: 3.2,
saturation: 2.04,
floorLevel: 0
}
},
// cool glass at rest, then pure white on black for both working states —
// the answering one keeps the faintly warm body
stateColors: {
idle: { low: "#ffffff", high: "#eef5ff", body: "#05070c", sheen: "#9dbfe4" },
thinking: { low: "#dae6ff", high: "#ffffff", body: "#000000", sheen: "#ffffff" },
speaking: { low: "#ffffff", high: "#ffffff", body: "#140a06", sheen: "#ffffff" }
}
};
export type Shdr19Props = Omit<ShaderOrbProps, "variant">;
export function Shdr19({ size = 280, ...rest }: Shdr19Props) {
return <ShaderOrb variant={shdr19Orb} size={size} {...rest} />;
}
export default Shdr19;
22. components/ui/shdr-20.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-20 — a water film rushing down the ball, fountain-style.
Ported from a golfed twigl listing:
vec3 x,c,p;x.x+=9.;
for(float i,z,f;i++<5e1;
p=mix(c,p,.3),
z+=f=.2*(abs(p.z+p.x+16.+tanh(p.y)/.1)+sin(p.x-p.z+t+t)+1.),
o+=(cos(p.x*.2+f+vec4(6,1,2,0))+2.)/f/z)
for(c=p=z*normalize(FC.rgb*2.-r.xyy),p.y*=f=.3;f++<5.;
p+=cos(p.yzx*f+i+z+x*t)/f);
o=tanh(o/3e1);
What it actually is, decoded:
- p.y *= .3 SQUASHES the vertical axis before five cos octaves, so every
structure stretches into tall streaks — the falling-water grain.
- x is built by x.x += 9. on a zero-init local, so x*t = (9t,0,0): the
octave phases scroll at NINE times the clock. That rush is the fall.
- mix(c,p,.3) blends the turbulent point back toward the clean ray
point — only 30% of the displacement survives, a film of foam over a
coherent surface.
- abs(p.z+p.x+16.+tanh(p.y)/.1) is a SIGMOID CLIFF: two diagonal planes
(x+z = -6 low, -26 high) blended by 10*tanh(y), with a traveling
ripple sin(p.x-p.z+2t) running across the face.
- The palette (cos(p.x*.2+f+(6,1,2))+2)/f/z hangs vertical hue sheets
on the face, bright where the march grazes the surface.
Port decisions, each one a documented trap or rule in the README:
- THE ORB IS THE OBJECT: the cliff face becomes the sphere's own shell,
so the film flows down the BALL — a fountain, not a wall in a jar.
The squash, the 9t rush and the ripple all carry over unchanged; only
the geometry they decorate is swapped for the orb itself.
- The golfed listing relies on x, c, p, i, z, f starting at zero —
uninitialised locals are UNDEFINED in GLSL ES 1.0, explicit here.
- f can reach exactly ZERO (abs at the surface, ripple at -1, +1) and
the weight divides by it — guarded, as is the march step so it cannot
stall.
- Clocks enter only as additive phase. The rush gets its own integrated
clock so flow speed tunes without jumping the film.
---------------------------------------------------------------------------- */
/*
* Step counts are `#define`s: ES 1.0 requires constant loop bounds.
* TURB is 5 to match the original's octaves (divisors 1.3 .. 5.3).
*/
const FALLS_FRAG = `
#define STEPS 50
#define TURB 5
#define AA 1
// Volume-reactive values, resolved once per fragment in main().
float fallsFoam;
float fallsExposure;
mat2 fallsRot(float a) {
float c = cos(a);
float s = sin(a);
return mat2(c, -s, s, c);
}
vec3 fallsRender(vec2 fragCoord) {
float animTime = uP_speed; // integrated clock: ripple phase
float flow = uP_flow; // integrated clock: the 9t rush, tunable
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
vec3 ro = vec3(0.0, 0.0, uP_camDist);
vec3 rd = normalize(vec3(uv, -uP_focal));
float rShell = uP_envRadius * 0.92;
vec3 acc = vec3(0.0);
// transmittance carried front-to-back — near foam veils far foam
float T = 1.0;
// march only the span the envelope can light, as in shdr-01
float z = max(uP_camDist - uP_envRadius * 1.3, 0.0);
float zEnd = uP_camDist + uP_envRadius * 1.3;
for (int it = 0; it < STEPS; it++) {
vec3 c = ro + rd * z;
// a slight static tilt of the flow axis
c.yz = fallsRot(uP_tilt) * c.yz;
/*
The fall grain: squash the vertical axis, then the five octaves with
the rush phase on the first component — cos(p.yzx*f + ...) writes
that component to x, so height and time drive the sideways waves,
exactly the original's x*t construction.
*/
vec3 p = c;
p.y *= uP_stretch;
for (int j = 0; j < TURB; j++) {
float fj = float(j) + 1.3;
p += cos(p.yzx * fj + float(it) + z + vec3(flow, 0.0, 0.0)) / fj;
}
// the foam blend — most of the displacement is thrown away, leaving a
// film of detail over a coherent surface
vec3 pm = mix(c, p, fallsFoam);
/*
The surface, swapped from the sigmoid cliff to the ball's own shell:
distance to the sphere (sharpened by uP_wall) plus the original's
traveling ripple. f can still reach zero exactly — the guard feeds
both the division and the march step.
*/
float f = uP_stepScale * (abs(length(pm) - rShell) * uP_wall
+ sin(pm.x - pm.z + animTime * 2.0) + 1.0);
f = max(f, 1e-3);
z += f;
/*
Vertical hue sheets from the listing, bright where the march grazes
the film. The CLAMP is load-bearing, as in every accumulator here —
one f-null step would own the whole 50-step sum.
*/
vec3 w = (cos(pm.x * uP_hueScale + f + vec3(6.0, 1.0, 2.0)) + 2.0) / f / max(z, 1.0);
w = min(w, vec3(uP_stepClamp));
// envelope: plateau through the ball, cut 12% past the radius so the
// analytic silhouette in main() still has emission left to cut
float env = smoothstep(uP_envRadius * 1.12, uP_envRadius * uP_envCore, length(ro + rd * z));
w = (w + uP_fill) * env;
acc += T * w;
T *= exp(-dot(w, vec3(0.299, 0.587, 0.114)) * uP_scatter);
if (T < 0.004 || z > zEnd) break;
}
return acc;
}
void main() {
fallsFoam = uP_foam * (1.0 + 0.5 * uInput);
fallsExposure = uP_exposure * (1.0 - 0.35 * uOutput);
vec3 acc = vec3(0.0);
#if AA > 1
for (int mx = 0; mx < AA; mx++) {
for (int my = 0; my < AA; my++) {
vec2 offset = vec2(float(mx), float(my)) / float(AA) - 0.5;
acc += fallsRender(gl_FragCoord.xy + offset);
}
}
acc /= float(AA * AA);
#else
acc = fallsRender(gl_FragCoord.xy);
#endif
// tanh tone map per channel — the golfed /3e1 knee is a tunable here
vec3 col = tanh3(acc / max(fallsExposure, 1.0));
col = pow(clamp(col, 0.0, 1.0), vec3(uP_contrast));
// saturation about luminance, then the tint
float lum = dot(col, vec3(0.299, 0.587, 0.114));
col = mix(vec3(lum), col, uP_saturation);
col *= uC_tint;
// alpha from the brightest channel, not luminance — a deep blue sheet
// has low luminance but must not go transparent
float peak = max(col.r, max(col.g, col.b));
float a = clamp(peak * uP_alphaGain, 0.0, 1.0);
// Analytic silhouette — identical construction to shdr-01: exact
// ray-to-centre distance against the radius, colour AND alpha.
vec3 mrd = normalize(vec3(orbUV(), -uP_focal));
float closest = length(cross(vec3(0.0, 0.0, uP_camDist), mrd));
float band = mix(0.35, 0.012, clamp(uP_edge, 0.0, 1.0));
float mask = 1.0 - smoothstep(uP_envRadius * (1.0 - band), uP_envRadius * 1.005, closest);
col *= mask;
a *= mask;
// safety taper at the frame boundary — colour as well as alpha
float r2d = length(orbUV());
float fade = 1.0 - smoothstep(uP_edgeFade, 1.0, r2d);
col *= fade;
a *= fade;
// Emitted light, so rgb is already premultiplied — do NOT scale by alpha
// again (see the same note in shdr-31).
gl_FragColor = vec4(col, a);
}
`;
export const shdr20Orb: OrbVariant = {
key: "shdr-20",
label: "SHDR-20",
note: "a water film rushing down the ball, fountain-style",
frag: FALLS_FRAG,
params: [
{ key: "speed", label: "Ripple speed", min: 0.015, max: 10, step: 0.05, default: 0.5, integrate: true },
{ key: "flow", label: "Fall rush", min: 0, max: 20, step: 0.1, default: 3, integrate: true },
{ key: "camDist", label: "Camera distance", min: 1, max: 50, step: 0.3, default: 7 },
{ key: "focal", label: "Lens", min: 0.15, max: 15, step: 0.1, default: 2.25 },
{ key: "tilt", label: "Flow tilt", min: 0, max: 4, step: 0.02, default: 0.15 },
{ key: "stretch", label: "Fall stretch", min: 0.03, max: 3, step: 0.015, default: 0.3 },
{ key: "foam", label: "Foam", min: 0, max: 3, step: 0.015, default: 0.3 },
{ key: "wall", label: "Film sharpness", min: 0.15, max: 20, step: 0.1, default: 3 },
{ key: "stepScale", label: "Step scale", min: 0.015, max: 1.5, step: 0.01, default: 0.2 },
{ key: "hueScale", label: "Hue banding", min: 0, max: 3, step: 0.015, default: 0.85 },
{ key: "envRadius", label: "Envelope radius", min: 0.15, max: 15, step: 0.1, default: 2.6 },
{ key: "envCore", label: "Envelope core", min: 0.3, max: 1.02, step: 0.01, default: 1 },
{ key: "fill", label: "Body fill", min: 0, max: 100, step: 0.3, default: 0.4 },
{ key: "stepClamp", label: "Step clamp", min: 0.3, max: 300, step: 1.5, default: 20 },
{ key: "scatter", label: "Diffusion", min: 0, max: 0.5, step: 0.003, default: 0.01 },
{ key: "exposure", label: "Exposure", min: 1.5, max: 1500, step: 10, default: 22 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 15, step: 0.1, default: 1.15 },
{ key: "saturation", label: "Saturation", min: 0, max: 4, step: 0.02, default: 1.55 },
{ key: "alphaGain", label: "Alpha gain", min: 0.05, max: 15, step: 0.1, default: 2 },
{ key: "edge", label: "Edge sharpness", min: 0, max: 1, step: 0.01, default: 1 },
{ key: "edgeFade", label: "Halo falloff", min: 0.1, max: 3, step: 0.015, default: 0.98 }
],
colors: [{ key: "tint", label: "Tint", default: "#ffffff" }],
statePresets: {
idle: {
speed: 0.5,
flow: 3,
foam: 0.3,
exposure: 22,
scatter: 0.01,
alphaGain: 2
},
thinking: {
speed: 0.6,
flow: 3.3,
foam: 0.33,
exposure: 21,
scatter: 0.0095,
alphaGain: 2.1
},
// loudest: full rush, thick foam, hot film
speaking: {
speed: 1,
flow: 5.5,
foam: 0.45,
exposure: 16,
scatter: 0.0075,
alphaGain: 2.5
}
}
};
export type Shdr20Props = Omit<ShaderOrbProps, "variant">;
export function Shdr20({ size = 280, ...rest }: Shdr20Props) {
return <ShaderOrb variant={shdr20Orb} size={size} {...rest} />;
}
export default Shdr20;
23. components/ui/shdr-21.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-21 — light diffusing through a cloud.
A real volumetric integration rather than a surface. The march walks a
density field bounded by a sphere and, at every step, does three things:
TRANSMITTANCE how much of the background still gets through, tracked as
T *= exp(-density * dt * absorb). Beer-Lambert.
SHADOW a short second march toward the light, so the far side of a
dense clump is dimmer than the lit side. This is the whole
reason the orb reads as volume and not as a flat glow.
IN-SCATTER light added at this step, weighted by density, by the shadow
term, and by a Henyey-Greenstein phase function.
The phase function is what makes it feel like light rather than paint. It
biases scattering forward, so the limb facing the light blooms and the rest
stays soft — the same reason a cloud is blinding when you look toward the sun
through it and merely bright otherwise.
Alpha is 1 - T, which is exactly what the volume occludes, so this one needs
no radial fade to hide the canvas edge: density falls to zero at the sphere
boundary and the alpha goes with it.
---------------------------------------------------------------------------- */
/*
* Step counts are `#define`s: ES 1.0 requires constant loop bounds. The march
* is STEPS * (1 + LIGHT_STEPS) density evaluations, so LIGHT_STEPS is the
* expensive knob — 4 is enough for readable self-shadowing.
*/
const NIMBUS_FRAG = `
#define STEPS 56
#define LIGHT_STEPS 4
#define DENSITY_OCT 4
#define AA 1
const float PI = 3.14159265359;
// Volume-reactive values, resolved once per fragment in main().
float nimbusPower;
float nimbusDensity;
/*
Density inside the sphere.
The radial term falls to zero at the boundary, which both bounds the volume
and gives the soft edge for free. The cos-warp folds the sample point a few
times — the same cheap turbulence the other orbs use — and the threshold
carves that into clumps rather than an even fog.
*/
float density(vec3 p, float animTime) {
float shell = 1.0 - length(p) / uP_radius;
if (shell <= 0.0) return 0.0;
vec3 q = p * uP_scale;
float f = 1.0;
for (int k = 0; k < DENSITY_OCT; k++) {
q += cos(q.yzx * f + animTime * uP_churn) / f;
f *= 1.8;
}
float n = (sin(q.x) + sin(q.y) + sin(q.z)) / 3.0 * 0.5 + 0.5;
// smoothstep against the threshold is the clump control: high threshold
// leaves sparse wisps, low fills the sphere with even fog
float clump = smoothstep(uP_threshold, 1.0, n);
return clump * pow(shell, uP_edgeSoft) * nimbusDensity;
}
/*
Henyey-Greenstein: g > 0 biases scattering forward, which is what gives the
bloom on the limb facing the light.
The physical form carries a 1/(4*PI) normalisation. It is dropped here and
folded into uP_power instead — kept in, the whole term sits around 0.02 and
the orb renders black unless power is pushed into the hundreds, which makes
the slider useless.
*/
float phaseHG(float c, float g) {
float g2 = g * g;
return (1.0 - g2) / pow(max(1.0 + g2 - 2.0 * g * c, 0.0001), 1.5);
}
vec4 nimbusRender(vec2 fragCoord) {
float animTime = uP_speed; // integrated clock
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
vec3 ro = vec3(0.0, 0.0, -uP_camDist);
vec3 rd = normalize(vec3(uv, uP_focal));
/*
Light direction, slowly orbiting so the shading is never static.
The z term is kept POSITIVE — the camera looks along +z, so a light also
pointing along +z sits behind the cloud. That is the back-lit case, where
dot(rd, L) approaches 1 and the forward-scattering phase blooms. Put the
light on the camera's side instead and every ray samples the phase function
on its back-scatter tail, where it is roughly ten times smaller, and the orb
goes muddy.
*/
vec3 L = normalize(vec3(
cos(animTime * uP_lightSpin) * 0.7,
0.45,
sin(animTime * uP_lightSpin) * 0.35 + 0.65
));
float phase = phaseHG(dot(rd, L), uP_aniso);
// Start the march at the sphere's front face instead of the camera — every
// step before that contributes nothing, and at 56 steps they are expensive.
float toCentre = uP_camDist;
float tStart = max(toCentre - uP_radius, 0.0);
float span = 2.0 * uP_radius;
float dt = span / float(STEPS);
float T = 1.0;
vec3 scattered = vec3(0.0);
for (int i = 0; i < STEPS; i++) {
float t = tStart + (float(i) + 0.5) * dt;
vec3 p = ro + rd * t;
float dn = density(p, animTime);
if (dn > 0.001) {
// short march toward the light for self-shadowing
float shadow = 1.0;
float lstep = uP_radius / float(LIGHT_STEPS);
for (int k = 1; k <= LIGHT_STEPS; k++) {
vec3 lp = p + L * (float(k) - 0.5) * lstep;
shadow *= exp(-density(lp, animTime) * lstep * uP_shadowAbsorb);
}
/*
In-scattered light: warm where lit, cool where the volume shadows
itself.
The shadow term appears ONCE, inside the mix. Multiplying by it again
as a factor — the obvious-looking thing to write — scales the shadowed
end of the mix toward zero, so the cool colour is always multiplied
away and the cloud comes out monochrome beige however it is tinted.
uP_shadowLift is how much light still reaches the shadowed side.
*/
vec3 lit = mix(uC_shadow * uP_shadowLift, uC_light, shadow);
scattered += T * dn * dt * lit * phase * nimbusPower;
T *= exp(-dn * dt * uP_absorb);
if (T < 0.01) break;
}
}
// a soft ambient body so the unlit side is not pure black
float body = 1.0 - T;
scattered += uC_shadow * body * uP_ambient;
return vec4(scattered, body);
}
void main() {
/*
Agent output turns the light up; user input thickens the cloud. Both are
AMPLITUDES. Churn is deliberately NOT volume-scaled: it multiplies the
accumulated clock into a phase (animTime * churn), so scaling it by the
live volume would turn every volume wobble into a phase jump the size of
the whole clock — the cloud scrambles chaotically on each state change
instead of gliding, and gets worse the longer the page is open.
*/
nimbusPower = uP_power * (0.7 + 0.9 * uOutput);
nimbusDensity = uP_density * (1.0 + 0.35 * uInput);
vec4 acc = vec4(0.0);
#if AA > 1
for (int mx = 0; mx < AA; mx++) {
for (int my = 0; my < AA; my++) {
vec2 offset = vec2(float(mx), float(my)) / float(AA) - 0.5;
acc += nimbusRender(gl_FragCoord.xy + offset);
}
}
acc /= float(AA * AA);
#else
acc = nimbusRender(gl_FragCoord.xy);
#endif
vec3 col = tanh3(acc.rgb * uP_exposure);
float a = clamp(acc.a * uP_alphaGain, 0.0, 1.0);
// Emitted/scattered light, so rgb is already premultiplied — do NOT multiply
// by alpha again (see the same note in shdr-31).
gl_FragColor = vec4(col, a);
}
`;
export const shdr21Orb: OrbVariant = {
key: "shdr-21",
label: "SHDR-21",
note: "light diffusing through a cloud",
frag: NIMBUS_FRAG,
params: [
{ key: "speed", label: "Anim speed", min: 0.015, max: 10, step: 0.05, default: 10, integrate: true },
{ key: "camDist", label: "Camera distance", min: 0.5, max: 40, step: 0.2, default: 4.4 },
{ key: "focal", label: "Lens", min: 0.3, max: 15, step: 0.1, default: 1.8 },
{ key: "radius", label: "Cloud radius", min: 0.15, max: 10, step: 0.05, default: 2 },
{ key: "scale", label: "Cloud scale", min: 0.1, max: 15, step: 0.1, default: 0.8 },
{ key: "churn", label: "Churn", min: 0, max: 5, step: 0.03, default: 0.3 },
{ key: "threshold", label: "Clumping", min: 0, max: 3, step: 0.015, default: 0.075 },
{ key: "edgeSoft", label: "Edge softness", min: 0.1, max: 10, step: 0.05, default: 0.8 },
{ key: "density", label: "Density", min: 0.03, max: 20, step: 0.1, default: 3.2 },
{ key: "absorb", label: "Absorption", min: 0.03, max: 15, step: 0.1, default: 1.4 },
{ key: "shadowAbsorb", label: "Shadow depth", min: 0, max: 20, step: 0.1, default: 2.4 },
{ key: "shadowLift", label: "Shadow lift", min: 0, max: 5, step: 0.03, default: 0.55 },
{ key: "aniso", label: "Forward scatter", min: -0.9, max: 0.9, step: 0.01, default: 0.45 },
{ key: "lightSpin", label: "Light orbit", min: 0, max: 3, step: 0.015, default: 0.12 },
{ key: "power", label: "Light power", min: 0.03, max: 40, step: 0.2, default: 1.9 },
{ key: "ambient", label: "Ambient", min: 0, max: 3, step: 0.015, default: 0.12 },
{ key: "exposure", label: "Exposure", min: 0.03, max: 10, step: 0.05, default: 1 },
{ key: "alphaGain", label: "Alpha gain", min: 0.05, max: 10, step: 0.05, default: 1.5 }
],
/*
* The engine uploads these as uC_<key> vec3 uniforms. Warm light against a
* cool shadow is what reads as depth — a single-hue cloud looks flat however
* well it is shadowed.
*/
colors: [
{ key: "light", label: "Light", default: "#ffd7a3" },
{ key: "shadow", label: "Shadow", default: "#3a4a8c" }
],
statePresets: {
/*
Every state shares the same speed, geometry and cloud shape — only the
AMBIENCE and the palette move, so switching state relights the cloud
instead of restaging it. The engine glides params and cross-fades
colours on one shared easing, so the change reads as a mood shift.
*/
idle: {
ambient: 0.12,
power: 1.9,
shadowLift: 0.55
},
thinking: {
ambient: 0.22,
power: 2.15,
shadowLift: 0.65
},
speaking: {
ambient: 0.46,
power: 3.1,
shadowLift: 0.95
}
},
/*
The palette carries the rest of the state read: a warm lamp over cool
shadow at rest, shifting violet while it thinks, and burning hot while
speaking.
*/
stateColors: {
idle: { light: "#ffd7a3", shadow: "#3a4a8c" },
thinking: { light: "#e6d4ff", shadow: "#3b3f96" },
speaking: { light: "#ffb066", shadow: "#7a2f6e" }
}
};
export type Shdr21Props = Omit<ShaderOrbProps, "variant">;
export function Shdr21({ size = 280, ...rest }: Shdr21Props) {
return <ShaderOrb variant={shdr21Orb} size={size} {...rest} />;
}
export default Shdr21;
24. components/ui/shdr-22.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-22 — field lines swirling around the ball about a wandering axis.
Ported from a golfed twigl listing:
for(float i,z,d;i++<7e1;o+=vec4(9,i,z,1)/d){
vec3 p=z*normalize(FC.rgb*2.-r.xyy),
a=normalize(sin(t/4.+vec3(0,2,4))),v;
p.z+=7.;
v=a=dot(a,p)*a+cross(a,p);
for(d=2.;d++<9.;a+=sin(ceil(a*d)-t).yzx/d);
z+=d=.1*length(sin(a*a))*sqrt(length(v*sin(v.yzx)));}
o=tanh(o/6e4);
What it actually is, decoded:
- dot(a,p)*a + cross(a,p) with unit a is Rodrigues at EXACTLY 90 degrees
— a real orthonormal rotation, for once not a golf approximation. The
axis a = normalize(sin(t/4 + (0,2,4))) wanders slowly with time.
- v = a = ... forks the rotated point: v stays CLEAN, a takes seven
octaves of turbulence. The density then multiplies a turbulent factor,
length(sin(a*a)), by a clean one, sqrt(length(v*sin(v.yzx))) — big
smooth streak surfaces detailed by turbulence.
- The turbulence is sin(ceil(a*d) - t): CELL-QUANTIZED. Every lattice
cell flickers on its own phase — the voxel shimmer that gives the
effect its name.
- vec4(9,i,z,1)/d colour-codes the march itself: red is constant, green
is the STEP INDEX, blue is DEPTH — cores run red-orange, deep tails go
green-blue. The listing's alpha (1/d) is never displayed; this port
derives its own.
- p.z += 7 puts the camera OUTSIDE already — this listing wanted to be
an orb.
Port decisions, each one a documented trap or rule in the README:
- THE ORB IS THE OBJECT: unbounded, the tangle is a storm in a jar. The
field coordinates are pulled onto the sphere's own shell BEFORE
evaluation — mix(q, normalize(q)*R, hug) — so the streaks read as
field lines wrapped around the ball, swirling about the wandering
axis. The envelope and analytic silhouette bound the residue.
- The golfed listing relies on i, z, d, v starting at zero —
uninitialised locals are UNDEFINED in GLSL ES 1.0, explicit here.
- The clock enters only as additive phase (the axis wander gets its own
integrated clock, so its rate tunes without jumping the cells).
- 1/d spikes where both density factors null together — clamped per
step, as in every accumulator here.
---------------------------------------------------------------------------- */
/*
* Step counts are `#define`s: ES 1.0 requires constant loop bounds.
* TURB is 7 to match the original's octaves (d = 3..9).
*/
const VECTORS_FRAG = `
#define STEPS 70
#define TURB 7
#define AA 1
// Volume-reactive values, resolved once per fragment in main().
float vectorsTurb;
float vectorsExposure;
float vectorsGlow;
vec3 vectorsRender(vec2 fragCoord) {
float animTime = uP_speed; // integrated clock: cell flicker phase
float wander = uP_wander; // integrated clock: axis drift
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
vec3 ro = vec3(0.0, 0.0, uP_camDist);
vec3 rd = normalize(vec3(uv, -uP_focal));
// the wandering rotation axis — unit by construction, which is what
// makes the 90-degree Rodrigues below exact
vec3 axis = normalize(sin(wander + vec3(0.0, 2.0, 4.0)));
vec3 acc = vec3(0.0);
// transmittance carried front-to-back — near streaks veil far ones
float T = 1.0;
// march only the span the envelope can light, as in shdr-01
float z = max(uP_camDist - uP_envRadius * 1.3, 0.0);
float zEnd = uP_camDist + uP_envRadius * 1.3;
for (int it = 0; it < STEPS; it++) {
vec3 p = ro + rd * z;
/*
THE ORB IS THE OBJECT — pull the sample toward the sphere's shell
before the field ever sees it. At hug 0 this is the raw 3D tangle;
at 1 the field is purely angular, painted on the ball's skin. The
guard keeps normalize() defined through the centre.
*/
float rl = max(length(p), 1e-3);
vec3 q = mix(p, p / rl * uP_envRadius, uP_hug);
// the exact 90-degree rotation about the wandering axis; v stays
// clean, a takes the turbulence — the fork is the original's v=a=...
vec3 v = dot(axis, q) * axis + cross(axis, q);
vec3 a = v;
// cell-quantized turbulence: every ceil() lattice cell flickers on
// its own phase
for (int j = 0; j < TURB; j++) {
float dj = float(j) + 3.0;
a += vectorsTurb * sin(ceil(a * dj) - animTime).yzx / dj;
}
// the density product — turbulent detail times clean streak surfaces
float d = uP_stepScale * length(sin(a * a)) * sqrt(length(v * sin(v.yzx)));
d = max(d, 1e-4);
/*
The march's own colour code, from the listing: red constant, green
by step index, blue by depth into the ball — with the two ramps
exposed. The CLAMP is load-bearing as everywhere: one grazing step
would own the whole 70-step sum at some phases. Green gets twice the
clamp headroom: its ramp runs to STEPS (70) where red is fixed at 9,
and an equal clamp would crush the step gradient first.
*/
vec3 w = vec3(9.0, float(it) * uP_hueStep, (z - uP_camDist + uP_envRadius) * uP_hueDepth) / d;
w = min(w, vec3(uP_stepClamp) * vec3(1.0, 2.0, 1.0));
/*
Normalize the clamped weight back to family units (a ceiling of ~20,
like the sibling orbs). This orb's raw weights run in the hundreds —
the golfed knee was 6e4 — and without this one line the clamp value
leaks into total energy, so Exposure, Body fill and Diffusion would
all change meaning whenever the clamp moves. Normalized, stepClamp
is a pure dynamic-range knob: low flattens the streaks, high lets
the 1/d spikes whiten.
*/
w *= 20.0 / max(uP_stepClamp, 1.0);
/*
A few vectors GLOW. Cells of the clean rotated frame are hashed, and
each cell's hash cycles against the clock so only a small fraction
(uP_glowFew of the cycle) are hot at any moment. Where a hot cell
meets a streak null, the same 1/d spike is re-read on a far higher
ceiling than the step clamp — deliberately bypassing it — and pushed
as warm-white light. The two smoothsteps make a triangle window, so
each glow blooms and fades instead of popping at the cycle wrap.
*/
float few = max(uP_glowFew, 1e-3);
vec3 vc = ceil(v * 2.0);
float hcell = hash(vc.xy + vc.z * vec2(7.31, 3.17));
float cyc = fract(hcell + animTime * 0.05);
float sel = smoothstep(1.0 - few, 1.0 - 0.5 * few, cyc) * smoothstep(1.0, 1.0 - 0.5 * few, cyc);
// QUADRATIC in 1/d, unlike the linear base weight — the glow hugs the
// filament core and falls off fast, a hot wire rather than a lit sector
w += vec3(1.0, 0.96, 0.88) * min(0.08 / (d * d), 500.0) * sel * vectorsGlow;
// envelope: plateau through the ball, cut 12% past the radius so the
// analytic silhouette in main() still has emission left to cut
float env = smoothstep(uP_envRadius * 1.12, uP_envRadius * uP_envCore, length(p));
w = (w + uP_fill) * env;
acc += T * w;
T *= exp(-dot(w, vec3(0.299, 0.587, 0.114)) * uP_scatter);
z += d;
if (T < 0.004 || z > zEnd) break;
}
return acc;
}
void main() {
vectorsTurb = uP_turb * (1.0 + 0.5 * uInput);
vectorsExposure = uP_exposure * (1.0 - 0.35 * uOutput);
// the glows flare when the agent speaks
vectorsGlow = uP_glow * (1.0 + 0.8 * uOutput);
vec3 acc = vec3(0.0);
#if AA > 1
for (int mx = 0; mx < AA; mx++) {
for (int my = 0; my < AA; my++) {
vec2 offset = vec2(float(mx), float(my)) / float(AA) - 0.5;
acc += vectorsRender(gl_FragCoord.xy + offset);
}
}
acc /= float(AA * AA);
#else
acc = vectorsRender(gl_FragCoord.xy);
#endif
// tanh tone map per channel — the envelope and transmittance change the
// accumulator's scale, so the golfed /6e4 knee is a tunable here
vec3 col = tanh3(acc / max(vectorsExposure, 1.0));
col = pow(clamp(col, 0.0, 1.0), vec3(uP_contrast));
// saturation about luminance, then the tint
float lum = dot(col, vec3(0.299, 0.587, 0.114));
col = mix(vec3(lum), col, uP_saturation);
col *= uC_tint;
// alpha from the brightest channel, not luminance — a deep blue tail
// has low luminance but must not go transparent
float peak = max(col.r, max(col.g, col.b));
float a = clamp(peak * uP_alphaGain, 0.0, 1.0);
// Analytic silhouette — identical construction to shdr-01: exact
// ray-to-centre distance against the radius, colour AND alpha.
vec3 mrd = normalize(vec3(orbUV(), -uP_focal));
float closest = length(cross(vec3(0.0, 0.0, uP_camDist), mrd));
float band = mix(0.35, 0.012, clamp(uP_edge, 0.0, 1.0));
float mask = 1.0 - smoothstep(uP_envRadius * (1.0 - band), uP_envRadius * 1.005, closest);
col *= mask;
a *= mask;
// safety taper at the frame boundary — colour as well as alpha
float r2d = length(orbUV());
float fade = 1.0 - smoothstep(uP_edgeFade, 1.0, r2d);
col *= fade;
a *= fade;
// Emitted light, so rgb is already premultiplied — do NOT scale by alpha
// again (see the same note in shdr-31).
gl_FragColor = vec4(col, a);
}
`;
export const shdr22Orb: OrbVariant = {
key: "shdr-22",
label: "SHDR-22",
note: "field lines swirling around the ball about a wandering axis",
frag: VECTORS_FRAG,
params: [
{ key: "speed", label: "Anim speed", min: 0.015, max: 10, step: 0.05, default: 0.5, integrate: true },
{ key: "wander", label: "Axis wander", min: 0, max: 3, step: 0.015, default: 0.12, integrate: true },
{ key: "camDist", label: "Camera distance", min: 1, max: 50, step: 0.3, default: 7 },
{ key: "focal", label: "Lens", min: 0.15, max: 15, step: 0.1, default: 2.25 },
{ key: "hug", label: "Surface hug", min: 0, max: 3, step: 0.015, default: 0.8 },
{ key: "turb", label: "Cell shimmer", min: 0, max: 5, step: 0.03, default: 0.6 },
{ key: "stepScale", label: "Step scale", min: 0.005, max: 1.5, step: 0.01, default: 0.07 },
{ key: "hueStep", label: "Step hue", min: 0, max: 10, step: 0.03, default: 0.12 },
{ key: "hueDepth", label: "Depth hue", min: 0, max: 10, step: 0.03, default: 1.4 },
{ key: "glow", label: "Vector glow", min: 0, max: 10, step: 0.05, default: 1.4 },
{ key: "glowFew", label: "Glow density", min: 0, max: 1.5, step: 0.01, default: 0.12 },
{ key: "envRadius", label: "Envelope radius", min: 0.15, max: 15, step: 0.1, default: 2.6 },
{ key: "envCore", label: "Envelope core", min: 0.3, max: 1.02, step: 0.01, default: 0.88 },
{ key: "fill", label: "Body fill", min: 0, max: 100, step: 0.3, default: 0.15 },
{ key: "stepClamp", label: "Step clamp", min: 3, max: 5000, step: 30, default: 800 },
{ key: "scatter", label: "Diffusion", min: 0, max: 0.5, step: 0.003, default: 0.01 },
{ key: "exposure", label: "Exposure", min: 1.5, max: 5000, step: 15, default: 260 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 15, step: 0.1, default: 1.3 },
{ key: "saturation", label: "Saturation", min: 0, max: 4, step: 0.02, default: 1.15 },
{ key: "alphaGain", label: "Alpha gain", min: 0.05, max: 15, step: 0.1, default: 2 },
{ key: "edge", label: "Edge sharpness", min: 0, max: 1, step: 0.01, default: 1 },
{ key: "edgeFade", label: "Halo falloff", min: 0.1, max: 3, step: 0.015, default: 0.98 }
],
colors: [{ key: "tint", label: "Tint", default: "#ffffff" }],
/*
The states are staged on this orb's two best levers, both phase-safe
integrated clocks: the AXIS WANDER (the whole field re-orients as the
axis moves) and the cell-flicker speed. glowFew is the third lever —
how many of the hot-wire vectors are lit at once.
*/
statePresets: {
// calm: slow shimmer, near-still axis, a few soft glows
idle: {
speed: 0.4,
wander: 0.1,
turb: 0.55,
glow: 1.3,
glowFew: 0.12,
exposure: 260,
scatter: 0.01,
alphaGain: 2
},
/*
searching: the axis HUNTS — wander runs six times idle, so the field
lines continuously re-orient as if trying directions — while the
glows go SPARSER but sharper: rare single sparks, ideas catching.
*/
thinking: {
speed: 1.2,
wander: 0.6,
turb: 0.7,
glow: 1.7,
glowFew: 0.07,
exposure: 230,
scatter: 0.0095,
alphaGain: 2.1
},
/*
answering: the axis settles (it found the direction) and the energy
moves to the field itself — fast flicker, many hot wires at once
(glowFew 0.22, further flared by the output volume), bright.
*/
speaking: {
speed: 2.2,
wander: 0.3,
turb: 0.85,
glow: 2.4,
glowFew: 0.22,
exposure: 170,
scatter: 0.0075,
alphaGain: 2.5
}
},
// the tint carries the at-a-glance read, as in chords: neutral at rest,
// cooled while searching, warmed while answering
stateColors: {
idle: { tint: "#ffffff" },
thinking: { tint: "#c3d2ff" },
speaking: { tint: "#ffd9c4" }
}
};
export type Shdr22Props = Omit<ShaderOrbProps, "variant">;
export function Shdr22({ size = 280, ...rest }: Shdr22Props) {
return <ShaderOrb variant={shdr22Orb} size={size} {...rest} />;
}
export default Shdr22;
25. components/ui/shdr-23.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-23 — an ASCII glyph matrix in CRT green, wrapped on the ball.
The terminal renderer of the family. The screen is a fixed grid of glyph
cells, and each cell draws a woven character — stacked horizontal dashes
split by thin vertical gaps — whose LIT ROW COUNT quantizes the field
behind it. That is exactly how ASCII art shades: space, dot, dash, block.
The field itself is drifting fbm sampled through a stereographic
projection of the rotating dome, so the character weave streams and
compresses around the sphere. On top of it, a DROPOUT mask sampled on a
coarser super-grid eats blocky rectangular voids out of the matrix —
the field's dark zones become the chunky black holes of the reference
aesthetic rather than dim glyphs.
Construction notes:
- The cell grid is RESOLUTION-RELATIVE (uP_cells across the canvas), the
lesson shdr-14 learned: sized in device pixels, a gallery card
collapses to a few dozen blotches while the playground looks right.
- The FIELD is sampled once per cell (at the cell's centre) so a glyph
is one character, not a gradient; the glyph geometry itself renders
per fragment so its dashes stay crisp at any size. The dropout mask
snaps to 2x2 super-cells, which is what makes the voids blocky.
- Clocks enter only as additive phase (field drift) or through the dome
rotation (integrated spin clock) — phase-safe as everywhere here.
- Surface-lit and mask-bounded, so alpha IS coverage — premultiplied
output, as in shdr-28.
---------------------------------------------------------------------------- */
const PHOSPHOR_FRAG = `
void main() {
// Volume coupling: user input densifies the glyphs, agent output turns
// the phosphor up — the matrix visibly burns brighter while it speaks.
float densBias = uP_density + 0.2 * uInput;
float gainNow = uP_gain * (0.85 + 0.5 * uOutput);
// resolution-relative glyph grid — same character count at every size
float cellPx = max(min(uRes.x, uRes.y) / max(uP_cells, 8.0), 4.0);
vec2 cellIdx = floor(gl_FragCoord.xy / cellPx);
vec2 cellCentre = (cellIdx + 0.5) * cellPx;
vec2 g = fract(gl_FragCoord.xy / cellPx); // 0..1 inside the cell
vec2 suv = (2.0 * cellCentre - uRes) / min(uRes.x, uRes.y);
vec2 uv = suv / uP_radius;
float r2 = dot(uv, uv);
// blocky silhouette, cut on the cell grid like the rest of the matrix
float mask = 1.0 - step(1.0, r2);
float z = sqrt(max(1.0 - r2, 0.0));
vec3 n = vec3(uv, z);
// rotating dome, stereographic projection — the weave compresses toward
// the rim and rolls around the ball as the dome turns
float rot = uP_spin; // integrated clock
float cr = cos(rot);
float sr = sin(rot);
vec3 sp = vec3(n.x * cr - n.z * sr, n.y, n.x * sr + n.z * cr);
vec2 p2 = sp.xy / (abs(sp.z) + 1.2) * uP_scale * 3.0;
/*
Three motions, one per state, each on its OWN integrated clock so a
state change morphs the movement instead of jumping it:
DRIFT diagonal lava-flow streaming (idle)
SCROLL vertical paging, terminal-style (thinking)
PULSE radial waves radiating from centre (speaking)
The clocks are rates in the presets — a rate gliding to zero freezes
that motion in place, phase intact. The pulse's amplitude is a separate
non-integrated param, so idle carries no static rings.
*/
float driftT = uP_drift; // integrated clock: diagonal stream
float scrollT = uP_scroll; // integrated clock: vertical paging
float t = uP_speed; // integrated clock: pulse phase
vec2 flow = vec2(driftT * 0.6, -driftT * 0.45 - scrollT);
float field = fbm(p2 + flow);
float lambert = clamp(dot(n, normalize(vec3(-0.45, 0.55, 0.7))), 0.0, 1.0);
float dens = clamp((field - 0.5) * 1.8 + densBias + 0.4 * uP_light * lambert
+ uP_pulse * 0.35 * sin(length(uv) * 5.5 - t * 2.4), 0.0, 1.0);
/*
The glyph: four dash rows split by three stripe gaps. Rows light from
the bottom as density rises — the step() against the row index IS the
ASCII quantizer, so a cell is always a whole character.
*/
float rowI = floor(g.y * 4.0);
float bar = step(0.22, fract(g.y * 4.0)) * step(fract(g.y * 4.0), 0.9);
float stripe = step(0.18, fract(g.x * 3.0));
float lit = step(rowI + 0.5, dens * 4.0 * gainNow);
float glyph = bar * stripe * lit;
/*
Blocky dropouts: the same field, resampled on a 2x2 super-grid and
thresholded. Because whole super-cells fail together, the dark zones
become hard rectangular holes instead of dim characters.
*/
vec2 superCentre = (floor(cellIdx / 2.0) * 2.0 + 1.0) * cellPx;
vec2 sSuv = (2.0 * superCentre - uRes) / min(uRes.x, uRes.y);
vec2 sUv2 = sSuv / uP_radius;
float sz = sqrt(max(1.0 - dot(sUv2, sUv2), 0.0));
vec3 ssp = vec3(sUv2.x * cr - sz * sr, sUv2.y, sUv2.x * sr + sz * cr);
float superField = fbm(ssp.xy / (abs(ssp.z) + 1.2) * uP_scale * 3.0 + flow);
float keep = step(uP_dropout, superField + 0.15 * uOutput);
glyph *= keep;
// phosphor ramp: deep green floor to hot glow, whitening at the top end
vec3 glyphCol = mix(uC_deep, uC_glow, dens);
glyphCol += vec3(0.7, 1.0, 0.9) * pow(dens, 3.0) * 0.35;
// a dark body under the matrix plus a glow-coloured fresnel rim, so the
// orb reads as a solid ball and not loose characters
float fres = pow(1.0 - z, 2.2);
vec3 col = uC_deep * 0.22 + glyphCol * glyph + uC_glow * fres * uP_rim;
col = pow(max(col, 0.0), vec3(uP_contrast));
// Surface-lit orb bounded by a mask: alpha IS coverage, so premultiply —
// the opposite convention from the emissive orbs (see shdr-31).
float a = mask;
gl_FragColor = vec4(col * a, a);
}
`;
export const shdr23Orb: OrbVariant = {
key: "shdr-23",
label: "SHDR-23",
note: "an ASCII glyph matrix in CRT green, wrapped on the ball",
frag: PHOSPHOR_FRAG,
params: [
{ key: "drift", label: "Drift", min: 0, max: 10, step: 0.05, default: 0.55, integrate: true },
{ key: "scroll", label: "Scroll", min: 0, max: 10, step: 0.05, default: 0.05, integrate: true },
{ key: "speed", label: "Pulse rate", min: 0.015, max: 10, step: 0.05, default: 0.5, integrate: true },
{ key: "pulse", label: "Pulse depth", min: 0, max: 2, step: 0.01, default: 0 },
{ key: "spin", label: "Roll", min: 0, max: 5, step: 0.03, default: 0.12, integrate: true },
{ key: "radius", label: "Radius", min: 0.15, max: 3, step: 0.015, default: 0.9 },
{ key: "cells", label: "Glyph grid", min: 16, max: 120, step: 2, default: 40 },
{ key: "scale", label: "Field scale", min: 0.3, max: 10, step: 0.1, default: 1.6 },
{ key: "density", label: "Glyph density", min: 0, max: 2, step: 0.01, default: 0.48 },
{ key: "dropout", label: "Dropout", min: 0, max: 1, step: 0.01, default: 0.48 },
{ key: "light", label: "Key light", min: 0, max: 3, step: 0.015, default: 0.6 },
{ key: "rim", label: "Rim glow", min: 0, max: 3, step: 0.015, default: 0.45 },
{ key: "gain", label: "Phosphor gain", min: 0.05, max: 5, step: 0.05, default: 1 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 10, step: 0.05, default: 1 }
],
colors: [
{ key: "glow", label: "Glow", default: "#57ffc9" },
{ key: "deep", label: "Deep", default: "#0b3b2d" }
],
/*
Each state ANIMATES differently — its own kind of motion, not just its
own speed — and each has its own phosphor colour. Every motion has its own integrated clock, so a rate gliding to zero
freezes that motion in place with its phase intact; the pulse depth is
an amplitude.
*/
statePresets: {
/*
idle DRIFTS: slow diagonal lava-flow, lazy roll — on a far finer,
sparser screen than the working states. The glyph grid is nearly
doubled and the field scale tripled, with the density down by a third
and half the dropout, so the matrix reads as a fine violet mesh under
a strong key light and a bright rim, slightly dimmed.
*/
idle: {
drift: 0.55,
scroll: 0.05,
pulse: 0,
speed: 0.52,
spin: 0.12,
cells: 68,
scale: 5.1,
density: 0.31,
dropout: 0.24,
light: 1.11,
rim: 0.66,
gain: 0.9
},
/*
thinking STREAMS: the drift runs at six times idle with a steady
vertical scroll under it and a pulse near speaking depth, on a dome
almost stopped — the matrix pours across the ball rather than paging.
The finest grid of the three and the least dropout, so the field is
nearly solid, on idle's field scale with a touch more contrast.
*/
thinking: {
drift: 3.05,
scroll: 0.65,
pulse: 0.58,
speed: 0.45,
spin: 0.04,
cells: 86,
density: 0.33,
dropout: 0.13,
light: 0.855,
rim: 0.51,
contrast: 1.3,
gain: 0.95
},
/*
speaking PULSES, hard: radial waves at more than double the thinking
depth, driven at five times its rate, radiate through the glyphs while
the dome rolls at nearly a full spin. The grid goes to its finest and
the field scale past idle's, with the densest glyphs and the heaviest
dropout of the three — a coarse, flickering red screen — under a dim
key light with the phosphor gain tripled.
*/
speaking: {
drift: 0.4,
scroll: 0.1,
pulse: 1.27,
speed: 2.85,
spin: 1.05,
cells: 120,
scale: 7.2,
density: 0.56,
dropout: 0.59,
light: 0.39,
rim: 0.51,
gain: 2.95
}
},
// violet at rest, aqua while searching, red while answering
stateColors: {
idle: { glow: "#6a57ff" },
thinking: { glow: "#57ffe3" },
speaking: { glow: "#ff5757" }
}
};
export type Shdr23Props = Omit<ShaderOrbProps, "variant">;
export function Shdr23({ size = 280, ...rest }: Shdr23Props) {
return <ShaderOrb variant={shdr23Orb} size={size} {...rest} />;
}
export default Shdr23;
26. components/ui/shdr-24.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-24 — a Minecraft Earth: a voxel planet with continents, oceans and
biomes wrapped around the whole ball.
THE ORB IS THE OBJECT taken literally: instead of wrapping a block texture
onto a smooth ball, the ball is BUILT FROM BLOCKS. A DDA voxel march
(Amanatides & Woo) walks an axis-aligned grid in object space; a voxel is
solid wherever its centre sits below the terrain radius for its direction.
Up is RADIAL, as on a planet seen from space.
THE GROUND IS A PERFECT SPHERE. Terrain relief is strictly ADDITIVE —
max(noise - sea level, 0) — so oceans and coastal plains sit exactly on
the unit sphere and only the mountains climb above it. The silhouette
reads as a clean round globe with peaks and tree fuzz breaking the rim,
not as a lumpy rock. From space down:
- OCEANS: where the field sits below sea level the ground stays at the
sphere and its surface blocks render as water — flush on the globe,
lighter over coastal shallows, deep blue mid-ocean, shimmering on the
flow clock. SAND beaches band every shore.
- BIOMES: a continent-scale noise field paints deserts (sand runs deep,
no trees), plains (grass, scattered trees) and forests (dense cover).
Elevation cuts across all of them: rising ground bares brown
hillsides, and the peaks stand as naked stone.
- SEASONS: an integrated season clock carries the whole planet through
five worlds on a cycle — lush, CHERRY GROVE, ICE, MESA, desert —
crossfading two at a time (blossom thaws into snow, terracotta dries
into sand), racing while the orb thinks. The ice world is drawn from
the ice-spikes and frozen-peaks biomes: dense tapering packed-ice
spires instead of trees, glacier-blue patches over the risen faces,
elevation browns buried under snow, oceans stilled to a frozen
sheet. The mesa amplifies its relief into big flat-topped banded
towers with red-sand flats and cacti. The cherry grove covers vivid
meadows with broad flat blossom-pink puffs on short dark trunks.
- TREES: block trees on a cubemap-cell lattice — the direction is
projected onto its dominant cube face and quantized, so every voxel
on a radial line agrees which tree cell it is in. Trunks grow
radially, which is what fuzzes the silhouette green over forests;
canopy radius is re-hashed per voxel for ragged blocky foliage.
- STRATA under the surface: grass (or desert sand) on the outward
faces of surface blocks, MUD on their sides and for a band below —
mottled with hashed stone patches — then STONE, seamed with ORE.
- ORE VEINS: a coarse cell grid hashes multi-block clusters into the
deep stone — diamond (the tunable ore colour), lapis (a deep-blue
remap of it), or coal (unlit) — and each ore block is stone FLECKED
with the hue on its texture grain, like the actual ore tile. Only
the flecks glow.
- CAVES: a band of a second 3D field is carved to air, but ONLY where
the ground has risen — mountainsides get entrances, the smooth
lowland sphere stays pristine — and the rock warms toward a MOLTEN
CORE that blazes through the deep pits when the agent speaks.
Construction notes:
- The terrain noise is a tri-planar sum of the prelude's 2D value noise
(seam-free, no atan). Averaging three samples squeezes the
distribution toward 0.5, so the range is stretched back out — without
that the field never reaches the extremes where oceans and peaks live.
- The march is BOUNDED: rays intersect an analytic sphere around the
tallest possible tree first, so empty pixels cost two dot products.
- Faces are shaded flat off their axis-aligned normals — the Minecraft
read — plus a radial wrap term for planetary roundness, a crevice AO,
and a depth dimming that sinks cave interiors into darkness so the
ore and core glow read as underground.
- No fwidth, no round, constant loop bound with inner breaks —
GLSL ES 1.0 throughout, as everywhere in this repo.
- Surface-lit and hit-bounded, so alpha IS coverage — premultiplied
output (trivially: hits are opaque, misses are clear). The blocky
aliasing on the rim is the aesthetic, not a bug.
---------------------------------------------------------------------------- */
const CHUNK_FRAG = `
#define STEPS 160
// Per-fragment state, resolved once in main() before the march.
vec3 ckDrift;
float ckVs;
float ckMaxH;
float ckSeaN;
// Climate weights (lush, desert, ice, mesa) plus the cherry grove — a
// partition of unity driven by the integrated season clock — and the tree
// density they imply.
vec4 ckClim;
float ckCherry;
float ckTreeMul;
// Tree-cell lookup results (GLSL ES 1.0 has no out-struct ergonomics).
vec3 ckTreeDir;
float ckTreeH1;
float ckTreeH2;
mat2 ckRot(float a) {
float c = cos(a);
float s = sin(a);
return mat2(c, -s, s, c);
}
// Seam-free noise on the direction sphere: tri-planar sum of the prelude's
// 2D value noise, range-stretched (see the header) and clamped so the
// terrain bound stays a true bound.
float ckN3(vec3 p) {
float v = (noise(p.xy) + noise(p.yz + 19.1) + noise(p.zx + 47.3)) / 3.0;
return clamp(0.5 + (v - 0.5) * 1.9, 0.0, 1.0);
}
// The raw terrain field for a surface direction, 0..1. Two octaves only —
// the voxel grid quantizes away anything finer.
float ckField(vec3 dir) {
vec3 q = dir * uP_scale + ckDrift;
return ckN3(q) * 0.65 + ckN3(q * 2.6 + 31.7) * 0.35;
}
// Local terrain radius. Relief is strictly ADDITIVE above the unit sphere:
// oceans and plains sit exactly on it, mountains climb from the shoreline.
// Under the mesa climate the relief terraces into three-block steps —
// flat-topped buttes and benches, the badlands profile.
float ckTerrain(vec3 dir) {
// the mesa amplifies its relief into big banded towers
float h = 1.0 + uP_rough * max(ckField(dir) - ckSeaN, 0.0) * 1.2
* (1.0 + ckClim.w * 0.8);
float stepH = 3.0 * ckVs;
float hq = 1.0 + floor((h - 1.0) / stepH) * stepH;
return mix(h, hq, ckClim.w * 0.85);
}
// The continent-scale biome field: below 0.3 desert, above 0.58 forest,
// plains between. Drifts with the terrain so biomes move with their land.
float ckBiome(vec3 dir) {
return ckN3(dir * 1.3 + ckDrift + 57.9);
}
/*
Which tree cell does this direction fall in? The direction is projected
onto its dominant cube face and quantized there — every voxel along a
radial line lands in the same cell, which is what keeps a tree's trunk
and canopy agreeing across grid levels. The anchor direction is rebuilt
from the jittered cell centre.
*/
void ckTreeCell(vec3 dir) {
vec3 ad = abs(dir);
vec2 fuv;
float face;
if (ad.x >= ad.y && ad.x >= ad.z) {
fuv = dir.yz / ad.x;
face = dir.x > 0.0 ? 0.0 : 1.0;
} else if (ad.y >= ad.z) {
fuv = dir.xz / ad.y;
face = dir.y > 0.0 ? 2.0 : 3.0;
} else {
fuv = dir.xy / ad.z;
face = dir.z > 0.0 ? 4.0 : 5.0;
}
float grid = max(uP_blocks / 6.0, 2.0);
vec2 cell = floor((fuv * 0.5 + 0.5) * grid);
ckTreeH1 = hash(cell * 1.17 + face * 19.3);
ckTreeH2 = hash(cell * 0.71 + face * 7.7 + 9.3);
vec2 jit = vec2(hash(cell + 7.1 + face), hash(cell + 13.7 + face)) - 0.5;
vec2 auv = ((cell + 0.5 + jit * 0.3) / grid) * 2.0 - 1.0;
vec3 cp;
if (face < 1.5) cp = vec3(face < 0.5 ? 1.0 : -1.0, auv.x, auv.y);
else if (face < 3.5) cp = vec3(auv.x, face < 2.5 ? 1.0 : -1.0, auv.y);
else cp = vec3(auv.x, auv.y, face < 4.5 ? 1.0 : -1.0);
ckTreeDir = normalize(cp);
}
/*
The world function: what fills this voxel?
0 air 1 ground 2 trunk 3 leaves
Ground is the terrain sphere; caves are carved ONLY where the ground has
risen above the base sphere, so the smooth lowlands stay pristine. Water
is not a voxel here — ocean surface blocks are painted as water in the
material pass. Trees grow radially from dry anchors below the tree line,
dense where the biome says forest.
*/
float ckVoxel(vec3 cc) {
float r = length(cc);
vec3 dir = cc / max(r, 1.0e-4);
if (r < ckMaxH) {
float h = ckTerrain(dir);
if (r < h) {
// carve caves into risen ground only — mountainsides get entrances,
// the perfect lowland sphere keeps its silhouette
if (h > 1.0 + 1.5 * ckVs) {
float cv = ckN3(cc * (uP_scale * 1.9) + 71.3);
float cw = uP_cave * 0.16 * smoothstep(ckMaxH, ckMaxH - 0.45, r);
if (abs(cv - 0.5) < cw) return 0.0;
}
return 1.0;
}
}
// trees live in a thin shell above the tallest terrain
if (r < ckMaxH + 8.0 * ckVs && uP_trees > 0.001) {
ckTreeCell(dir);
float thrMax = clamp(uP_trees, 0.0, 1.0) * 0.8;
if (ckTreeH1 > 1.0 - thrMax) {
// forest density comes from the biome at the ANCHOR, so a whole
// tree agrees with itself about existing
float bioA = ckBiome(ckTreeDir);
float dens = bioA > 0.58 ? 1.0 : (bioA > 0.3 ? 0.25 : 0.0);
dens *= ckTreeMul; // forests thin out under desert, ice and mesa skies
if (ckTreeH1 > 1.0 - thrMax * dens) {
float fA = ckField(ckTreeDir);
float ha = 1.0 + uP_rough * max(fA - ckSeaN, 0.0) * 1.2;
// dry land only, below the stone tree line
if (fA > ckSeaN + 0.015 && ha < 1.0 + uP_rough * 0.42) {
float lat = length(cc - dot(cc, ckTreeDir) * ckTreeDir);
if (ckClim.z > 0.5) {
// ICE SPIKES: the lattice grows tapering packed-ice spires in
// place of trees. Squaring the height hash makes many stubs
// and a few tall spires, the ice-plains skyline.
float spikeH = (2.0 + 6.0 * ckTreeH2 * ckTreeH2) * ckVs;
float w = mix(1.15, 0.3, clamp((r - ha) / spikeH, 0.0, 1.0)) * ckVs;
if (lat < w && r > ha - ckVs && r < ha + spikeH) return 3.0;
} else if (ckClim.w > 0.5) {
// CACTI: short green columns dotting the badlands flats
float cacH = (1.5 + 2.0 * ckTreeH2) * ckVs;
if (lat < 0.6 * ckVs && r > ha - ckVs && r < ha + cacH) return 3.0;
} else if (ckCherry > 0.5) {
// CHERRY GROVE: broad flat blossom puffs on short dark trunks —
// the radial component of the canopy test is stretched, which
// squashes the puff wide and flat like the cherry grove trees
float trunkTop = ha + (2.0 + 1.5 * ckTreeH2) * ckVs;
if (lat < 0.75 * ckVs && r > ha - ckVs && r < trunkTop) return 2.0;
vec3 dd = cc - ckTreeDir * (trunkTop + 0.6 * ckVs);
dd += ckTreeDir * dot(dd, ckTreeDir) * 0.8;
vec3 lv = floor(cc / ckVs);
float rag = hash(lv.xy * 0.61 + lv.z * 2.23);
if (length(dd) < (2.2 + 0.5 * rag) * ckVs) return 3.0;
} else {
float trunkTop = ha + (2.5 + 2.0 * ckTreeH2) * ckVs;
if (lat < 0.75 * ckVs && r > ha - ckVs && r < trunkTop) return 2.0;
vec3 dd = cc - ckTreeDir * (trunkTop + 0.7 * ckVs);
// canopy radius re-hashed per voxel — ragged blocky foliage
vec3 lv = floor(cc / ckVs);
float rag = hash(lv.xy * 0.61 + lv.z * 2.23);
if (length(dd) < (1.7 + 0.5 * rag) * ckVs) return 3.0;
}
}
}
}
}
return 0.0;
}
void main() {
// Volume coupling: agent output stokes the glow, the gain and the molten
// core; user input brightens the key light.
float glowNow = uP_glow * (0.7 + 1.0 * uOutput);
float gainNow = uP_gain * (0.9 + 0.3 * uOutput);
float lightNow = uP_light * (1.0 + 0.3 * uInput);
// the terrain field drifts on its own integrated clock — in the thinking
// state it streams, and blocks pop in and out like chunks loading
ckDrift = vec3(uP_drift * 0.31, uP_drift * 0.17, -uP_drift * 0.23);
/*
CLIMATE: the integrated season clock carries the planet through four
worlds — lush, desert, ice, mesa — on a cycle. The triangular weights
overlap so exactly two adjacent climates crossfade at any moment, and
because the clock integrates, changing the season rate never snaps the
phase: the world just weathers faster or slower.
*/
// five worlds in crossfade order: lush, cherry, ice, mesa, desert —
// blossom thaws into snow, terracotta dries into sand
float t5 = fract(uP_season * 0.05) * 5.0;
ckClim = vec4(
clamp(1.0 - min(abs(t5), abs(t5 - 5.0)), 0.0, 1.0), // lush (wraps)
clamp(1.0 - abs(t5 - 4.0), 0.0, 1.0), // desert
clamp(1.0 - abs(t5 - 2.0), 0.0, 1.0), // ice
clamp(1.0 - abs(t5 - 3.0), 0.0, 1.0) // mesa
);
ckCherry = clamp(1.0 - abs(t5 - 1.0), 0.0, 1.0); // cherry grove
// ice and cherry run HIGH (dense spikes / dense groves); mesa keeps cacti
ckTreeMul = dot(ckClim, vec4(1.0, 0.15, 0.9, 0.3)) + ckCherry * 0.9;
ckVs = 2.0 / clamp(uP_blocks, 8.0, 96.0); // voxel size, planet radius 1
// sea level in FIELD space: 0.5 puts about half the sphere under water
ckSeaN = 0.25 + clamp(uP_sea, 0.0, 1.0) * 0.5;
// tallest possible terrain — sized for the mesa's amplified towers so
// the viewport holds steady while the seasons turn
ckMaxH = 1.0 + uP_rough * (1.0 - ckSeaN) * 1.2 * 1.8 + 0.001;
float bound = ckMaxH + 8.5 * ckVs; // ...plus the tree shell
vec2 uv = orbUV() / uP_radius;
// orthographic camera, viewport sized to the bound so the treetops fit
vec3 ro = vec3(uv * bound, 2.9);
vec3 rd = vec3(0.0, 0.0, -1.0);
// rotate the RAY into object space (inverse tumble) — the grid stays
// axis-aligned, the planet appears to spin. The light rotates along,
// keeping the sun fixed relative to the viewer.
mat2 tiltM = ckRot(uP_tilt); // positive tilt looks DOWN at the north pole
mat2 spinM = ckRot(-uP_spin); // integrated clock
ro.yz = tiltM * ro.yz;
ro.xz = spinM * ro.xz;
rd.yz = tiltM * rd.yz;
rd.xz = spinM * rd.xz;
vec3 Lo = normalize(vec3(-0.5, 0.7, 0.55));
Lo.yz = tiltM * Lo.yz;
Lo.xz = spinM * Lo.xz;
// DDA needs nonzero direction components — nudge, keep the sign
vec3 sgn = vec3(
rd.x >= 0.0 ? 1.0 : -1.0,
rd.y >= 0.0 ? 1.0 : -1.0,
rd.z >= 0.0 ? 1.0 : -1.0
);
rd = normalize(sgn * max(abs(rd), vec3(1.0e-4)));
// analytic bounding sphere: empty pixels exit here, and the march below
// only ever walks the chord inside the bound
float b = dot(rd, ro);
float c = dot(ro, ro) - bound * bound;
float disc = b * b - c;
if (disc < 0.0) {
gl_FragColor = vec4(0.0);
return;
}
float sq = sqrt(disc);
vec3 p0 = ro + rd * (-b - sq + ckVs * 0.001);
float tSpan = 2.0 * sq;
// Amanatides & Woo init: current voxel, per-axis distance to the next
// grid plane, per-axis crossing stride
vec3 vp = floor(p0 / ckVs);
vec3 tDelta = ckVs / abs(rd);
vec3 tMax = ((vp + step(vec3(0.0), rd)) * ckVs - p0) / rd;
float mat = 0.0;
vec3 mask = vec3(0.0, 0.0, 1.0); // first-voxel fallback: face the viewer
float tCur = 0.0;
for (int i = 0; i < STEPS; i++) {
float m = ckVoxel((vp + 0.5) * ckVs);
if (m > 0.5) {
mat = m;
break;
}
// step to the next voxel across the nearest grid plane
if (tMax.x < tMax.y && tMax.x < tMax.z) {
tCur = tMax.x;
tMax.x += tDelta.x;
vp.x += sgn.x;
mask = vec3(1.0, 0.0, 0.0);
} else if (tMax.y < tMax.z) {
tCur = tMax.y;
tMax.y += tDelta.y;
vp.y += sgn.y;
mask = vec3(0.0, 1.0, 0.0);
} else {
tCur = tMax.z;
tMax.z += tDelta.z;
vp.z += sgn.z;
mask = vec3(0.0, 0.0, 1.0);
}
if (tCur > tSpan) break; // left the bound: miss
}
if (mat < 0.5) {
gl_FragColor = vec4(0.0);
return;
}
// the hit voxel, its radial "up", and the face that was struck
vec3 cc = (vp + 0.5) * ckVs;
float r = length(cc);
vec3 dir = cc / max(r, 1.0e-4);
vec3 n = -mask * sgn;
vec3 hp = p0 + rd * tCur;
// per-voxel hashes: core phase and material variety
vec2 vseed = vec2(dot(vp, vec3(1.0, 57.0, 113.0)), dot(vp, vec3(27.0, 7.0, 91.0)));
float h1 = hash(vseed * 0.013);
float h2 = hash(vseed * 0.029 + 5.7);
// block-texture grain: a 4x4 hash grid on the struck face
vec2 uvFace;
if (mask.x > 0.5) uvFace = hp.yz;
else if (mask.y > 0.5) uvFace = hp.xz;
else uvFace = hp.xy;
float grain = hash(floor(fract(uvFace / ckVs) * 4.0) * 0.37 + vseed * 0.11);
float texMul = mix(1.0, 0.72 + 0.55 * grain, uP_texture);
// flat face lambert + radial wrap for roundness + crevice AO
float lam = clamp(dot(n, Lo), 0.0, 1.0);
float wrap = clamp(dot(dir, Lo) * 0.5 + 0.5, 0.0, 1.0);
float ao = 0.55 + 0.45 * clamp(dot(n, dir) * 0.5 + 0.5, 0.0, 1.0);
float shade = (0.32 + 0.5 * wrap * wrap + 0.85 * lam * lightNow) * ao;
vec3 col;
if (mat < 1.5) {
float f = ckField(dir);
float h = 1.0 + uP_rough * max(f - ckSeaN, 0.0) * 1.2;
float depth = h - r;
float topF = step(depth, ckVs * 1.15);
/*
The climate palette. Every material the strata paint with is a
blend over the four climate weights: snow caps the ice world, the
mesa runs banded terracotta hashed per RADIAL LAYER (the same band
wraps the whole planet, the badlands look), desert bleaches the
land to sand, and lush keeps the tunable colours.
*/
vec3 snow = vec3(0.92, 0.95, 1.0);
/*
Mesa strata: two-block-tall bands hashed per radial layer, weighted
the way real badlands run — long terracotta stretches broken by
thin red, white, yellow and dark-brown accent stripes. The same
band circles the whole planet at its height.
*/
float layer = hash(vec2(floor(r / (ckVs * 2.0)) * 0.371, 5.3));
vec3 mesaBand = layer < 0.5 ? vec3(0.74, 0.42, 0.21)
: (layer < 0.68 ? vec3(0.63, 0.26, 0.15)
: (layer < 0.8 ? vec3(0.88, 0.79, 0.67)
: (layer < 0.9 ? vec3(0.84, 0.65, 0.27) : vec3(0.4, 0.25, 0.18))));
// mesa tops: red-sand flats low down, banded rock on the risen buttes
vec3 mesaTop = mix(vec3(0.72, 0.38, 0.2), mesaBand, step(1.0 + uP_rough * 0.1, h));
vec3 climGrass = uC_grass * ckClim.x + uC_sand * ckClim.y
+ snow * ckClim.z + mesaTop * ckClim.w
+ mix(uC_grass, vec3(0.62, 0.85, 0.3), 0.6) * ckCherry; // vivid meadow
vec3 climDirt = uC_dirt * (ckClim.x + ckClim.y + ckCherry)
+ uC_dirt * vec3(0.75, 0.85, 1.05) * ckClim.z + mesaBand * ckClim.w;
vec3 climSand = uC_sand * (ckClim.x + ckClim.y + ckCherry)
+ mix(uC_sand, snow, 0.9) * ckClim.z + vec3(0.72, 0.35, 0.2) * ckClim.w;
vec3 climWater = uC_water * (ckClim.x + ckClim.y + ckCherry)
+ vec3(0.62, 0.82, 0.92) * ckClim.z
+ mix(uC_water, vec3(0.42, 0.3, 0.22), 0.4) * ckClim.w;
if (topF > 0.5 && f < ckSeaN) {
// OCEAN: the surface of the perfect sphere painted as water —
// lighter over coastal shallows, deep blue mid-ocean, with a sun
// glint and a shimmer on the flow clock. The ice world stills the
// shimmer and pales the depths: a frozen sheet.
float deep = clamp((ckSeaN - f) / 0.12, 0.0, 1.0) * (1.0 - 0.55 * ckClim.z);
vec3 wc = climWater * mix(1.3, 0.55, deep);
float shim = 0.85 + 0.25 * sin(uAnim * 2.5 + grain * 6.2831 + dir.x * 4.0);
shim = mix(shim, 1.02, ckClim.z);
col = wc * (0.45 + 0.55 * wrap) * shim + wc * lam * 0.35;
} else {
/*
LAND. Strata by radial depth below the local surface — grass or
desert sand on the outward faces of surface blocks, mud with
hashed stone patches beneath, then ore-seamed stone. Elevation
overrides the biome: rising ground bares brown hillsides, peaks
stand as naked stone, and every shore gets a sand band.
*/
float dirtF = step(depth, ckVs * 2.4);
float up = clamp(dot(n, dir), 0.0, 1.0);
vec3 albedo = mix(uC_stone, climDirt, dirtF);
// stone patches in the exposed mud, below the grass line
albedo = mix(albedo, uC_stone, dirtF * (1.0 - topF) * step(h2, 0.3));
float bio = ckBiome(dir);
float desertF = step(bio, 0.3);
albedo = mix(albedo, climGrass, topF * step(0.45, up) * (1.0 - desertF));
albedo = mix(albedo, climSand, desertF * dirtF); // desert sand runs deep
// elevation bands: brown hillsides, then bare stone peaks — both
// buried under snow when the ice climate holds (frozen peaks stay
// white with only crevice shadow, not brown or gray)
albedo = mix(albedo, climDirt,
topF * step(1.0 + uP_rough * 0.28, h) * 0.85 * (1.0 - 0.9 * ckClim.z));
albedo = mix(albedo, uC_stone,
topF * step(1.0 + uP_rough * 0.45, h) * (1.0 - 0.85 * ckClim.z));
// beach: a narrow field-space band above the shoreline turns to sand
albedo = mix(albedo, climSand, topF * step(abs(f - ckSeaN - 0.017), 0.018));
// the coarse cluster cells serve ore veins AND glacier patches
vec3 oc = floor(cc / (2.5 * ckVs));
vec2 oseed = vec2(dot(oc, vec3(1.0, 57.0, 113.0)), dot(oc, vec3(27.0, 7.0, 91.0)));
float fleck = step(0.5, hash(floor(fract(uvFace / ckVs) * 4.0) * 0.53 + oseed * 0.19));
// ICE climate: packed-ice blue patches cluster over the risen
// ground — glacier faces streaking the snowy mountainsides
float icePatch = ckClim.z * step(hash(oseed * 0.023 + 9.1), 0.5)
* step(1.0 + uP_rough * 0.06, h);
albedo = mix(albedo, vec3(0.55, 0.7, 0.92), icePatch * (0.45 + 0.4 * fleck));
/*
Ore veins: a coarse cell grid hashes veins into the deep stone, so
ore comes in multi-block clusters like the cross-section dioramas.
Each vein rolls a type — diamond (the tunable ore colour), lapis
(a deep-blue remap of it), or coal (unlit) — and each ore block is
stone FLECKED with the hue on its texture grain, the way the
actual ore tile is drawn. Only the flecks glow.
*/
float veinF = (1.0 - dirtF) * step(1.0 - uP_ore, hash(oseed * 0.017)) * step(h1, 0.8);
float oreType = hash(oseed * 0.041 + 2.9);
vec3 oreHue = oreType < 0.4
? uC_ore
: (oreType < 0.75 ? uC_ore * vec3(0.25, 0.45, 1.2) : vec3(0.16));
float oreLit = oreType < 0.75 ? 1.0 : 0.0;
albedo = mix(albedo, oreHue, veinF * (0.2 + 0.65 * fleck));
float twinkle = 0.55 + 0.45 * sin(uP_shuffle + hash(oseed * 0.013) * 37.0); // integrated clock
// depth below the surface darkens: cave interiors and cleft walls
// sink into shadow, which makes the glow read as underground
float depthDim = mix(1.0, 0.62, clamp(depth / max(uP_rough * 0.9, 0.05), 0.0, 1.0));
// the deeper the rock, the closer to the molten core
float coreR = 1.0 - uP_rough * 0.6;
float coreF = uP_core * smoothstep(coreR + 0.15, coreR - 0.05, r);
vec3 emis = oreHue * veinF * fleck * oreLit * glowNow * twinkle
+ uC_lava * coreF * (0.9 + 0.4 * sin(uP_shuffle * 1.6 + h1 * 51.0))
* (0.6 + 1.4 * uOutput);
col = albedo * shade * depthDim + emis;
}
} else if (mat < 2.5) {
// trunk: dark wood, derived from the mud so the palette stays small
col = uC_dirt * 0.5 * shade;
} else {
// leaves: heavier grain reads as foliage clumps. Under the ice climate
// this material IS the spikes, so it turns packed-ice blue and the
// grain smooths toward faceted ice.
vec3 climLeaf = uC_leaf * (ckClim.x + ckClim.y * 0.9)
+ vec3(0.62, 0.76, 0.95) * ckClim.z
+ mix(uC_leaf, vec3(0.45, 0.62, 0.25), 0.5) * ckClim.w // cactus green
+ vec3(0.93, 0.7, 0.82) * ckCherry; // blossom pink
col = climLeaf * shade;
float leafGrain = mix(0.5 + 0.9 * grain, 0.85 + 0.3 * grain, ckClim.z);
texMul = mix(1.0, leafGrain, uP_texture);
}
col *= texMul * gainNow;
col = pow(max(col, 0.0), vec3(uP_contrast));
// Surface-lit orb bounded by the hit test: alpha IS coverage, and a hit
// is fully opaque — premultiplied output, trivially (see shdr-28).
gl_FragColor = vec4(col, 1.0);
}
`;
export const shdr24Orb: OrbVariant = {
key: "shdr-24",
label: "SHDR-24",
note: "a Minecraft Earth — a perfect voxel sphere whose seasons cycle it through lush, cherry-grove, ice, mesa and desert worlds",
frag: CHUNK_FRAG,
params: [
{ key: "spin", label: "Spin", min: 0, max: 5, step: 0.03, default: 0.22, integrate: true },
{ key: "tilt", label: "Tilt", min: 0, max: 4, step: 0.02, default: 0.45 },
{ key: "drift", label: "Terrain drift", min: 0, max: 10, step: 0.05, default: 0.12, integrate: true },
{ key: "season", label: "Season rate", min: 0, max: 10, step: 0.05, default: 0.3, integrate: true },
{ key: "shuffle", label: "Ember rate", min: 0, max: 20, step: 0.1, default: 0.8, integrate: true },
{ key: "radius", label: "Radius", min: 0.15, max: 3, step: 0.015, default: 1.15 },
{ key: "blocks", label: "Blocks", min: 16, max: 96, step: 1, default: 64 },
{ key: "rough", label: "Mountains", min: 0, max: 0.8, step: 0.01, default: 0.45 },
{ key: "scale", label: "Terrain scale", min: 0.5, max: 8, step: 0.05, default: 2.4 },
{ key: "sea", label: "Sea level", min: 0, max: 1, step: 0.01, default: 0.5 },
{ key: "trees", label: "Trees", min: 0, max: 1, step: 0.01, default: 0.75 },
{ key: "cave", label: "Caves", min: 0, max: 1, step: 0.01, default: 0.4 },
{ key: "ore", label: "Ore density", min: 0, max: 0.6, step: 0.01, default: 0.12 },
{ key: "glow", label: "Ore glow", min: 0, max: 5, step: 0.03, default: 0.9 },
{ key: "core", label: "Molten core", min: 0, max: 1, step: 0.01, default: 0.5 },
{ key: "texture", label: "Texture grain", min: 0, max: 1, step: 0.01, default: 0.6 },
{ key: "light", label: "Key light", min: 0, max: 3, step: 0.015, default: 1 },
{ key: "gain", label: "Gain", min: 0.05, max: 5, step: 0.05, default: 1 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 10, step: 0.05, default: 1 }
],
colors: [
{ key: "grass", label: "Grass", default: "#6abe30" },
{ key: "dirt", label: "Mud", default: "#6f4a2f" },
{ key: "stone", label: "Stone", default: "#8a8a90" },
{ key: "sand", label: "Sand", default: "#dbcf9c" },
{ key: "water", label: "Water", default: "#2f66d0" },
{ key: "leaf", label: "Leaves", default: "#3e8f27" },
{ key: "ore", label: "Ore", default: "#4de3ff" },
{ key: "lava", label: "Lava", default: "#ff7b26" }
],
/*
Each state animates DIFFERENTLY on the integrated clocks — same palette
and biomes throughout (no stateColors on purpose):
idle DRIFTS lazy spin, terrain barely morphing, embers twinkling
thinking LOADS the spin all but stops while the terrain field
streams — continents morph and blocks pop in and out
like chunks loading — and the ore twinkle races
speaking ERUPTS the planet turns fast to answer, the molten core
blazes through the caves, ore glow flares
*/
statePresets: {
idle: {
spin: 0.22,
drift: 0.12,
season: 0.3,
shuffle: 0.8,
glow: 0.9,
core: 0.5,
gain: 1,
light: 1
},
// thinking races the seasons as well as the terrain: the planet cycles
// through its worlds while it considers
thinking: {
spin: 0.04,
drift: 1.7,
season: 1.8,
shuffle: 4.5,
glow: 1.3,
core: 0.35,
gain: 0.95,
light: 0.9
},
speaking: {
spin: 0.85,
drift: 0.35,
season: 0.6,
shuffle: 1.6,
glow: 1.6,
core: 1,
gain: 1.1,
light: 1.15
}
}
};
export type Shdr24Props = Omit<ShaderOrbProps, "variant">;
export function Shdr24({ size = 280, ...rest }: Shdr24Props) {
return <ShaderOrb variant={shdr24Orb} size={size} {...rest} />;
}
export default Shdr24;
27. components/ui/shdr-25.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-25 — the folds of a warped field, drawn by their own steepness.
Ported from a one-line twigl listing:
vec2 p=FC.xy/r.y*2e1+t;
for(float i;i++<8.;)
p+=sin(p+t/.2+i)*.4,
p*=mat2(6,-8,8,6)/9.;
o=vec4(tanh(length(fwidth(sin(p*.3)/.1))),texture(b,FC.xy/r));
What it actually is, decoded:
- THE IMAGE IS A DERIVATIVE. Nothing here draws a shape; it measures how
fast a rippled field changes from one pixel to the next and paints
that. Where eight octaves of warp have folded the plane onto itself
the field races, and those folds come out as bright filigree. Where it
is stretched flat, nothing.
- mat2(6,-8,8,6)/9 IS AN EXACT SIMILARITY, and it is the neatest thing
in the listing. 6-8-10 is a Pythagorean triple, so that matrix is
precisely a rotation through the 3-4-5 angle — about 53.13 degrees —
times a scale of 10/9. Rotating between octaves is the standard way to
stop a warp from lining up with the axes; doing it with integers and
one divide is not.
- THE CLOCK RUNS TWICE. t translates the whole field once at the start,
and t/.2 — five times faster — drives the warp inside every octave, so
the pattern drifts slowly while its detail boils.
- vec4(scalar, texture(...)) puts the derivative in RED and takes green,
blue and alpha from the previous frame.
Port decisions, each one a documented trap or rule in the README:
- fwidth() CANNOT BE USED. It needs OES_standard_derivatives in WebGL 1,
and the #extension directive that enables it cannot legally follow the
prelude's declarations — the README says so, and shdr-28 works
around it by deriving its pixel footprint analytically. There is no
analytic footprint through eight octaves of feedback warp, so the
derivative is taken by FINITE DIFFERENCES instead: evaluate the whole
chain at the two neighbouring pixel centres and difference. Three
evaluations rather than one, and closer to the truth than fwidth,
which is constant across each 2x2 quad.
- THE FEEDBACK TERM IS NOT PORTED. One pass, one canvas, no
previous-frame texture (the same limit as shdr-09). Green and blue
arrive only from that line, so without a substitute this orb would be
red and nothing else — the derivative is instead run through a palette
keyed to the field itself.
- THE ORB IS THE OBJECT: a flat 2D field, so it is sampled through a
stereographic projection of the dome, and motion is projection-safe
2D as in shdr-08. That wrap pays for itself twice here — because the
measurement is a SCREEN-SPACE derivative, the projection's own
compression toward the limb steepens the field there, so the filigree
tightens at the rim exactly the way a texture on a real sphere would.
- The golfed listing relies on i starting at zero; uninitialised locals
are UNDEFINED in GLSL ES 1.0, explicit here.
- Surface-lit and mask-bounded, so alpha IS coverage — premultiplied
output, as in shdr-17.
---------------------------------------------------------------------------- */
/*
* Step counts are `#define`s: ES 1.0 requires constant loop bounds. OCTAVES
* is the listing's i++ < 8. The finite difference costs three passes of it
* per sample, so this is 24 warp steps before supersampling.
*/
const CREASE_FRAG = `
#define OCTAVES 8
#define AA 2
// Volume-reactive values, resolved once per fragment in main().
float creaseWarp;
float creaseGain;
// GLSL ES 1.0 has no scalar tanh; the prelude ships the vec3 form only.
float tanh1(float x) {
x = clamp(x, -10.0, 10.0);
float e = exp(2.0 * x);
return (e - 1.0) / (e + 1.0);
}
/*
The whole chain for one pixel centre: dome, stereographic wrap, then the
eight-octave warp. Called three times per sample so the derivative below
can be differenced — see the header for why fwidth is unavailable.
*/
vec2 creaseField(vec2 fragCoord, float t, float drift, float sw) {
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
vec2 pl = uv / max(uP_radius, 0.001);
float z = sqrt(max(1.0 - dot(pl, pl), 0.0));
vec2 p = pl / (z + 1.0 + uP_bulge) * uP_scale;
p = mat2(cos(sw), -sin(sw), sin(sw), cos(sw)) * p;
p += drift;
/*
The listing's matrix, decomposed. mat2(6,-8,8,6)/9 is exactly
(10/9) * mat2(.6,-.8,.8,.6), and that second factor is a true rotation
because 6-8-10 is a Pythagorean triple — so the octave transform is a
rotation through the 3-4-5 angle times a clean zoom, with the zoom
pulled out as a slider.
*/
for (int i = 0; i < OCTAVES; i++) {
float fi = float(i) + 1.0;
p += sin(p + t + fi) * creaseWarp;
p = uP_zoom * (mat2(0.6, -0.8, 0.8, 0.6) * p);
}
return p;
}
vec3 creaseRender(vec2 fragCoord) {
float t = uP_speed; // integrated clock: the boil
float drift = uP_drift; // integrated clock: the slow travel
float sw = uP_swirl; // integrated clock
/*
The three taps the difference needs. uP_blur is how far apart they sit:
at one pixel this is fwidth exactly, and wider is a deliberate blur —
the derivative of a folded field is a hairline, and a rim wants width.
*/
vec2 p0 = creaseField(fragCoord, t, drift, sw);
vec2 px = creaseField(fragCoord + vec2(uP_blur, 0.0), t, drift, sw);
vec2 py = creaseField(fragCoord + vec2(0.0, uP_blur), t, drift, sw);
/*
fwidth, by hand and once per channel. The sum of the absolute
differences on each axis is exactly what the built-in returns — but
taking it three times at slightly offset ripple phases puts each
channel's rim in a slightly different place, which is where the warm
and cool fringes on the edges come from. One field evaluation still
serves all three.
*/
vec3 e;
for (int c = 0; c < 3; c++) {
float ph = float(c) * uP_fringe;
vec2 v0 = sin(p0 * uP_ripple + ph);
vec2 d = abs(sin(px * uP_ripple + ph) - v0)
+ abs(sin(py * uP_ripple + ph) - v0);
float m = tanh1(length(d) * creaseGain / max(uP_exposure, 0.001));
if (c == 0) e.r = m;
else if (c == 1) e.g = m;
else e.b = m;
}
e = pow(clamp(e, 0.0, 1.0), vec3(uP_contrast));
vec3 col = uC_tint * e;
// a dark body under the filigree, so the flat regions read as the ball
col += uC_body * uP_floorLevel;
float lum = dot(col, vec3(0.299, 0.587, 0.114));
col = mix(vec3(lum), col, uP_saturation);
// dome shading keeps the ball a ball under the folds
vec2 pl = ((2.0 * fragCoord - uRes) / min(uRes.x, uRes.y)) / max(uP_radius, 0.001);
float z = sqrt(max(1.0 - dot(pl, pl), 0.0));
vec3 n = vec3(pl, z);
float lambert = clamp(dot(n, normalize(vec3(-0.45, 0.55, 0.72))), 0.0, 1.0);
col *= 0.62 + uP_light * lambert;
float fres = 1.0 - z;
fres = fres * fres * fres;
col += uC_sheen * uP_rim * fres;
return col;
}
void main() {
// Volume coupling: the user's voice folds the field harder, the agent's
// steepens what counts as a crease.
creaseWarp = uP_warp * (1.0 + 0.4 * uInput);
creaseGain = uP_edgeGain * (1.0 + 0.5 * uOutput);
vec2 uv = orbUV();
float mask = smoothstep(0.012, -0.012, length(uv) - max(uP_radius, 0.001));
// Twenty-four warp steps per sample — none of them worth paying for
// outside the silhouette.
if (mask <= 0.0) {
gl_FragColor = vec4(0.0);
return;
}
vec3 col = vec3(0.0);
#if AA > 1
for (int mx = 0; mx < AA; mx++) {
for (int my = 0; my < AA; my++) {
vec2 off = (vec2(float(mx), float(my)) + 0.5) / float(AA) - 0.5;
col += creaseRender(gl_FragCoord.xy + off);
}
}
col /= float(AA * AA);
#else
col = creaseRender(gl_FragCoord.xy);
#endif
// Surface orb bounded by a mask: alpha IS coverage, so premultiply — the
// opposite convention from the emissive orbs (see shdr-31).
float a = mask;
gl_FragColor = vec4(max(col, vec3(0.0)) * a, a);
}
`;
export const shdr25Orb: OrbVariant = {
key: "shdr-25",
label: "SHDR-25",
note: "the folds of a warped field, drawn by their own steepness",
frag: CREASE_FRAG,
params: [
{ key: "speed", label: "Boil", min: 0.015, max: 20, step: 0.05, default: 3, integrate: true },
{ key: "drift", label: "Drift", min: 0, max: 8, step: 0.02, default: 0.6, integrate: true },
{ key: "swirl", label: "Swirl", min: 0, max: 3, step: 0.015, default: 0.05, integrate: true },
{ key: "radius", label: "Radius", min: 0.15, max: 3, step: 0.015, default: 0.9 },
{ key: "scale", label: "Cell scale", min: 0.3, max: 60, step: 0.1, default: 7.5 },
{ key: "bulge", label: "Dome bulge", min: 0, max: 4, step: 0.02, default: 0.3 },
{ key: "warp", label: "Fold", min: 0, max: 2, step: 0.01, default: 0.4 },
{ key: "zoom", label: "Octave zoom", min: 0.6, max: 2, step: 0.005, default: 1.111 },
{ key: "ripple", label: "Ripple", min: 0.02, max: 3, step: 0.01, default: 0.3 },
{ key: "edgeGain", label: "Edge gain", min: 0.5, max: 60, step: 0.5, default: 10 },
{ key: "blur", label: "Rim width", min: 0.5, max: 12, step: 0.25, default: 2.5 },
{ key: "fringe", label: "Chromatic fringe", min: 0, max: 1.5, step: 0.005, default: 0.09 },
{ key: "exposure", label: "Exposure", min: 0.05, max: 40, step: 0.05, default: 0.8 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 8, step: 0.05, default: 1.15 },
{ key: "saturation", label: "Saturation", min: 0, max: 4, step: 0.02, default: 1.5 },
{ key: "floorLevel", label: "Body fill", min: 0, max: 2, step: 0.01, default: 0.16 },
{ key: "light", label: "Key light", min: 0, max: 3, step: 0.015, default: 0.35 },
{ key: "rim", label: "Rim sheen", min: 0, max: 3, step: 0.015, default: 0.45 }
],
colors: [
{ key: "tint", label: "Rim", default: "#dbe8f7" },
{ key: "body", label: "Body", default: "#0d1118" },
{ key: "sheen", label: "Sheen", default: "#a8c8f0" }
],
/*
Staged on the FOLD, which is what makes creases exist at all, and on
edge gain, which decides how steep a slope has to be to count as one.
Cell scale moves only for the answer — it sets how much pattern is on
the ball, and as it glides in the ball reads as inflating; that is the
answer's entrance.
*/
statePresets: {
// at rest: a slow boil, folds moderate, rims clean — the octave zoom
// pulled in a touch under the default and the edge gain a quarter up,
// so slightly gentler slopes count as creases
idle: {
speed: 3,
drift: 0.6,
swirl: 0.05,
warp: 0.4,
zoom: 1.07,
edgeGain: 12.5,
exposure: 0.8,
contrast: 1.15
},
/*
searching: the folds RELAX a touch below idle, but the edge gain
nearly triples so even the shallowest slope lights up as a crease,
on a boil half again idle's. The ripple tightens, the rim widens with
more than double the chromatic fringe, and the saturation, key light
and rim sheen all come up — every cell rims at once in colour, and
none of it settles.
*/
thinking: {
speed: 4.7,
drift: 0.15,
swirl: 0.02,
bulge: 0.38,
warp: 0.34,
zoom: 1.12,
ripple: 0.18,
edgeGain: 33,
blur: 2.75,
fringe: 0.2,
exposure: 0.55,
contrast: 1.5,
saturation: 2,
light: 0.525,
rim: 0.69
},
/*
answering: the field FOLDS hardest of the three and the gain drops to
under a sixth of the thinking state, so the creases are deep but only
the steepest rims light. The cell scale is pushed past idle's and the
dome bulged to near a hemisphere, the swirl opened an order of
magnitude, the ripple widened — a slow, heavy, swirling boil, with
the exposure tripled so what does light, burns.
*/
speaking: {
speed: 1.4,
drift: 0.8,
swirl: 0.6,
scale: 11.5,
bulge: 2.22,
warp: 1.55,
zoom: 0.945,
ripple: 1.18,
edgeGain: 5,
blur: 2,
fringe: 0.23,
exposure: 2.35,
contrast: 1.35
}
},
// cool steel at rest, cold indigo for both working states — the answer
// is told apart by its fold and scale, not its colour
stateColors: {
idle: { tint: "#dbe8f7", body: "#0d1118", sheen: "#a8c8f0" },
thinking: { tint: "#c2d6f5", body: "#090d1c", sheen: "#8fb4f2" },
speaking: { tint: "#c2d6f5", body: "#090d1c", sheen: "#8fb4f2" }
}
};
export type Shdr25Props = Omit<ShaderOrbProps, "variant">;
export function Shdr25({ size = 280, ...rest }: Shdr25Props) {
return <ShaderOrb variant={shdr25Orb} size={size} {...rest} />;
}
export default Shdr25;
28. components/ui/shdr-26.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-26 — a crazed web of thin coloured threads, knotted to a cell grid.
Ported from a three-line listing in one of the golf dialects:
f2 c = C.xy / R.y*4+R, s = flr(c), i;
@(9) c+=cos(++i * c.yx + .1/(s-c) + T) / i
O = exp(-3*abs(sin(c.y+f4(,.4,.2,))))
(f2/f4 are vec2/vec4, flr is floor, @(9) is a nine-iteration loop, and the
empty slots in f4(,.4,.2,) are zeros.)
What it actually is, decoded:
- The warp is the same nine-octave FEEDBACK curl as shdr-08's — each
octave reads the last one's result with its components swapped — but
with two differences that change everything: the clock enters as a
SHARED phase rather than one multiplied per octave, so this field
boils coherently instead of shimmering, and each octave carries a
LATTICE POLE term.
- .1/(s-c) is the pole, and it is the whole idea. s is the sample's cell
corner, taken ONCE before the loop, so s - c starts as -fract(c) and
then drifts as the warp moves c away from its own cell. The phase
blows up as a sample approaches a lattice point, so the field knots
violently around a grid of singularities: cell interiors flow, cell
corners tear.
- The listing adds the RESOLUTION to c, which looks like it seeds the
lattice — but s - c subtracts it straight back out, so it cancels
exactly and only shifts the lattice phase. Dropped here (the same
resolution-as-phase trap written up in shdr-08, where it did NOT
cancel).
- exp(-3*abs(sin(x))) is a completely different band function from
Nacre's saturating cot: a THIN bright ridge every PI with an
exponential falloff and a dim floor at exp(-3), not a wide crest. That
floor is what keeps the shell lit between threads.
- vec4(0,.4,.2,0) offsets green furthest and blue between. Against a
ridge this thin the offset is a large fraction of the line width, so
every thread splits into three coloured filaments running in parallel
rather than merely fringing at its edges.
Port decisions, each one a documented trap or rule in the README:
- THE ORB IS THE OBJECT: a flat 2D field, so it is sampled through a
stereographic projection of the dome and finished with a fresnel
sheen, never masked out of the plane as a disc.
- Like shdr-08, and for a reason worth reading before changing it,
it projects the unrotated dome and moves in 2D. The abs(sp.z) form
that lets shdr-28 and shdr-29 roll their domes was tried first
and had to go: it turns the ball into an exact mirror image of itself
once a quarter turn, which a cellular grid hides and a web of long
threads does not. The full derivation is at the projection.
- The pole is SOFTENED, not guarded: x/(x*x+g) is 1/x everywhere the
listing cared about and finite where it did not. A hard guard leaves a
sign discontinuity on every lattice line, and the raw divide reaches
infinite phase frequency at every lattice point, which no amount of
supersampling can resolve.
- The golfed listing relies on i starting at zero; uninitialised locals
are UNDEFINED in GLSL ES 1.0, explicit here.
- Surface-lit and mask-bounded, so alpha IS coverage — premultiplied
output, as in shdr-17.
---------------------------------------------------------------------------- */
/*
* Step counts are `#define`s: ES 1.0 requires constant loop bounds. OCTAVES is
* the listing's @(9). AA supersamples the threads, which are a pixel or two
* wide at the centre of the ball and thinner than that toward the limb —
* WebGL 1 has no fwidth without an extension, so brute force is the defence.
*/
const LATTICE_FRAG = `
#define OCTAVES 9
#define AA 3
const float TAU = 6.28318530718;
// Volume-reactive values, resolved once per fragment in main().
float latticePole;
float latticeSharp;
float latticeGain;
vec3 latticeRender(vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
float R = max(uP_radius, 0.001);
// the dome: the front hemisphere of a unit ball, in screen space
vec2 pl = uv / R;
float z = sqrt(max(1.0 - dot(pl, pl), 0.0));
vec3 n = vec3(pl, z);
float t = uP_speed; // integrated clock
/*
Stereographic projection of the UNROTATED dome.
This orb was built on the abs(sp.z) form first — the one shdr-28
and shdr-29 use, which survives a 3D roll because the divisor can
only fall TO zero at the terminator, never through it. It renders, but
every quarter turn it makes the visible hemisphere an EXACT mirror
image about the view axis: at a roll of 90 degrees sp becomes
(-n.z, n.y, n.x), so the projected coordinate depends on n.z and on
abs(n.x), and both of those are even in screen x. A cellular grid
hides that. A web of long threads does not — the ball turns into a
Rorschach blot for a quarter of every revolution.
So the dome stays put, as in shdr-08, and all the motion below is
projection-safe 2D.
*/
vec2 p = n.xy / (n.z + 1.0 + uP_bulge) * uP_scale;
// the plane turns while the lattice drifts across it, so the crazing
// migrates over the glaze instead of sitting welded to it
float sw = uP_swirl; // integrated clock
p = mat2(cos(sw), -sin(sw), sin(sw), cos(sw)) * p;
p.x += uP_drift; // integrated clock
/*
A fractional offset off the pattern origin. The listing carries the
resolution here; it cancels out of the pole term exactly (see the
header) but it does keep the lattice off centre, and without something
in its place a cell corner sits pinned at the dead middle of the ball.
*/
p += vec2(0.37, 0.21);
// the cell the sample starts in, fixed before the warp — everything the
// pole term does is relative to THIS corner, not to wherever c wanders
vec2 cell = floor(p);
/*
Each cell knots on its own hashed phase, so the grid breathes instead
of pulsing as one sheet. The rate is a constant, not a slider: this
multiplies the integrated clock, and a slider there would jump the
phase of every cell on a state change.
*/
float breathe = 1.0 + uP_pulse * sin(TAU * hash(cell) + t * 0.35);
vec2 c = p;
for (int j = 0; j < OCTAVES; j++) {
float i = float(j) + 1.0;
/*
The lattice pole, softened. d/(d*d+g) tracks 1/d away from the cell
corner and rolls over to a finite peak at it, so the phase stays
band-limited and the knot has a SIZE — uP_poleSoft is that size, and
it is the difference between crisp cell knots and a corner full of
aliased noise.
*/
vec2 d = cell - c;
vec2 pole = latticePole * breathe * d / (d * d + vec2(uP_poleSoft));
c += uP_warp * cos(i * c.yx + pole + t) / i;
}
/*
The listing's tone map: a thin ridge every PI with an exponential
falloff. The per-channel offsets are kept as their original ratio
(0, 2, 1) so one slider widens the whole split, and against a ridge
this thin they separate the thread into three coloured filaments.
*/
vec3 x = vec3(c.y) + vec3(0.0, 2.0, 1.0) * uP_split;
vec3 thread = exp(-latticeSharp * abs(sin(x)));
float lev = dot(thread, vec3(1.0 / 3.0));
/*
The exp() floor never reaches zero, so the shell is lit between the
threads by construction — the body colour is added under it rather
than filling a hole. The thread term stays PER-CHANNEL through the
palette multiply; collapsing it to lev first would throw away the
filament split, which is the only thing the vec4 phase was for.
*/
vec3 col = uC_deep * uP_floor;
/*
The ramp between the two thread colours runs nearly the whole range of
lev deliberately. Started at 0.35 it reached uC_hot — which is near-white in
every state — across most of the visible web, and the state palettes,
which ride on uC_line, never got to the eye at all.
The other half of that fix is in the presets: the chromatic split makes
the three channels independent, so at a wide split THREAD sets the hue
and no palette can. It is staged with the rest — widest while
searching, nearly closed while answering, which is when the warm
palette has to carry.
*/
col += thread * mix(uC_line, uC_hot, smoothstep(0.12, 1.0, lev)) * latticeGain;
/*
A tight second read of the same ridge, added on top: raising a value
that is already exp(-k*|sin|) to a high power is the same ridge at a
fraction of the width, which lands as a hot core inside each thread.
Taken from lev — the MEAN of the three channels — deliberately, so the
core is achromatic and lands only where all three filaments coincide.
Read per-channel it would just be a fourth colour-separated ridge, and
the web would stay a scatter of green and magenta flecks instead of
resolving into white threads with coloured shoulders. Squared twice
rather than pow() — pow is undefined for a negative base and this is
cheaper anyway (see the README).
*/
float core = lev * lev;
core = core * core;
col += uC_hot * core * uP_core;
col = pow(max(col, vec3(0.0)), vec3(uP_contrast));
// dome shading keeps the ball a ball under the web
float lambert = clamp(dot(n, normalize(vec3(-0.45, 0.55, 0.72))), 0.0, 1.0);
col *= 0.35 + uP_light * lambert;
// fresnel sheen: the glaze the threads are crazed into, and the thing
// that keeps the limb reading as a surface where the cells have
// compressed past resolving
float fres = 1.0 - z;
fres = fres * fres * fres;
col += uC_sheen * uP_rim * fres;
return col;
}
void main() {
// Volume coupling: the user's voice tightens the knots, the agent's
// thickens the threads and brightens them.
latticePole = uP_pole * (1.0 + 0.8 * uInput);
latticeSharp = uP_sharp * (1.0 - 0.25 * uOutput);
latticeGain = uP_gain * (0.85 + 0.4 * uOutput);
vec2 uv = orbUV();
float mask = smoothstep(0.012, -0.012, length(uv) - max(uP_radius, 0.001));
// Nothing outside the silhouette is ever visible, so skip AA * AA warps
// for it rather than shading transparent sky.
if (mask <= 0.0) {
gl_FragColor = vec4(0.0);
return;
}
vec3 col = vec3(0.0);
#if AA > 1
for (int mx = 0; mx < AA; mx++) {
for (int my = 0; my < AA; my++) {
vec2 off = (vec2(float(mx), float(my)) + 0.5) / float(AA) - 0.5;
col += latticeRender(gl_FragCoord.xy + off);
}
}
col /= float(AA * AA);
#else
col = latticeRender(gl_FragCoord.xy);
#endif
// Surface orb bounded by a mask: alpha IS coverage, so premultiply — the
// opposite convention from the emissive orbs (see shdr-31).
float a = mask;
gl_FragColor = vec4(max(col, vec3(0.0)) * a, a);
}
`;
/*
The look at rest, which thinking builds on: slow boil, threads fine —
on a dome bulged nearly all the way and the warp pushed to more than
double, so the field wraps hard around the ball. The knots are halved in
strength and pinched to the smallest size, the hot core is run up five
times and the body fill cut to a third: a dark ball with a fierce centre.
*/
const KNOT_REST = {
speed: 0.42,
swirl: 0.075,
drift: 0.18,
bulge: 3.28,
warp: 2,
pole: 0.13,
poleSoft: 0.001,
pulse: 0.35,
split: 0.045,
sharp: 4.5,
core: 2.235,
floor: 0.26,
gain: 1,
contrast: 1.35,
light: 0.705
};
const KNOT_PALETTE = {
deep: "#111a2e",
line: "#3fd2ff",
hot: "#fff4d6",
sheen: "#a9d8ff"
};
export const shdr26Orb: OrbVariant = {
key: "shdr-26",
label: "SHDR-26",
note: "a crazed web of coloured threads knotted to a cell grid",
frag: LATTICE_FRAG,
params: [
{ key: "speed", label: "Boil", min: 0.015, max: 10, step: 0.05, default: 0.4, integrate: true },
{ key: "swirl", label: "Swirl", min: 0, max: 3, step: 0.015, default: 0.07, integrate: true },
{ key: "drift", label: "Crazing drift", min: 0, max: 5, step: 0.03, default: 0.18, integrate: true },
{ key: "radius", label: "Radius", min: 0.15, max: 3, step: 0.015, default: 0.9 },
{ key: "scale", label: "Cell scale", min: 0.3, max: 20, step: 0.1, default: 9 },
{ key: "bulge", label: "Dome bulge", min: 0, max: 4, step: 0.02, default: 0.25 },
{ key: "warp", label: "Warp", min: 0, max: 3, step: 0.02, default: 0.85 },
{ key: "pole", label: "Knot strength", min: 0, max: 2, step: 0.005, default: 0.25 },
{ key: "poleSoft", label: "Knot size", min: 0.001, max: 1, step: 0.001, default: 0.012 },
{ key: "pulse", label: "Cell breathing", min: 0, max: 2, step: 0.01, default: 0.35 },
{ key: "sharp", label: "Thread width", min: 0.3, max: 20, step: 0.05, default: 4.5 },
{ key: "split", label: "Chromatic split", min: 0, max: 1, step: 0.005, default: 0.045 },
{ key: "core", label: "Hot core", min: 0, max: 3, step: 0.015, default: 0.45 },
{ key: "floor", label: "Body fill", min: 0, max: 3, step: 0.01, default: 0.9 },
{ key: "gain", label: "Brightness", min: 0.05, max: 5, step: 0.05, default: 1 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 10, step: 0.05, default: 1.35 },
{ key: "light", label: "Key light", min: 0, max: 3, step: 0.015, default: 0.7 },
{ key: "rim", label: "Rim sheen", min: 0, max: 3, step: 0.015, default: 0.45 }
],
/*
* Four stops: the glaze the web is crazed into, the two ends of the thread
* ramp, and the fresnel sheen. The chromatic split runs the threads apart
* into three filaments on its own, so the palette only has to set the mood.
*/
colors: [
{ key: "deep", label: "Glaze", default: "#111a2e" },
{ key: "line", label: "Thread", default: "#3fd2ff" },
{ key: "hot", label: "Hot thread", default: "#fff4d6" },
{ key: "sheen", label: "Sheen", default: "#a9d8ff" }
],
/*
Staged on the pole, which is this orb's loudest control: knot strength
decides whether the field flows past the lattice or tears itself around
it. Thread width is the second lever, and the two integrated clocks —
boil and roll — carry the tempo.
*/
statePresets: {
/*
at rest: slow boil, threads fine — on a dome bulged nearly all the
way and the warp pushed to more than double, so the field wraps hard
around the ball. The knots are halved in strength and pinched to the
smallest size, the hot core is run up five times and the body fill
cut to a third: a dark ball with a fierce centre.
*/
idle: KNOT_REST,
/*
searching: the rest look, set MOVING. The boil runs at nearly two and
a half times idle, the swirl four times and the drift three, so the
web migrates over the glaze instead of sitting on it. The knots come
up a third but breathe less, the threads soften a touch on a tighter
split, and the contrast is pushed — busier, but no brighter.
*/
thinking: {
...KNOT_REST,
speed: 1,
swirl: 0.3,
drift: 0.51,
pole: 0.18,
pulse: 0.22,
sharp: 3.6,
split: 0.03,
floor: 0.28,
contrast: 1.6
},
/*
answering: the web goes FAST and FLOODS. The boil runs at six times
thinking and the drift more than three, the knots breathe at their
deepest, and the threads spread to their softest, so the ridges bloom
into broad light. The dome is flattened back toward the default and
the warp relaxed to a third of rest, with the cell scale nudged up;
the core is halved from rest, but the fill, gain and contrast all
come up — the brightest, busiest state.
*/
speaking: {
speed: 6.45,
swirl: 0.555,
drift: 1.74,
scale: 11,
bulge: 0.78,
warp: 0.76,
pole: 0.195,
poleSoft: 0.001,
pulse: 1.39,
sharp: 2.1,
split: 0.045,
core: 1.08,
floor: 0.48,
gain: 1.3,
contrast: 1.95
}
},
// one palette, cold cyan porcelain, across all three states — unlike the
// sibling orbs, this one tells its states apart by the knots and the
// tempo alone, not by colour
stateColors: {
idle: KNOT_PALETTE,
thinking: KNOT_PALETTE,
speaking: KNOT_PALETTE
}
};
export type Shdr26Props = Omit<ShaderOrbProps, "variant">;
export function Shdr26({ size = 280, ...rest }: Shdr26Props) {
return <ShaderOrb variant={shdr26Orb} size={size} {...rest} />;
}
export default Shdr26;
29. components/ui/shdr-27.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-27 — a weather-radar mosaic: fronts of colour-classed pixels sweeping
across the ball.
The reference is a precipitation map drawn as a coarse grid of square
dots on cream paper: each dot is one of a handful of flat colours — grey
where there is barely anything, then blue, cyan, green, red, yellow, a
rare magenta at the peaks — and the dots thin out to nothing in the
quiet areas, so the fronts have speckled edges and the calm between
them is mostly paper. The fronts are long and diagonal, streaked along
their direction of travel.
- THE FIELD. The sphere is twisted by a handful of vortices — hashed
points on it, each rotating the space about the axis through it by a
Gaussian-falloff angle, alternating in sense — and two 3D fbm scales
are then read through that twist and MULTIPLIED: a broad
one decides where the storm systems are, a finer one gives each a
core and ragged edges, so the calm between systems is empty and the
peaks sit inside the cells, which is what draws the concentric class
rings. The noise drifts through the twist on the integrated clock, so
every system winds into a spiral around the nearest centre and keeps
swirling as new noise slides in. A threshold window and a response
curve cut the intensity out of it.
- THE MOSAIC. The wrapped plane is cut into a grid and the field is
read ONCE per cell, at the cell's centre, so every dot is one flat
colour — no gradient ever crosses a dot, which is the whole look. The
intensity is quantized into seven classes, with a per-cell hash added
first so the class boundaries dither into speckle rather than draw a
contour.
- DROPOUT. Each cell hashes against a density that rises with the
intensity: the quiet areas keep only a sparse scatter of grey dots and
the fronts fill in solid. The hash re-rolls on the ambient clock at a
slow rate, so the scatter twinkles without the fronts moving.
- THE DOT. A square, inset in its cell so the paper shows as a lattice
between dots.
The grid lives on a stereographic wrap of the dome as it faces the
viewer, so the mosaic compresses gently toward the limb — the pixels are
ON the sphere — while the field is evaluated in 3D on the sphere's own
points (3D value noise, vortices as rotations about points on the
sphere), so the roll only rotates the picture under the pixels and no
projection ever distorts it. Surface-lit and
mask-bounded, so alpha IS coverage — premultiplied output, as in
shdr-14.
---------------------------------------------------------------------------- */
const RADAR_FRAG = `
#define VORTICES 6
#define FBM3_OCT 4
// Volume-reactive values, resolved once per fragment in main().
float radarLoNow;
float radarDensityNow;
/*
3D value noise. The prelude's noise is 2D, and a 2D field wrapped onto
the ball has to be projected — and every projection either distorts
somewhere or seams somewhere, which is exactly what the roll dragged
into view. Evaluating the field ON the sphere's own points needs
nothing projected: the roll is just a rotation of the sample point.
*/
float hash3(vec3 p) {
return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453123);
}
float noise3(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float n000 = hash3(i);
float n100 = hash3(i + vec3(1.0, 0.0, 0.0));
float n010 = hash3(i + vec3(0.0, 1.0, 0.0));
float n110 = hash3(i + vec3(1.0, 1.0, 0.0));
float n001 = hash3(i + vec3(0.0, 0.0, 1.0));
float n101 = hash3(i + vec3(1.0, 0.0, 1.0));
float n011 = hash3(i + vec3(0.0, 1.0, 1.0));
float n111 = hash3(i + vec3(1.0, 1.0, 1.0));
return mix(
mix(mix(n000, n100, f.x), mix(n010, n110, f.x), f.y),
mix(mix(n001, n101, f.x), mix(n011, n111, f.x), f.y),
f.z
);
}
float fbm3(vec3 p) {
float v = 0.0;
float a = 0.5;
for (int i = 0; i < FBM3_OCT; i++) {
v += a * noise3(p);
p = p * 2.03 + vec3(11.7, 7.3, 3.1);
a *= 0.5;
}
return v;
}
// rotate v about the unit axis k by angle a (Rodrigues)
vec3 rotateAbout(vec3 v, vec3 k, float a) {
float c = cos(a);
float s = sin(a);
return v * c + cross(k, v) * s + k * dot(k, v) * (1.0 - c);
}
// the precipitation intensity, 0..1, at a point of the unit sphere
float intensity(vec3 sp, float t) {
vec3 q = sp;
/*
The vortices: hashed points on the sphere, each twisting the space
around itself — a rotation about the axis through it, by an angle
that falls off with the angular distance, alternating in sense. The
centres wander slowly so no spiral sits still.
*/
for (int k = 0; k < VORTICES; k++) {
float fk = float(k);
vec3 c = normalize(vec3(
hash(vec2(fk * 3.7, 1.1)) - 0.5,
hash(vec2(fk * 5.9, 2.3)) - 0.5,
hash(vec2(fk * 7.1, 4.9)) - 0.5
));
c = rotateAbout(c, vec3(0.0, 1.0, 0.0), sin(t * 0.09 + fk * 1.7) * 0.25);
float ang = acos(clamp(dot(q, c), -1.0, 1.0));
float fall = exp(-ang * ang / (uP_vortex * uP_vortex));
float a = uP_swirl * fall * (mod(fk, 2.0) < 0.5 ? 1.0 : -1.0);
q = rotateAbout(q, c, a);
}
// bend, then drift the noise through the twisted space
vec3 w = vec3(noise3(q * 1.3 + 2.1), noise3(q * 1.3 + 7.3), noise3(q * 1.3 + 4.4)) - 0.5;
q += w * uP_warp;
vec3 pq = q * uP_freq + vec3(t * 0.22, -t * 0.13, t * 0.07);
/*
Two scales multiplied, not added: a broad mask decides WHERE the storms
are, a finer field gives each one a core and ragged edges. Adding them
fills the whole sphere with mid-tones; multiplying leaves the calm
between systems genuinely empty and puts the peaks inside the cells,
which is what draws the concentric class rings.
*/
float big = clamp((fbm3(pq) - 0.5) * 3.0 + 0.5, 0.0, 1.0);
float fine = clamp((fbm3(pq * 2.6 + 4.7) - 0.5) * 2.4 + 0.5, 0.0, 1.0);
float f = big * (0.55 + 0.45 * fine);
f = clamp((f - radarLoNow) / max(uP_hi - radarLoNow, 0.01), 0.0, 1.0);
// a response curve: the top classes are the rare peaks of a real map
return pow(f, uP_curve);
}
void main() {
// input lowers the window (more of the field reads as weather), output
// fills the dots in
radarLoNow = uP_lo - 0.08 * uInput;
radarDensityNow = uP_density * (1.0 + 0.5 * uOutput);
vec2 uv = orbUV();
float rd = length(uv);
float R = uP_radius;
float mask = smoothstep(0.012, -0.012, rd - R);
if (mask <= 0.0) {
gl_FragColor = vec4(0.0);
return;
}
vec2 pl = uv / R;
float r2 = dot(pl, pl);
float z = sqrt(max(1.0 - r2, 0.0));
vec3 n = vec3(pl, z);
float t = uP_speed; // integrated clock: the weather drifts
/*
The pixel grid lives on the UNROLLED dome: a stereographic wrap of the
front hemisphere as it faces the viewer, which compresses gently toward
the limb and never changes. The picture rolls underneath it — the
pixels are the screen, the weather is what is on it. Laying the grid on
the rolled dome instead put the projection's blow-up (the far side of
the ball) wherever the roll had turned it, and the cells smeared into
streaks there.
*/
vec2 st = n.xy / (1.3 + n.z) * uP_scale;
vec2 g = st * uP_cells;
vec2 cell = floor(g);
vec2 fr = fract(g) - 0.5;
// the cell centre, back on the dome: invert the wrap, then roll it and
// read the field there — once per cell, so every dot is one flat colour
vec2 v = (cell + 0.5) / uP_cells / uP_scale;
float vv = dot(v, v);
float A = vv + 1.0;
float B = 2.6 * vv;
float C = 1.69 * vv - 1.0;
float zc = (-B + sqrt(max(B * B - 4.0 * A * C, 0.0))) / (2.0 * A);
vec3 nc = vec3(v * (1.3 + zc), zc);
float cr = cos(uP_spin);
float sr = sin(uP_spin);
vec3 spc = vec3(nc.x * cr - nc.z * sr, nc.y, nc.x * sr + nc.z * cr);
float f = intensity(spc, t);
// dither the class boundaries with a per-cell hash, then quantize into
// the legend's seven classes. The bands are NOT even: red is broad and
// green thin, as on the reference, and magenta is the rare peak.
float h = hash(cell + 11.7);
float fd = clamp(f + (h - 0.5) * uP_dither, 0.0, 1.0);
float cls = 0.0;
cls += step(0.10, fd);
cls += step(0.26, fd);
cls += step(0.38, fd);
cls += step(0.46, fd);
cls += step(0.78, fd);
cls += step(0.94, fd);
// dropout: density rises with the intensity; the hash re-rolls slowly
float frame = floor(uTime * uP_twinkle);
float roll = hash(cell + vec2(frame * 3.7, -frame * 1.3));
float density = mix(uP_sparse, 1.0, smoothstep(0.0, 0.6, f)) * radarDensityNow;
float keep = step(roll, density);
// the dot: a square inset in its cell
float dsq = max(abs(fr.x), abs(fr.y));
float dotMask = 1.0 - smoothstep(uP_dot - 0.06, uP_dot + 0.06, dsq);
// the class palette
vec3 ink = uC_c0;
ink = cls > 0.5 && cls < 1.5 ? uC_c1 : ink;
ink = cls > 1.5 && cls < 2.5 ? uC_c2 : ink;
ink = cls > 2.5 && cls < 3.5 ? uC_c3 : ink;
ink = cls > 3.5 && cls < 4.5 ? uC_c4 : ink;
ink = cls > 4.5 && cls < 5.5 ? uC_c5 : ink;
ink = cls > 5.5 ? uC_c6 : ink;
vec3 col = mix(uC_paper, ink, dotMask * keep);
// paper grain, so the flats are not dead
col *= 1.0 + (hash(floor(gl_FragCoord.xy / 2.0) + frame) - 0.5) * uP_grain;
// dome shading keeps the ball a ball under the mosaic
float lambert = clamp(dot(n, normalize(vec3(-0.45, 0.55, 0.7))), 0.0, 1.0);
col *= 1.0 - uP_light * (1.0 - lambert);
float fres = pow(1.0 - z, 3.0);
col = mix(col, uC_c0, fres * uP_rim);
// Surface orb bounded by a mask: alpha IS coverage, so premultiply — the
// opposite convention from the emissive orbs (see shdr-31).
float a = mask;
gl_FragColor = vec4(max(col, vec3(0.0)) * a, a);
}
`;
export const shdr27Orb: OrbVariant = {
key: "shdr-27",
label: "SHDR-27",
note: "a weather-radar mosaic, fronts of coloured pixels sweeping the ball",
frag: RADAR_FRAG,
params: [
{ key: "speed", label: "Front speed", min: 0.015, max: 10, step: 0.05, default: 0.6, integrate: true },
{ key: "spin", label: "Roll", min: 0, max: 5, step: 0.03, default: 0.04, integrate: true },
{ key: "radius", label: "Radius", min: 0.15, max: 3, step: 0.015, default: 0.9 },
{ key: "scale", label: "Grid zoom", min: 0.3, max: 8, step: 0.05, default: 2.4 },
{ key: "cells", label: "Grid", min: 8, max: 120, step: 1, default: 34 },
{ key: "dot", label: "Dot size", min: 0.1, max: 0.5, step: 0.01, default: 0.36 },
{ key: "swirl", label: "Swirl", min: 0, max: 8, step: 0.05, default: 1.5 },
{ key: "vortex", label: "Vortex size", min: 0.1, max: 2, step: 0.01, default: 0.45 },
{ key: "freq", label: "Storm scale", min: 0.2, max: 8, step: 0.05, default: 1.6 },
{ key: "warp", label: "Bend", min: 0, max: 3, step: 0.02, default: 0.5 },
{ key: "lo", label: "Quiet threshold", min: 0, max: 1, step: 0.005, default: 0.12 },
{ key: "hi", label: "Peak threshold", min: 0, max: 1, step: 0.005, default: 0.82 },
{ key: "curve", label: "Response curve", min: 0.5, max: 4, step: 0.05, default: 1.4 },
{ key: "dither", label: "Class dither", min: 0, max: 0.6, step: 0.005, default: 0.12 },
{ key: "sparse", label: "Quiet density", min: 0, max: 1, step: 0.01, default: 0.16 },
{ key: "density", label: "Fill", min: 0, max: 1.5, step: 0.01, default: 1 },
{ key: "twinkle", label: "Twinkle rate", min: 0, max: 30, step: 0.5, default: 3 },
{ key: "grain", label: "Paper grain", min: 0, max: 1, step: 0.01, default: 0.1 },
{ key: "light", label: "Key light", min: 0, max: 1, step: 0.01, default: 0.18 },
{ key: "rim", label: "Rim", min: 0, max: 1, step: 0.01, default: 0.35 }
],
/*
* The paper and the seven classes, quiet to peak: grey, blue, cyan,
* green, red, yellow, magenta — the radar legend of the reference.
*/
colors: [
{ key: "paper", label: "Paper", default: "#efe9dc" },
{ key: "c0", label: "Quiet", default: "#a9a9a6" },
{ key: "c1", label: "Class 1", default: "#2e5df0" },
{ key: "c2", label: "Class 2", default: "#38d9ec" },
{ key: "c3", label: "Class 3", default: "#22c35c" },
{ key: "c4", label: "Class 4", default: "#e8322a" },
{ key: "c5", label: "Class 5", default: "#f5d020" },
{ key: "c6", label: "Peak", default: "#e030c0" }
],
/*
Staged on the drift, the swirl, the window and the fill. The grid, the
zoom and the storm scale all multiply a coordinate or sit inside a
floor, so they never move between states. The swirl is an angle,
bounded, and glides safely.
*/
statePresets: {
// at rest: fronts drifting, the ball rolling at a steady turn, the
// quiet areas sparse, a slow twinkle
idle: {
speed: 1,
spin: 0.27,
lo: 0.12,
hi: 0.82,
curve: 1.4,
sparse: 0.16,
density: 1,
twinkle: 3,
warp: 0.5,
swirl: 1.5,
dither: 0.12
},
/*
searching: the storms go FINE and the swirl hard — the storm scale at
three times rest, the vortices twisting at nearly three times the
rest angle on a doubled bend, the roll doubled — with the window
thrown open (quiet threshold at zero, peak at half), so the whole
ball is small, tightly wound systems. The storm scale multiplies a
coordinate, so the glide into and out of thinking passes through a
rescale — chosen deliberately.
*/
thinking: {
speed: 2.4,
spin: 0.51,
lo: 0,
hi: 0.545,
curve: 1.7,
sparse: 0.22,
density: 0.9,
twinkle: 12,
freq: 5.2,
warp: 1.06,
swirl: 4,
dither: 0.18
},
/*
answering: the weather FILLS IN and RACES. The quiet threshold drops
to zero so every cell reads as weather, the fronts widen into red and
yellow with magenta peaks, the drift runs at six times rest on the
thinking roll, and the paper grain comes up.
*/
speaking: {
speed: 6.5,
spin: 0.51,
lo: 0,
hi: 0.8,
curve: 1.3,
sparse: 0.22,
density: 1.15,
twinkle: 5,
warp: 0.45,
swirl: 2,
grain: 0.35,
dither: 0.1
}
},
// the legend holds; the paper cools while searching and warms while
// answering
stateColors: {
idle: { paper: "#efe9dc" },
thinking: { paper: "#e6e9ee" },
speaking: { paper: "#f5e6d0" }
}
};
export type Shdr27Props = Omit<ShaderOrbProps, "variant">;
export function Shdr27({ size = 280, ...rest }: Shdr27Props) {
return <ShaderOrb variant={shdr27Orb} size={size} {...rest} />;
}
export default Shdr27;
30. components/ui/shdr-28.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-28 — a tumbling bit-sphere: nested binary grids shuttering on a ball.
Ported from a golfed twigl listing:
vec2 p=(round(FC.xy)-.5*r)/r.y,v;
for(float i;i++<20.;o+=vec4(fwidth(v=ceil(p)).xyy,
fract(length(v)/i-t*.2))*(1.-o.a))p+=p;
What it actually is, decoded:
- p += p is the whole engine: a BINARY ZOOM. Twenty doublings give
twenty nested power-of-two grids over the same pixel.
- fwidth(v = ceil(p)) is zero inside a cell and spikes exactly where the
cell index jumps — it draws the grid lines of every level. The .xyy
swizzle splits them: x-boundaries in red, y-boundaries in green+blue.
- The alpha channel accumulates fract(length(v)/i - t*.2) — an animated
value per CELL — and the *(1.-o.a) factor is front-to-back UNDER
compositing: each level's cells shutter the levels beneath. That
occlusion cascade is the whole bit-plane flicker.
- Deep levels alias (a cell per pixel) into solid planes; the shutter is
what keeps them from whiting out.
Port decisions:
- THE ORB IS THE OBJECT — the standing design rule (see README). A flat
binary lattice masked to a disc reads as a coin. Instead the lattice is
wrapped ON the ball: dome point, a real 3D tilt + spin rotation (the
spin on its own integrated clock), then a stereographic projection so
the grids curve and compress around the sphere. A shaded body and a
fresnel rim make it a solid, tumbling bit-sphere.
- fwidth() needs the OES_standard_derivatives EXTENSION in WebGL 1, and
the #extension directive cannot legally follow the prelude's
declarations. It is not needed: the zoom chain is exact, so the pixel
footprint at level i is analytic — px0 * 2^i — and the same edge test
falls out of fract() against it, with line width as a free parameter.
- round() is ES 3.0 (the original only pixel-snaps with it) and o, v, i
rely on twigl's zero-init — uninitialised locals are UNDEFINED in
GLSL ES 1.0, so everything is explicit here.
- The clock enters only as an additive phase inside fract(), so the
unbounded integrated clock stays safe, as everywhere in this repo.
- Surface-lit and mask-bounded, so alpha IS coverage — premultiplied
output, the opposite convention from the emissive orbs (see README).
---------------------------------------------------------------------------- */
/*
* The level cap is a `#define`: ES 1.0 requires constant loop bounds.
* uP_levels breaks out early below it.
*/
const BITDUMB_FRAG = `
#define LEVELS 20
// Volume-reactive values, resolved once per fragment in main().
float bitdumbGain;
float bitdumbBody;
mat2 bdRot(float a) {
float c = cos(a);
float s = sin(a);
return mat2(c, -s, s, c);
}
void main() {
bitdumbGain = uP_gain * (1.0 + 0.6 * uInput);
bitdumbBody = uP_body * (1.0 + 0.8 * uOutput);
vec2 uv = orbUV() / uP_radius;
float r2 = dot(uv, uv);
// analytic disc silhouette — this orb is parameterised on the dome, so
// the exact edge is just the unit circle, with the same tunable band as
// the raymarched orbs
float band = mix(0.35, 0.012, clamp(uP_edge, 0.0, 1.0));
float mask = 1.0 - smoothstep(1.0 - band, 1.005, length(uv));
// front dome point and its normal (view space)
float zc = sqrt(max(1.0 - r2, 0.0));
vec3 n = vec3(uv, zc);
// tumble the sphere point with real rotations, then project. abs() on z
// mirror-wraps the hemisphere the tumble turns away, avoiding the
// stereographic pole blow-up.
vec3 sp = n;
sp.yz = bdRot(uP_tilt) * sp.yz;
sp.xz = bdRot(uP_spin) * sp.xz; // integrated clock
vec2 p = sp.xy / (abs(sp.z) + 1.0) * uP_gridScale;
/*
Analytic pixel footprint in grid space, in place of fwidth(): one
screen pixel in uv units, through the radius scale, the dome stretch
(grids compress toward the rim, so a pixel covers more of them there),
and the grid scale. Doubled alongside p every level.
*/
float px = (2.0 / min(uRes.x, uRes.y)) / uP_radius / max(zc, 0.2) * uP_gridScale;
vec4 acc = vec4(0.0);
float phase = uP_speed * 0.2; // integrated clock, additive phase
for (int i = 0; i < LEVELS; i++) {
float fi = float(i) + 1.0;
if (fi > uP_levels) break;
// the listing's engine, kept verbatim: binary zoom
p += p;
px += px;
vec2 v = ceil(p);
vec2 f = fract(p);
// distance to the nearest cell line, against this level's footprint —
// the extension-free fwidth. Deep levels saturate to solid planes,
// exactly like the original's aliasing.
vec2 e2 = 1.0 - smoothstep(vec2(0.0), vec2(px * uP_lineW), min(f, 1.0 - f));
// x-lines and y-lines separately tintable — the original's .xyy
vec3 edgeCol = uC_lineA * e2.x + uC_lineB * e2.y;
// the per-cell shutter value, and the under-compositing that makes
// level i occlude level i+1 — both straight from the listing
float aBit = fract(length(v) / fi - phase) * uP_shutter;
acc += vec4(edgeCol, aBit) * (1.0 - acc.a);
if (acc.a > 0.996) break;
}
vec3 col = acc.rgb * bitdumbGain;
// the ball body: a lambert-shaded base under the lattice, so the orb
// reads as a solid object rather than lines floating on nothing
vec3 L = normalize(vec3(-0.4, 0.5, 0.75));
float shade = 0.25 + 0.75 * clamp(dot(n, L), 0.0, 1.0);
col += uC_base * shade * bitdumbBody;
// fresnel rim to sell the sphere
col += uC_rim * pow(1.0 - zc, uP_rimPow) * uP_rim;
col = pow(max(col, 0.0), vec3(uP_contrast));
// coverage alpha; safety taper fades colour AND alpha, as always
float fade = 1.0 - smoothstep(uP_edgeFade, 1.0, length(orbUV()));
float a = mask * fade;
// Surface-lit orb bounded by a mask: alpha IS coverage, so premultiply —
// the opposite of the emissive orbs (see the note in shdr-31).
gl_FragColor = vec4(col * a, a);
}
`;
export const shdr28Orb: OrbVariant = {
key: "shdr-28",
label: "SHDR-28",
note: "nested binary grids shuttering on a tumbling bit-sphere",
frag: BITDUMB_FRAG,
params: [
{ key: "speed", label: "Shutter drift", min: 0.015, max: 10, step: 0.05, default: 0.5, integrate: true },
{ key: "spin", label: "Tumble rate", min: 0, max: 5, step: 0.03, default: 0.15, integrate: true },
{ key: "tilt", label: "Tumble tilt", min: 0, max: 4, step: 0.02, default: 0.5 },
{ key: "radius", label: "Radius", min: 0.1, max: 3, step: 0.015, default: 0.9 },
{ key: "gridScale", label: "Grid scale", min: 0.5, max: 20, step: 0.1, default: 2 },
// 12 levels / wider lines: past ~12 the deep grids alias into solid white
// planes that swallow the line palette — the state staging below depends
// on the coarse lines and body actually carrying their colours
{ key: "levels", label: "Bit depth", min: 4, max: 20, step: 1, default: 12 },
{ key: "lineW", label: "Line width", min: 0.5, max: 15, step: 0.1, default: 2 },
{ key: "shutter", label: "Shutter", min: 0, max: 4, step: 0.02, default: 1 },
{ key: "gain", label: "Line gain", min: 0.05, max: 10, step: 0.05, default: 1 },
{ key: "body", label: "Body glow", min: 0, max: 5, step: 0.03, default: 1 },
{ key: "rim", label: "Rim light", min: 0, max: 5, step: 0.03, default: 0.6 },
{ key: "rimPow", label: "Rim tightness", min: 0.3, max: 20, step: 0.1, default: 3 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 10, step: 0.05, default: 1 },
{ key: "edge", label: "Edge sharpness", min: 0, max: 1, step: 0.01, default: 1 },
{ key: "edgeFade", label: "Halo falloff", min: 0.1, max: 3, step: 0.015, default: 0.98 }
],
colors: [
{ key: "lineA", label: "X lines", default: "#ff5a4d" },
{ key: "lineB", label: "Y lines", default: "#59d8ff" },
{ key: "base", label: "Body", default: "#101528" },
{ key: "rim", label: "Rim", default: "#bcd8ff" }
],
/*
The states are staged on the two integrated clocks: thinking runs the
shutter cascade hot AND sets the sphere tumbling — the bits computing
furiously while the orb turns them over — and speaking tumbles harder
still while the flicker stays moderate: the orb turning to answer. Both
clocks integrate, so every rate change glides without a phase jump.
*/
statePresets: {
/*
calm: a steady flicker at double the old rate, lazy tumble. Two bits
shallower and the lines a quarter wider, so the grid reads bolder
and coarser; the body glow eased down and the rim pulled tight —
red and white lines on black, no halo.
*/
idle: {
speed: 1,
spin: 0.1,
levels: 10,
lineW: 2.5,
shutter: 1.02,
gain: 1,
body: 0.9,
rim: 0.6,
rimPow: 5.2,
contrast: 1.2
},
// computing: the shutter cascade races (2.4x idle) and the tumble goes
// with it, eight times idle, on wider lines and a lifted gain; the body
// dims so the flickering cells carry the light
thinking: {
speed: 2.4,
spin: 0.81,
lineW: 2.9,
shutter: 0.94,
gain: 1.2,
body: 0.85,
rim: 0.7
},
/*
answering: hard fast tumble on a grid five times finer and seven bits
deeper, so the sphere goes dense with cells; the body is all but cut
and the rim brought up hard and pulled tight, so the light sits on
the limb and the circuitry, not the ball.
gain stays LOW on purpose: the shader multiplies it by (1 + 0.6 *
input volume), and speaking synthesizes input around 0.65 — a 1.35
preset lands near x1.9 effective, which clamps the lines to white
and reads as a pale wash. 0.95 keeps the effective gain near 1.3,
where the red survives.
*/
speaking: {
speed: 3,
spin: 1.1,
gridScale: 10.3,
levels: 17,
shutter: 1.2,
gain: 0.95,
body: 0.27,
rim: 1.53,
rimPow: 10,
contrast: 1.45
}
},
/*
Four stageable colours, and one palette across all three states: red
and white circuitry on pure black. The states are told apart by the
tumble, the grid and the line weight, not the colour.
*/
stateColors: {
idle: { lineA: "#ff1100", lineB: "#ffffff", base: "#000000", rim: "#000000" },
thinking: { lineA: "#ff0000", lineB: "#ffffff", base: "#000000", rim: "#000000" },
speaking: { lineA: "#ff0000", lineB: "#ffffff", base: "#000000", rim: "#000000" }
}
};
export type Shdr28Props = Omit<ShaderOrbProps, "variant">;
export function Shdr28({ size = 280, ...rest }: Shdr28Props) {
return <ShaderOrb variant={shdr28Orb} size={size} {...rest} />;
}
export default Shdr28;
31. components/ui/shdr-29.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-29 — an LED tile wall lighting up in flowing blobs, wrapped on the
ball, with confetti tiles scattered through the bright regions.
Every screen cell is one physical TILE: a bright bevelled face inside a
visible frame, and even an unlit tile keeps a faint dark presence — the
wall itself never disappears, which is what makes the lit blobs read as
light ON hardware rather than paint. A drifting fbm field, sampled
through the stereographic wrap of a rotating dome, decides which tiles
light and how hard; most lit tiles burn warm white, but a per-tile hash
promotes a scattering of them to fully saturated confetti hues that
reshuffle on their own clock.
Construction notes:
- The tile grid is RESOLUTION-RELATIVE (uP_cells across the canvas) —
the lesson shdr-14 learned — so a gallery card and the playground
show the same wall.
- The FIELD samples once per tile (cell centre): one tile, one light
level. The tile geometry renders per fragment so bevels stay crisp.
- Confetti hues come from cos palettes over per-tile hashes; the
promotion cycles with an integrated shuffle clock, so which tiles are
coloured slowly reshuffles — and races while the orb thinks.
- Each state ANIMATES differently on its own integrated clock (drift /
shuffle / pulse), phase-safe as everywhere in this repo.
- Surface-lit and mask-bounded, so alpha IS coverage — premultiplied
output, as in shdr-28.
---------------------------------------------------------------------------- */
const MOSAIC_FRAG = `
void main() {
// Volume coupling: user input widens the lit coverage, agent output turns
// the panel brightness up.
float coverNow = uP_coverage + 0.07 * uInput;
float gainNow = uP_gain * (0.85 + 0.5 * uOutput);
// resolution-relative tile grid — same wall at every size
float cellPx = max(min(uRes.x, uRes.y) / max(uP_cells, 8.0), 4.0);
vec2 cellIdx = floor(gl_FragCoord.xy / cellPx);
vec2 cellCentre = (cellIdx + 0.5) * cellPx;
vec2 g = fract(gl_FragCoord.xy / cellPx); // 0..1 inside the tile
vec2 suv = (2.0 * cellCentre - uRes) / min(uRes.x, uRes.y);
vec2 uv = suv / uP_radius;
float r2 = dot(uv, uv);
// blocky silhouette, cut on the tile grid like the wall itself
float mask = 1.0 - step(1.0, r2);
float z = sqrt(max(1.0 - r2, 0.0));
vec3 n = vec3(uv, z);
// rotating dome, stereographic projection — the blobs roll around the
// ball as the dome turns
float rot = uP_spin; // integrated clock
float cr = cos(rot);
float sr = sin(rot);
vec3 sp = vec3(n.x * cr - n.z * sr, n.y, n.x * sr + n.z * cr);
vec2 p2 = sp.xy / (abs(sp.z) + 1.2) * uP_scale * 3.0;
/*
Per-state motion, each on its own integrated clock:
DRIFT the blob field streams across the wall (idle flows)
CHURN the fluid warp evolves in place (thinking boils)
SHUFFLE the confetti promotion cycles (thinking races it)
PULSE rings radiate from the centre (speaking)
Rates glide; a rate at zero freezes that motion with its phase intact.
The pulse depth is an amplitude, so idle carries no static rings.
*/
float driftT = uP_drift; // integrated clock: blob stream
float churnT = uP_churn; // integrated clock: warp evolution
float shuffleT = uP_shuffle; // integrated clock: confetti reshuffle
vec2 f1 = vec2(driftT * 0.5, -driftT * 0.35);
vec2 f2 = vec2(-churnT * 0.4, churnT * 0.6);
/*
FLUID domain warp: two decorrelated fbm channels displace the sample
point before the blob field reads it, and the displacement itself
evolves on the churn clock. The blobs curl, stretch and merge like
liquid instead of sliding across the wall as one rigid sheet.
*/
vec2 warp = vec2(
fbm(p2 * 0.9 + f2),
fbm(p2 * 0.9 + f2.yx + 13.7)
) - 0.5;
float field = fbm(p2 + f1 + warp * uP_swirl * 2.4);
float lambert = clamp(dot(n, normalize(vec3(-0.45, 0.55, 0.7))), 0.0, 1.0);
float lum = smoothstep(1.0 - coverNow, 1.14 - coverNow, field
+ 0.25 * uP_light * lambert
+ uP_pulse * 0.3 * sin(length(uv) * 5.0 - driftT * 3.2));
lum *= gainNow;
/*
The tile: a bevelled square face inside a frame. The face is the lit
part; the frame stays dark; an unlit tile keeps a faint presence so the
wall reads as hardware even where nothing is lit.
*/
vec2 d2 = abs(g - 0.5);
float d = max(d2.x, d2.y);
float face = 1.0 - smoothstep(0.26, 0.36, d);
float tile = 1.0 - smoothstep(0.42, 0.48, d);
// a soft centre hot-spot on the face, like an LED under a diffuser
float hot = 1.0 - smoothstep(0.0, 0.34, length(d2));
/*
Confetti: a per-tile hash cycles against the shuffle clock, and the top
uP_confetti slice of the cycle is promoted from warm white to a fully
saturated hue drawn from a second hash. Which tiles are coloured
therefore reshuffles continuously — slowly at rest, fast in thought.
*/
float h1 = hash(cellIdx * 1.618 + 7.3);
float h2 = hash(cellIdx * 2.113 + 41.7);
float cyc = fract(h1 + shuffleT * 0.06);
float promoted = step(1.0 - uP_confetti, cyc);
vec3 confetti = 0.5 + 0.5 * cos(6.2831 * (h2 + vec3(0.0, 0.33, 0.67)));
confetti = normalize(confetti + 0.05) * 1.2;
vec3 litCol = mix(uC_lit, confetti, promoted);
// lit face over the dark wall; frames and off-tiles stay faintly present
vec3 offCol = uC_wall * tile;
vec3 onCol = litCol * (face * 1.05 + hot * 0.5) * lum;
vec3 col = offCol + onCol;
col = pow(max(col, 0.0), vec3(uP_contrast));
// Surface-lit orb bounded by a mask: alpha IS coverage, so premultiply —
// the opposite convention from the emissive orbs (see shdr-31).
float a = mask;
gl_FragColor = vec4(col * a, a);
}
`;
export const shdr29Orb: OrbVariant = {
key: "shdr-29",
label: "SHDR-29",
note: "an LED tile wall lighting up in flowing blobs, wrapped on the ball",
frag: MOSAIC_FRAG,
params: [
{ key: "drift", label: "Drift", min: 0, max: 10, step: 0.05, default: 0.45, integrate: true },
{ key: "churn", label: "Churn", min: 0, max: 10, step: 0.05, default: 0.5, integrate: true },
{ key: "swirl", label: "Fluidity", min: 0, max: 3, step: 0.015, default: 1.2 },
{ key: "shuffle", label: "Shuffle", min: 0, max: 20, step: 0.1, default: 0.6, integrate: true },
{ key: "pulse", label: "Pulse depth", min: 0, max: 2, step: 0.01, default: 0 },
{ key: "spin", label: "Roll", min: 0, max: 5, step: 0.03, default: 0.1, integrate: true },
{ key: "radius", label: "Radius", min: 0.15, max: 3, step: 0.015, default: 0.9 },
{ key: "cells", label: "Tile grid", min: 16, max: 160, step: 2, default: 48 },
{ key: "scale", label: "Blob scale", min: 0.3, max: 10, step: 0.1, default: 1.3 },
{ key: "coverage", label: "Coverage", min: 0, max: 1.2, step: 0.01, default: 0.52 },
{ key: "confetti", label: "Confetti", min: 0, max: 1, step: 0.01, default: 0.22 },
{ key: "light", label: "Key light", min: 0, max: 3, step: 0.015, default: 0.6 },
{ key: "gain", label: "Panel gain", min: 0.05, max: 5, step: 0.05, default: 1 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 10, step: 0.05, default: 1 }
],
colors: [
{ key: "lit", label: "Lit tile", default: "#fff2dd" },
{ key: "wall", label: "Wall", default: "#161616" }
],
/*
Each state animates DIFFERENTLY — its own motion, same palette and
composition throughout (no stateColors on purpose).
*/
/*
Coverage is staged DOWN in the active states on purpose: the synthesized
volumes push it up, and without the counterweight thinking and speaking
flood the wall with light — the composition lives on its big dark
voids, so every state keeps them.
*/
statePresets: {
// idle FLOWS: blobs streaming and curling slowly, lava-lamp pace
idle: {
drift: 0.45,
churn: 0.5,
shuffle: 0.6,
pulse: 0,
spin: 0.1,
coverage: 0.52,
gain: 1
},
// thinking BOILS: the stream stops but the fluid warp churns hard in
// place while the confetti races — blobs kneading among the voids
thinking: {
drift: 0.1,
churn: 1.9,
shuffle: 5,
pulse: 0,
spin: 0.03,
coverage: 0.42,
gain: 0.95
},
// speaking PULSES: rings radiate through the flowing wall as the dome
// rolls — the rings carve dark bands as much as they light bright ones
speaking: {
drift: 0.5,
churn: 0.9,
shuffle: 1.2,
pulse: 0.55,
spin: 0.45,
coverage: 0.44,
gain: 1.15
}
}
};
export type Shdr29Props = Omit<ShaderOrbProps, "variant">;
export function Shdr29({ size = 280, ...rest }: Shdr29Props) {
return <ShaderOrb variant={shdr29Orb} size={size} {...rest} />;
}
export default Shdr29;
32. components/ui/shdr-30.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-30 — a meadow folding into itself, falling forever toward a blue
vanishing point.
Not a port of a golfed listing like its neighbours: built to a reference
picture — a flower meadow whose sky peels up and inward through nested
rectangular frames, each one the same landscape a size smaller, until
the recursion closes on a scrap of blue sky and cloud.
How it is put together:
- THE RECURSION IS A LOGARITHM. Take the CHEBYSHEV norm of the point,
max(|x|,|y|), and the level sets are SQUARES rather than circles —
which is the whole difference between this and a spiral tunnel.
log of that norm, base K, is a continuous frame index; its fractional
part says where inside one frame the fragment sits, and raising K back
to that fraction gives the radius the fragment maps to in the BASE
frame. Every fragment therefore reads one small picture, and the
picture contains itself.
- THE CLOCK GOES INTO THE LOG, not into a scale. Adding to the frame
index slides the whole mapping, so the tunnel flies inward forever
with no seam to hide and no accumulating error — an integrated clock
makes the rate tunable without jumping the fall.
- THE PICTURE is painted in the base frame's own coordinates: sky and
cloud above the horizon line, canopy and meadow below it, and flowers
hashed into the low ground. Because every frame reads the SAME base
picture, the clouds and flowers repeat at every scale, which is what
the reference does and what makes the recursion legible.
- THE SMEAR ON THE WALLS is the reference's most distinctive texture,
and it comes for free from the geometry: the land noise is sampled on
(distance around the frame, frame index), with a low frequency on the
second axis, so its features run long in the direction the recursion
stretches them.
- AERIAL PERSPECTIVE IS LOAD-BEARING. Depth has to come from the SCREEN
radius, not from the position inside a frame — every frame has the
same fractional part, so that number carries no depth at all. Fading
toward the sky colour as the screen radius goes to zero is what makes
the middle read as far away rather than as small, and it doubles as
the guard on the log at the exact centre.
Orb decisions, each one a rule in the README:
- THE ORB IS THE OBJECT: the tunnel is sampled through a stereographic
projection of the dome, so the frames compress toward the limb the
way a texture on a real sphere does, and the vanishing point sits at
the ball's centre. With the fresnel sheen over it the ball reads as
glass with a world falling away inside it, not as a disc cut out of a
picture.
- The frame index runs continuously across frames while the picture
coordinate resets at every boundary. That reset is not a defect to
smooth away: it IS the edge of the picture, and it draws the nested
frame borders the reference is made of.
- Distance around the frame is a real arc length around the square, not
atan and not a swap between the two axes at the corners. Both of those
put a seam on the wall; the arc length puts its one wrap at a corner,
where a frame corner already is.
- Surface-lit and mask-bounded, so alpha IS coverage — premultiplied
output, as in shdr-17.
---------------------------------------------------------------------------- */
/*
* AA is a `#define`: ES 1.0 requires constant loop bounds. The recursion
* crowds an unbounded number of frames into the last few pixels before the
* limb, so supersampling here is not polish — it is the only thing standing
* between the rim and a band of noise.
*/
const DROSTE_FRAG = `
#define AA 2
// Volume-reactive values, resolved once per fragment in main().
float drosteHaze;
float drosteCloud;
float drosteBloom;
/*
Arc length around the unit square, in [0,8), counter-clockwise from the
bottom-right corner — two units per face. Continuous everywhere except
its single wrap, which lands on a corner.
*/
float squareArc(vec2 q) {
if (abs(q.x) >= abs(q.y)) {
if (q.x > 0.0) return q.y + 1.0;
return 5.0 - q.y;
}
if (q.y > 0.0) return 3.0 - q.x;
return 7.0 + q.x;
}
/*
Flower colour by hash: mostly white daisies, then the planted warm, then
the cornflower blues — which reuse the SKY colour rather than adding a
sixth stop, because that is what keeps them reading as part of the same
picture instead of as confetti thrown over it.
*/
vec3 drosteFlower(float h) {
vec3 c = uC_cloud;
c = mix(c, uC_bloom, step(0.52, h));
c = mix(c, uC_sky, step(0.86, h));
return c;
}
vec3 drosteRender(vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
float R = max(uP_radius, 0.001);
// the dome: the front hemisphere of a unit ball, in screen space
vec2 pl = uv / R;
float z = sqrt(max(1.0 - dot(pl, pl), 0.0));
float fall = uP_fall; // integrated clock: the flight inward
float drift = uP_drift; // integrated clock: weather
/*
The frame tilt is a STATIC angle, not an integrated clock like the roll
every other orb here gets. Those clocks seed at a random phase per
mount, which is exactly right for a field with no preferred direction
and exactly wrong for a picture: it lands the sky down one side of the
ball and the meadow up the other. This one has an up.
*/
float sw = uP_tilt;
// stereographic wrap — the tunnel is inside the ball, and compresses
// toward the limb the way a texture on a sphere does
vec2 p = pl / (z + 1.0 + uP_bulge) * uP_scale;
p = mat2(cos(sw), -sin(sw), sin(sw), cos(sw)) * p;
/*
The Chebyshev norm makes the level sets SQUARES. The floor on it is
what keeps the logarithm finite at the dead centre; the haze below
covers that last pixel anyway.
*/
float m = max(max(abs(p.x), abs(p.y)), 0.002);
float K = max(uP_ratio, 1.05);
float L = log2(m) / log2(K) + fall;
vec2 q = p / m; // direction, on the unit square boundary
float sm = pow(K, fract(L)); // this fragment's radius in base-frame units
vec2 P = q * sm; // where it lands in the base picture
float Yn = P.y / K; // picture height, about -1 at the bottom edge
float arc = squareArc(q); // distance around the frame
// ---- sky -----------------------------------------------------------
vec3 col = mix(uC_sky * 0.72, uC_sky, clamp(Yn * 1.3, 0.0, 1.0));
/*
Cloud and land are both read in BASE-PICTURE coordinates, so every
frame carries the same weather at its own scale — which is the whole
point of a picture that contains itself.
*/
float skyMask = smoothstep(uP_horizon - 0.3, uP_horizon + 0.2, Yn);
float cl = fbm(P * uP_cloudScale + vec2(drift, drift * 0.3));
cl = smoothstep(drosteCloud, drosteCloud + 0.16, cl);
col = mix(col, uC_cloud, cl * (0.2 + 0.8 * skyMask));
// ---- land ----------------------------------------------------------
/*
The smear. Sampled on (distance around the frame, frame index) with a
low frequency on the second axis, so features run LONG in the
direction the recursion stretches them — the streaked walls of the
reference, straight out of the geometry.
*/
float streak = fbm(vec2(arc * uP_streakFreq, L * uP_streakRad));
vec3 land = mix(uC_canopy, uC_meadow, smoothstep(0.02, -0.62, Yn));
land *= 0.42 + 1.25 * streak;
// water: the low ground holds it where the streak field pools
float water = smoothstep(0.42, 0.16, streak) * smoothstep(0.05, -0.3, Yn);
land = mix(land, uC_water, water * uP_water);
/*
Flowers, hashed one to a cell on the same (around, index) grid, jittered
inside it. Densest low in the picture and gone by the horizon.
*/
vec2 fg = vec2(arc * uP_flowerScale, L * uP_flowerScale * 0.3);
vec2 fc = floor(fg);
vec2 ff = fract(fg) - 0.5;
vec2 dcv = ff - (vec2(hash(fc + 3.7), hash(fc + 19.1)) - 0.5) * 0.6;
float petal = smoothstep(uP_flowerSize, uP_flowerSize * 0.35, length(dcv));
float present = step(1.0 - drosteBloom, hash(fc + 51.3));
float meadow = smoothstep(0.13, -0.38, Yn);
land = mix(land, drosteFlower(hash(fc + 7.9)), petal * present * meadow);
float landMask = 1.0 - smoothstep(uP_horizon - 0.12, uP_horizon + 0.16, Yn);
col = mix(col, land, landMask);
/*
The picture's own edge. Darkening across the frame and resetting hard
at its boundary is not an artefact to smooth away — it draws the
nested borders the reference is built out of.
*/
col *= mix(1.0, uP_frameShade, fract(L));
/*
Aerial perspective, from the SCREEN radius. Every frame has the same
fractional part, so depth cannot come from inside a frame — it has to
come from how far in the fragment sits. This is what makes the middle
read as far away instead of merely small.
*/
float deep = 1.0 - smoothstep(0.0, uP_hazeRange, m);
col = mix(col, uC_sky, deep * drosteHaze);
col = pow(max(col, vec3(0.0)), vec3(uP_contrast)) * uP_gain;
float lum = dot(col, vec3(0.299, 0.587, 0.114));
col = mix(vec3(lum), col, uP_saturation);
// dome shading, kept light — this is a window, not a lit surface
vec3 n = vec3(pl, z);
float lambert = clamp(dot(n, normalize(vec3(-0.45, 0.55, 0.72))), 0.0, 1.0);
col *= 0.72 + uP_light * lambert;
// the glass: a strong fresnel is what turns a picture into a sphere
// with a world inside it
float fres = 1.0 - z;
fres = fres * fres * fres;
col += uC_sheen * uP_rim * fres;
return col;
}
void main() {
// Volume coupling: the user's voice thickens the weather, the agent's
// clears the haze and brings the meadow into flower.
drosteCloud = clamp(uP_cloudCover - 0.12 * uInput, 0.02, 0.98);
drosteHaze = uP_haze * (1.0 - 0.25 * uOutput);
drosteBloom = clamp(uP_flowerDensity * (1.0 + 0.5 * uOutput), 0.0, 1.0);
vec2 uv = orbUV();
float mask = smoothstep(0.012, -0.012, length(uv) - max(uP_radius, 0.001));
// Two fbm evaluations and a flower grid per sample — none of it worth
// paying for outside the silhouette.
if (mask <= 0.0) {
gl_FragColor = vec4(0.0);
return;
}
vec3 col = vec3(0.0);
#if AA > 1
for (int mx = 0; mx < AA; mx++) {
for (int my = 0; my < AA; my++) {
vec2 off = (vec2(float(mx), float(my)) + 0.5) / float(AA) - 0.5;
col += drosteRender(gl_FragCoord.xy + off);
}
}
col /= float(AA * AA);
#else
col = drosteRender(gl_FragCoord.xy);
#endif
// Surface orb bounded by a mask: alpha IS coverage, so premultiply — the
// opposite convention from the emissive orbs (see shdr-31).
float a = mask;
gl_FragColor = vec4(max(col, vec3(0.0)) * a, a);
}
`;
export const shdr30Orb: OrbVariant = {
key: "shdr-30",
label: "SHDR-30",
note: "a meadow folding into itself toward a blue vanishing point",
frag: DROSTE_FRAG,
params: [
{ key: "fall", label: "Fall speed", min: 0, max: 4, step: 0.01, default: 0.12, integrate: true },
{ key: "tilt", label: "Frame tilt", min: -1.6, max: 1.6, step: 0.01, default: 0 },
{ key: "drift", label: "Weather drift", min: 0, max: 4, step: 0.02, default: 0.2, integrate: true },
{ key: "radius", label: "Radius", min: 0.15, max: 3, step: 0.015, default: 0.9 },
{ key: "scale", label: "Tunnel scale", min: 0.3, max: 20, step: 0.1, default: 4.5 },
{ key: "bulge", label: "Dome bulge", min: 0, max: 4, step: 0.02, default: 0.25 },
{ key: "ratio", label: "Frame ratio", min: 1.1, max: 6, step: 0.02, default: 1.8 },
{ key: "horizon", label: "Horizon", min: -0.9, max: 0.9, step: 0.01, default: 0.14 },
{ key: "cloudScale", label: "Cloud scale", min: 0.1, max: 8, step: 0.05, default: 1.6 },
{ key: "cloudCover", label: "Cloud cover", min: 0.02, max: 0.98, step: 0.01, default: 0.46 },
{ key: "streakFreq", label: "Wall detail", min: 0.2, max: 20, step: 0.1, default: 5 },
{ key: "streakRad", label: "Smear", min: 0.02, max: 4, step: 0.02, default: 0.5 },
{ key: "water", label: "Water", min: 0, max: 1, step: 0.01, default: 0.7 },
{ key: "flowerScale", label: "Flower scale", min: 2, max: 120, step: 1, default: 44 },
{ key: "flowerDensity", label: "Flower density", min: 0, max: 1, step: 0.01, default: 0.55 },
{ key: "flowerSize", label: "Flower size", min: 0.05, max: 0.6, step: 0.01, default: 0.26 },
{ key: "frameShade", label: "Frame shading", min: 0.2, max: 1.4, step: 0.01, default: 0.62 },
{ key: "haze", label: "Aerial haze", min: 0, max: 1, step: 0.01, default: 0.85 },
{ key: "hazeRange", label: "Haze reach", min: 0.005, max: 1.5, step: 0.005, default: 0.09 },
{ key: "gain", label: "Brightness", min: 0.05, max: 4, step: 0.02, default: 1.05 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 10, step: 0.05, default: 1.05 },
{ key: "saturation", label: "Saturation", min: 0, max: 4, step: 0.02, default: 1.1 },
{ key: "light", label: "Key light", min: 0, max: 3, step: 0.015, default: 0.35 },
{ key: "rim", label: "Rim sheen", min: 0, max: 3, step: 0.015, default: 0.55 }
],
/*
* Six stops, and they are the picture rather than a palette: the sky the
* recursion closes on, its cloud, the dark canopy, the meadow under the
* flowers, the planted warm colour, and the glass.
*/
colors: [
{ key: "sky", label: "Sky", default: "#4a92e0" },
{ key: "cloud", label: "Cloud", default: "#f7fbff" },
{ key: "canopy", label: "Canopy", default: "#12401f" },
{ key: "meadow", label: "Meadow", default: "#5aa63a" },
{ key: "water", label: "Water", default: "#156f6a" },
{ key: "bloom", label: "Bloom", default: "#ff6a3a" },
{ key: "sheen", label: "Sheen", default: "#cfe6ff" }
],
/*
Staged on the fall, which is the orb's whole subject, and on the haze,
which decides how far into the recursion the eye can see. Frame ratio
sets how many frames land on the ball; it differs only for idle, and
the glide out of rest reads as the tunnel breathing once.
*/
statePresets: {
/*
at rest: a slow fall, deep haze, weather barely moving — on a frame
ratio nearly double the working states, so fewer, larger frames land
on the ball, with the horizon dropped below centre. The wall detail
is coarsened to a broad smear, the water drained entirely, and the
meadow thinned to sparse, oversized flowers; brighter, and more
saturated, than the states it falls into.
*/
idle: {
fall: 0.12,
drift: 0.2,
bulge: 0.2,
ratio: 3.06,
horizon: -0.11,
cloudScale: 1.2,
cloudCover: 0.42,
streakFreq: 2.9,
streakRad: 0.54,
water: 0,
flowerScale: 24,
flowerDensity: 0.27,
flowerSize: 0.43,
haze: 0.8,
hazeRange: 0.09,
gain: 1.38,
saturation: 1.56,
light: 0.345,
rim: 0.555
},
/*
searching: the fall QUINTUPLES and the haze closes in over three
times as far, so the recursion is swallowed within a frame or two of
the middle — the eye is pulled down a tunnel it cannot see the end
of. The weather thickens and the meadow goes out of flower. Lit hard
against that: the key light nearly triples, and the gain, contrast
and saturation all come up, so what the haze leaves is vivid.
*/
thinking: {
fall: 0.6,
drift: 0.5,
haze: 1,
hazeRange: 0.3,
cloudCover: 0.3,
flowerDensity: 0.28,
gain: 1.42,
contrast: 1.45,
saturation: 2,
light: 0.96
},
/*
answering: the fall goes FASTEST of the three — twelve times idle —
on a drift five times as quick and a tunnel nearly doubled in scale,
with the frames given a slight tilt. The haze lifts to less than half
idle over a longer reach, opening the recursion to the vanishing
point; the walls go to fine, wide-smeared detail, the clouds scale up
threefold, and the meadow comes fully into flower on larger blooms.
Lit and saturated hardest of the three.
*/
speaking: {
fall: 1.47,
tilt: 0.03,
drift: 1.54,
scale: 8.7,
horizon: 0.03,
cloudScale: 3.65,
cloudCover: 0.44,
streakFreq: 11,
streakRad: 1.48,
flowerScale: 32,
flowerDensity: 0.95,
haze: 0.38,
hazeRange: 0.315,
gain: 1.36,
contrast: 1.45,
saturation: 2.32,
light: 0.57
}
},
// the picture keeps its own colours; the states move the weather and the
// light, cooling toward overcast while searching and warming while
// answering
stateColors: {
idle: {
sky: "#4a92e0",
cloud: "#f7fbff",
canopy: "#12401f",
meadow: "#5aa63a",
water: "#156f6a",
bloom: "#ff6a3a",
sheen: "#cfe6ff"
},
thinking: {
sky: "#3f6fa8",
cloud: "#dde8f4",
canopy: "#0e2c2e",
meadow: "#3f7f5c",
water: "#12525f",
bloom: "#7d8cff",
sheen: "#b6cdf0"
},
speaking: {
sky: "#6fb0ec",
cloud: "#fff6e8",
canopy: "#204a16",
meadow: "#7cc23f",
water: "#1d8a76",
bloom: "#ffb02e",
sheen: "#ffe3c4"
}
}
};
export type Shdr30Props = Omit<ShaderOrbProps, "variant">;
export function Shdr30({ size = 280, ...rest }: Shdr30Props) {
return <ShaderOrb variant={shdr30Orb} size={size} {...rest} />;
}
export default Shdr30;
33. components/ui/shdr-31.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-31 — a raymarched SDF shell with volumetric godrays.
The distance field is the space between a sine-warped sphere and a plain one
(`-smin(warped, inverted, k)`), so the camera looks into a glowing hollow
rather than at a solid surface. Light is accumulated along every ray as an
inverse-square falloff gated by the shell, which is what produces the
god-ray bleed — there is no light source, only the integral.
Ported to WebGL 1 / GLSL ES 1.0, which needed four fixes:
1. `transpose()` is ES 3.0 only — hand-written as `transpose3` below.
2. `fragColor` was assigned in main() but never declared; ES 1.0 writes to
the built-in `gl_FragColor`.
3. ES 1.0 restricts a for-loop condition to comparing the index against a
constant, so the march exits via `break` instead.
4. `u_time` / `u_resolution` are the runtime's `uP_speed` clock and `uRes`.
The original returned an opaque frame. Orbs composite onto the page, so the
accumulated luminance drives alpha instead and the orb glows over whatever
is behind it.
---------------------------------------------------------------------------- */
/*
* Supersampling factor and march ceiling are `#define`s, not params: ES 1.0
* needs constant loop bounds. AA=2 means 4 full marches per pixel — at DPR 2
* that is 16 marches per CSS pixel, which is why this ships at 1.
*/
const CORONA_FRAG = `
#define AA 1
#define MAX_STEPS 256
mat3 transpose3(mat3 m) {
return mat3(
m[0][0], m[1][0], m[2][0],
m[0][1], m[1][1], m[2][1],
m[0][2], m[1][2], m[2][2]
);
}
// An artistic tumble, not an orthonormal rotation — the axes shear against each
// other so the shell never repeats a clean spin.
mat3 coronaRot(float a) {
return mat3(
cos(a), sin(a / 2.0) * sin(a), sin(a) * cos(a / 2.0),
0.0, cos(a / 2.0), -sin(a / 2.0),
-sin(a), sin(a / 2.0) * cos(a), cos(a / 2.0) * cos(a)
);
}
mat3 globalRot;
mat3 globalInvRot;
// Volume-reactive values, resolved once per fragment in main().
float shellRadius;
float warpAmount;
float rayGain;
float warpFreqNow;
float smoothKNow;
float smin(float a, float b, float k) {
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
return mix(b, a, h) - k * h * (1.0 - h);
}
float coronaSDF(vec3 p) {
vec3 p1 = p;
p1.zyx += sin(p.xzy * warpFreqNow) / max(warpAmount, 0.001);
return -smin(length(p1) - shellRadius, shellRadius - length(p), smoothKNow);
}
vec3 shellColor(vec3 p) {
float eps = 0.001;
vec3 normal = globalInvRot * normalize(vec3(
coronaSDF(p + vec3(eps, 0.0, 0.0)) - coronaSDF(p - vec3(eps, 0.0, 0.0)),
coronaSDF(p + vec3(0.0, eps, 0.0)) - coronaSDF(p - vec3(0.0, eps, 0.0)),
coronaSDF(p + vec3(0.0, 0.0, eps)) - coronaSDF(p - vec3(0.0, 0.0, eps))
));
vec3 next = 1.0 - (normal * 0.5 + 0.5);
next = vec3(dot(next, vec3(1.0)) / 3.0);
return 1.025 - next * next;
}
vec4 coronaRender(vec2 fragCoord) {
vec2 uv = (fragCoord * 2.0 - uRes) / min(uRes.x, uRes.y);
vec3 ro = vec3(0.0, 0.0, -uP_camDist);
vec3 rd = normalize(vec3(uv, uP_fov));
ro = globalRot * ro;
rd = globalRot * rd;
vec3 p = ro;
float d = 1.0;
float t = 0.0;
float godrays = 0.0;
for (int i = 0; i < MAX_STEPS; i++) {
if (d <= 0.005 || t >= uP_maxDist) break;
p = ro + rd * t;
d = coronaSDF(p) / max(uP_stepScale, 0.5);
// Gate the accumulation on the shell so light bleeds out of the hollow
// instead of glowing uniformly through empty space.
float fog = length(p) > shellRadius
? smoothstep(0.0, 0.5, coronaSDF(normalize(p) * shellRadius))
: 1.0;
godrays += (rayGain / (1.0 + dot(p, p) * uP_rayFalloff)) * fog;
t += d;
}
vec3 col = vec3(uP_ambient);
if (t < uP_maxDist) col = shellColor(p) * uP_surfaceLit;
col += godrays;
return vec4(col, 1.0);
}
void main() {
float animTime = uP_speed; // integrated clock
globalRot = coronaRot(animTime);
globalInvRot = transpose3(coronaRot(animTime));
/*
One shared BACK-AND-FORTH phase for the swept values. sin() of an
integrated clock is a true round trip — it eases through both ends
instead of snapping at a wrap, which fract() or mod() would do.
Every swept value is resolved HERE, once per fragment, and never read
straight from its uniform inside coronaSDF: the normal estimate calls
that SDF six more times, and a value that moved between those calls
would corrupt the finite difference and pit the shading.
uP_sweepRate is its own integrated clock, so the breathing rate tunes
without jumping the phase, and it only ever enters through sin() —
safe for an unbounded clock. All three swings share the phase, so the
shell breathes as one motion rather than three unrelated wobbles.
*/
float sweepPhase = sin(uP_sweepRate);
// Louder agent output pushes the godrays; user input roughens the shell and
// swells it slightly, so the silhouette breathes with speech.
shellRadius = uP_radius + uP_swell * uInput;
warpAmount = (uP_warp + uP_warpSwing * sweepPhase) * (1.0 - 0.25 * uInput - 0.15 * uOutput);
warpAmount = max(warpAmount, 0.05);
rayGain = uP_rayGain * (0.7 + 0.8 * uOutput + 0.3 * uInput);
warpFreqNow = max(uP_warpFreq + uP_freqSwing * sweepPhase, 0.05);
/*
smoothK reaches EXACTLY zero at the bottom of the speaking sweep
(0.25 +/- 0.25), and smin() divides by it — an unguarded zero is a
NaN across the whole SDF. The floor keeps the blend hard but finite.
*/
smoothKNow = max(uP_smoothK + uP_smoothSwing * sweepPhase, 0.005);
vec4 acc = vec4(0.0);
#if AA > 1
for (int mx = 0; mx < AA; mx++) {
for (int my = 0; my < AA; my++) {
vec2 offset = vec2(float(mx), float(my)) / float(AA) - 0.5;
acc += coronaRender(gl_FragCoord.xy + offset);
}
}
acc /= float(AA * AA);
#else
acc = coronaRender(gl_FragCoord.xy);
#endif
// Luminance becomes alpha so the orb composites onto the page instead of
// painting an opaque square.
//
// The colour is emitted light, so it is already premultiplied: rgb is what
// the orb adds, alpha is only how much background it hides. Multiplying rgb
// by alpha again (the usual move for a lit surface) would darken the glow
// quadratically and wash the godrays out.
vec3 col = clamp(acc.rgb, 0.0, 1.0);
float lum = dot(col, vec3(0.2126, 0.7152, 0.0722));
float a = clamp(lum * uP_alphaGain, 0.0, 1.0);
// The godrays are volumetric, so they reach the frame boundary and would
// otherwise show the canvas as a hard-edged glowing square. Taper radially to
// let the halo fall off into the page instead — colour as well as alpha,
// since premultiplied output would otherwise keep emitting at full brightness
// right up to the cutoff and leave a visible rim.
float fade = 1.0 - smoothstep(uP_edgeFade, 1.0, length(orbUV()));
col *= fade;
a *= fade;
gl_FragColor = vec4(col, a);
}
`;
export const shdr31Orb: OrbVariant = {
key: "shdr-31",
label: "SHDR-31",
note: "raymarched shell, volumetric godrays",
frag: CORONA_FRAG,
params: [
{ key: "speed", label: "Anim speed", min: 0.015, max: 10, step: 0.05, default: 1, integrate: true },
{ key: "sweepRate", label: "Sweep rate", min: 0, max: 6, step: 0.02, default: 0.5, integrate: true },
{ key: "radius", label: "Shell radius", min: 0.4, max: 10, step: 0.05, default: 2.6 },
{ key: "swell", label: "Input swell", min: 0, max: 3, step: 0.015, default: 0.18 },
{ key: "warp", label: "Warp divisor", min: 0.3, max: 10, step: 0.05, default: 0.9 },
{ key: "warpFreq", label: "Warp frequency", min: 0.15, max: 30, step: 0.15, default: 5.25 },
{ key: "warpSwing", label: "Warp swing", min: 0, max: 8, step: 0.05, default: 0 },
{ key: "freqSwing", label: "Frequency swing", min: 0, max: 15, step: 0.05, default: 0 },
{ key: "smoothSwing", label: "Softness swing", min: 0, max: 2, step: 0.005, default: 0 },
{ key: "smoothK", label: "Blend softness", min: 0.015, max: 4, step: 0.02, default: 0.42 },
{ key: "rayGain", label: "Godray gain", min: 0, max: 4, step: 0.02, default: 0.8 },
{ key: "rayFalloff", label: "Godray falloff", min: 0.3, max: 100, step: 0.5, default: 10.5 },
{ key: "surfaceLit", label: "Surface light", min: 0, max: 3, step: 0.015, default: 0.105 },
{ key: "ambient", label: "Ambient", min: 0, max: 1, step: 0.005, default: 0 },
{ key: "alphaGain", label: "Alpha gain", min: 0.05, max: 15, step: 0.1, default: 3 },
{ key: "edgeFade", label: "Halo falloff", min: 0.1, max: 3, step: 0.015, default: 0.45 },
{ key: "camDist", label: "Camera distance", min: 1.5, max: 100, step: 0.5, default: 12 },
{ key: "fov", label: "Lens", min: 0.3, max: 20, step: 0.1, default: 3.5 },
{ key: "stepScale", label: "Step safety", min: 0.3, max: 30, step: 0.5, default: 9 },
{ key: "maxDist", label: "Max distance", min: 3, max: 100, step: 1, default: 22 }
],
colors: [],
statePresets: {
/*
idle is the reference look, dialled in by hand: every swing at zero,
so the shell holds still and only the tumble moves.
*/
idle: {
speed: 1,
swell: 0.18,
warp: 0.9,
warpFreq: 5.25,
smoothK: 0.42,
rayGain: 0.8,
rayFalloff: 10.5,
alphaGain: 3,
warpSwing: 0,
freqSwing: 0,
smoothSwing: 0
},
// thinking: warp frequency breathes 5 <-> 9 (7 +/- 2), everything else held
thinking: {
speed: 0.9,
swell: 0.66,
warp: 1,
warpFreq: 7,
smoothK: 0.58,
rayGain: 0.52,
rayFalloff: 8,
alphaGain: 1.8,
sweepRate: 0.5,
freqSwing: 2,
warpSwing: 0,
smoothSwing: 0
},
/*
speaking: three values sweep together, all of them round trips —
warp 1.5 <-> 6 (3.75 +/- 2.25)
warpFreq 15 <-> 24 (19.5 +/- 4.5)
smoothK 0 <-> 0.5 (0.25 +/- 0.25, floored in the shader)
*/
speaking: {
speed: 0.9,
swell: 0.66,
warp: 3.75,
warpSwing: 2.25,
warpFreq: 19.5,
freqSwing: 4.5,
smoothK: 0.25,
smoothSwing: 0.25,
sweepRate: 0.8,
rayGain: 0.52,
rayFalloff: 8,
alphaGain: 1.8
}
}
};
export type Shdr31Props = Omit<ShaderOrbProps, "variant">;
export function Shdr31({ size = 280, ...rest }: Shdr31Props) {
return <ShaderOrb variant={shdr31Orb} size={size} {...rest} />;
}
export default Shdr31;
34. components/ui/shdr-32.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-32 — a galaxy, marched as a volume of gas and dust inside the ball.
Built the way the raymarched orbs here are built (shdr-18, shdr-22,
shdr-21), not as a picture: the ray walks through the ball and at every
step reads a DENSITY, accumulates its emission front-to-back and carries
a transmittance so the near gas veils the far. The density is a galaxy:
- A FLARED DISC. Density falls exponentially in the cylindrical radius
and exponentially in height above the plane, with a scale height that
grows outward, so the disc is razor-thin at the core and puffs toward
the rim the way real discs do. A Gaussian BULGE sits on the centre.
- LOG-SPIRAL ARMS modulate it: cos(N*phi - k*log(rho)), N arms wound by
k, raised to a power for lanes. N multiplies phi so it stays an integer
(continuity across the branch cut) and, with k, never moves between
states — a gliding winding rakes the arms across the disc.
- TURBULENCE breaks everything into filaments: the same feedback curl the
family uses (q += cos(q.yzx * f + t) / f, four octaves, each reading the
last one's result with its components rolled), with the clock entering
as a shared phase so the gas boils coherently. Its sum is thresholded
into clumps, and it also wobbles the arm phase so the arms fray.
- DUST is the absorption. The transmittance is a vec3, and the extinction
is weighted toward blue, so light seen through thick gas comes out
reddened — the brown lanes of a galaxy photograph, for free, from the
physics rather than a painted-on colour.
- The colour is a ramp keyed to the cylindrical radius: three stops, the
core, the inner arms and the outer arms, so the hue changes with
distance from the centre; the bulge whitens toward the core colour.
- STARS are not marched — a volumetric point smears along the ray into a
streak. They are a hashed lattice evaluated at the ray's analytic hit
with the galactic plane, denser in the arms, then dimmed by the
transmittance the march left, so dust in front of them veils them.
The galaxy plane is tilted toward the viewer and turns on its own
integrated clock. Orb-family conventions throughout: the analytic
silhouette from shdr-01, tanh tone map with a tunable knee, alpha from
the peak channel for emitted light, a night fill so the ball reads as a
solid sphere behind the gas. Uninitialised locals are explicit; loop
bounds are defines.
---------------------------------------------------------------------------- */
const GALAXY_FRAG = `
#define STEPS 56
#define TURB_OCT 4
// Volume-reactive values, resolved once per fragment in main().
float galDensity;
float galCore;
float galFalloff;
/*
The galactic density at a point in the galaxy's own frame: the disc lies
in xz, the normal is y. Returns the density; writes the arm weight and the
cylindrical radius for the colour.
*/
float galaxy(vec3 p, float t, out float arm, out float rho) {
rho = length(p.xz);
float h = p.y;
// atan(0, 0) is undefined; the exact axis is all bulge anyway
float phi = rho > 1e-4 ? atan(p.z, p.x) : 0.0;
float lr = log(max(rho, 0.02));
float armPhase = phi * uP_arms - uP_wind * lr;
// feedback curl turbulence, shared phase
vec3 q = p * uP_turbScale;
float f = 1.0;
for (int k = 0; k < TURB_OCT; k++) {
q += cos(q.yzx * f + t) / f;
f *= 1.9;
}
float n = (sin(q.x) + sin(q.y) + sin(q.z)) / 3.0 * 0.5 + 0.5;
float clump = smoothstep(uP_threshold, 1.0, n);
arm = 0.5 + 0.5 * cos(armPhase + (n - 0.5) * uP_ragged);
arm = pow(arm, uP_armSharp);
float scaleH = uP_thick * (0.12 + rho);
float disc = exp(-rho * galFalloff) * exp(-abs(h) / scaleH);
float bulge = exp(-dot(p, p) * uP_bulge);
float dens = disc * (0.08 + 1.6 * arm) * (0.25 + 0.75 * clump) + bulge * galCore;
return dens * galDensity;
}
/*
One lattice of hashed stars. Each cell either carries a star or not, at a
hashed position, with its own twinkle rate; the 3x3 neighbourhood is
gathered so a star near a cell wall is not clipped.
*/
float starField(vec2 p, float density, float size, float twinkleT) {
vec2 id = floor(p);
vec2 f = fract(p);
float acc = 0.0;
for (int j = -1; j <= 1; j++) {
for (int i = -1; i <= 1; i++) {
vec2 o = vec2(float(i), float(j));
vec2 cid = id + o;
float h = hash(cid);
if (h > density) continue;
vec2 sp = o + vec2(hash(cid + 1.3), hash(cid + 2.7));
float dd = length(f - sp);
float tw = 0.55 + 0.45 * sin(twinkleT * (1.5 + 5.0 * hash(cid + 5.1)) + h * 40.0);
float sz = size * (0.5 + 1.2 * hash(cid + 8.9) * hash(cid + 8.9));
acc += tw * exp(-dd * dd / (sz * sz)) * (0.4 + 0.6 * h / max(density, 0.001));
}
}
return acc;
}
vec4 galaxyRender(vec2 fragCoord) {
vec2 uv = (2.0 * fragCoord - uRes) / min(uRes.x, uRes.y);
vec3 ro = vec3(0.0, 0.0, uP_camDist);
vec3 rd = normalize(vec3(uv, -uP_focal));
float t = uP_churn; // integrated clock: the turbulence boils
float spin = uP_spin; // integrated clock: the disc turns
// the galaxy frame: tip about x by the tilt, then turn about the disc's
// own normal
float ct = cos(uP_tilt);
float st = sin(uP_tilt);
float cs = cos(spin);
float sn = sin(spin);
vec3 acc = vec3(0.0);
vec3 T = vec3(1.0);
// extinction weighted to blue, so thick gas reddens what is behind it
vec3 absorb = vec3(0.7, 1.0, 1.5) * uP_absorb;
// march only the span the envelope can light
float z = max(uP_camDist - uP_envRadius * 1.05, 0.0);
float zEnd = uP_camDist + uP_envRadius * 1.05;
float dt = (zEnd - z) / float(STEPS);
// a hashed start offset per pixel hides the step banding
z += dt * hash(fragCoord * 0.37);
for (int i = 0; i < STEPS; i++) {
vec3 p = ro + rd * z;
// envelope: nothing outside the ball contributes
float env = 1.0 - smoothstep(uP_envRadius * 0.92, uP_envRadius, length(p));
if (env > 0.001) {
// into the galaxy frame
vec3 g = vec3(p.x, p.y * ct - p.z * st, p.y * st + p.z * ct);
g = vec3(g.x * cs - g.z * sn, g.y, g.x * sn + g.z * cs);
float arm = 0.0;
float rho = 0.0;
float d = galaxy(g / uP_envRadius, t, arm, rho) * env;
// the colour ramp, keyed to radius from the core
vec3 ramp = mix(uC_inner, uC_outer, smoothstep(0.12, uP_hueReach, rho));
float coreW = exp(-rho * rho * uP_bulge * 0.6);
vec3 emit = mix(ramp, uC_core, coreW) * (0.6 + 0.6 * arm);
acc += T * d * emit * dt;
T *= exp(-d * absorb * dt);
}
z += dt;
if (T.g < 0.004 || z > zEnd) break;
}
return vec4(acc, 1.0 - T.g);
}
void main() {
// The beat: one wave on the core-beat clock, shared by the core flare and
// the disc's breathing. Both depths are amplitudes, so they stage cleanly.
float wave = 0.5 + 0.5 * cos(uP_beat);
galDensity = uP_density * (1.0 + 0.35 * uInput);
galCore = uP_core * (1.0 + 0.7 * uOutput) * (1.0 + uP_pulse * wave);
// breathing: the disc's falloff relaxes on the wave, so the whole disc
// swells outward and draws back — a smooth exponential, safe to sweep
galFalloff = uP_falloff / (1.0 + uP_breathe * wave);
vec4 acc = galaxyRender(gl_FragCoord.xy);
/*
Stars, at the ray's exact hit with the galactic plane. The plane is the
tilted xz-plane through the origin; its world normal is the tilted y.
The lattice lives in the disc's own turning frame, so the stars turn
with the gas, and the march's transmittance dims them through the dust.
*/
{
vec3 ro = vec3(0.0, 0.0, uP_camDist);
vec3 rd = normalize(vec3(orbUV(), -uP_focal));
float ct = cos(uP_tilt);
float st = sin(uP_tilt);
vec3 N = vec3(0.0, ct, st);
float denom = dot(N, rd);
if (abs(denom) > 1e-4) {
float th = -dot(N, ro) / denom;
vec3 q = ro + rd * th;
if (th > 0.0 && dot(q, q) < uP_envRadius * uP_envRadius * 0.9) {
vec3 g = vec3(q.x, q.y * ct - q.z * st, q.y * st + q.z * ct) / uP_envRadius;
float cs = cos(uP_spin);
float sn = sin(uP_spin);
vec2 gp = vec2(g.x * cs - g.z * sn, g.x * sn + g.z * cs);
float rho = length(gp);
float phi = rho > 1e-4 ? atan(gp.y, gp.x) : 0.0;
float armW = 0.5 + 0.5 * cos(phi * uP_arms - uP_wind * log(max(rho, 0.02)));
float sf = starField(gp * uP_starScale, uP_starDensity * (0.3 + 0.7 * armW), 0.12, uP_twinkle);
float veil = 1.0 - acc.a; // what the march let through
acc.rgb += vec3(1.0, 0.97, 0.9) * sf * uP_stars * exp(-rho * 1.5) * (0.25 + 0.75 * veil);
}
}
}
// tanh tone map per channel, tunable knee
vec3 col = tanh3(acc.rgb / max(uP_exposure, 0.01));
col = pow(clamp(col, 0.0, 1.0), vec3(uP_contrast));
// saturation about luminance, then the tint
float lum = dot(col, vec3(0.299, 0.587, 0.114));
col = mix(vec3(lum), col, uP_saturation);
col *= uC_tint;
// alpha from the brightest channel — emitted light (see shdr-18)
float peak = max(col.r, max(col.g, col.b));
float a = clamp(peak * uP_alphaGain, 0.0, 1.0);
// the night behind: a fill so the ball is a solid sphere, not a cut-out
col += uC_deep * uP_fill;
a = max(a, uP_fill);
// Analytic silhouette — identical construction to shdr-01: exact
// ray-to-centre distance against the radius, colour AND alpha.
vec3 mrd = normalize(vec3(orbUV(), -uP_focal));
float closest = length(cross(vec3(0.0, 0.0, uP_camDist), mrd));
float band = mix(0.35, 0.012, clamp(uP_edge, 0.0, 1.0));
float mask = 1.0 - smoothstep(uP_envRadius * (1.0 - band), uP_envRadius * 1.005, closest);
col *= mask;
a *= mask;
// a fresnel rim on the glass, inside the mask
float fres = smoothstep(uP_envRadius * 0.7, uP_envRadius, closest);
col += uC_rim * uP_rim * fres * fres * mask;
// safety taper at the frame boundary — colour as well as alpha
float r2d = length(orbUV());
float fade = 1.0 - smoothstep(uP_edgeFade, 1.0, r2d);
col *= fade;
a *= fade;
// Emitted light, so rgb is already premultiplied — do NOT scale by alpha
// again (see the same note in shdr-31).
gl_FragColor = vec4(col, a);
}
`;
export const shdr32Orb: OrbVariant = {
key: "shdr-32",
label: "SHDR-32",
note: "a galaxy marched as gas and dust inside the ball",
frag: GALAXY_FRAG,
params: [
{ key: "spin", label: "Disc turn", min: 0, max: 3, step: 0.01, default: 0.06, integrate: true },
{ key: "churn", label: "Gas churn", min: 0, max: 5, step: 0.02, default: 0.25, integrate: true },
{ key: "beat", label: "Core beat", min: 0, max: 12, step: 0.05, default: 0.8, integrate: true },
{ key: "twinkle", label: "Twinkle rate", min: 0, max: 12, step: 0.05, default: 1.2, integrate: true },
{ key: "camDist", label: "Camera distance", min: 1, max: 50, step: 0.3, default: 7 },
{ key: "focal", label: "Lens", min: 0.15, max: 15, step: 0.05, default: 2.25 },
{ key: "envRadius", label: "Envelope radius", min: 0.15, max: 15, step: 0.1, default: 2.6 },
{ key: "tilt", label: "Tilt (0 edge-on)", min: 0, max: 1.5, step: 0.01, default: 0.85 },
{ key: "arms", label: "Arm count", min: 1, max: 6, step: 1, default: 2 },
{ key: "wind", label: "Arm winding", min: 0, max: 8, step: 0.05, default: 3.4 },
{ key: "ragged", label: "Arm fray", min: 0, max: 12, step: 0.05, default: 3 },
{ key: "armSharp", label: "Arm sharpness", min: 0.3, max: 8, step: 0.05, default: 2.2 },
{ key: "falloff", label: "Disc falloff", min: 0.3, max: 12, step: 0.05, default: 1.7 },
{ key: "thick", label: "Disc thickness", min: 0.01, max: 1, step: 0.005, default: 0.035 },
{ key: "bulge", label: "Core tightness", min: 2, max: 200, step: 1, default: 40 },
{ key: "core", label: "Core density", min: 0, max: 20, step: 0.1, default: 5 },
{ key: "turbScale", label: "Turbulence scale", min: 0.5, max: 30, step: 0.1, default: 9 },
{ key: "threshold", label: "Clumping", min: 0, max: 1, step: 0.01, default: 0.35 },
{ key: "density", label: "Gas density", min: 0.1, max: 40, step: 0.1, default: 14 },
{ key: "absorb", label: "Dust absorption", min: 0, max: 20, step: 0.1, default: 3.5 },
{ key: "stars", label: "Stars", min: 0, max: 10, step: 0.05, default: 1.2 },
{ key: "starDensity", label: "Star density", min: 0, max: 1, step: 0.01, default: 0.5 },
{ key: "starScale", label: "Star scale", min: 5, max: 200, step: 1, default: 48 },
{ key: "hueReach", label: "Hue reach", min: 0.15, max: 1.5, step: 0.01, default: 0.6 },
{ key: "pulse", label: "Beat depth", min: 0, max: 3, step: 0.01, default: 0.2 },
{ key: "breathe", label: "Disc breathing", min: 0, max: 2, step: 0.01, default: 0 },
{ key: "exposure", label: "Exposure", min: 0.05, max: 50, step: 0.05, default: 1.1 },
{ key: "contrast", label: "Contrast", min: 0.15, max: 6, step: 0.05, default: 1.15 },
{ key: "saturation", label: "Saturation", min: 0, max: 4, step: 0.02, default: 1.35 },
{ key: "alphaGain", label: "Alpha gain", min: 0.05, max: 15, step: 0.1, default: 2 },
{ key: "fill", label: "Night fill", min: 0, max: 1, step: 0.01, default: 0.85 },
{ key: "rim", label: "Rim light", min: 0, max: 3, step: 0.015, default: 0.35 },
{ key: "edge", label: "Edge sharpness", min: 0, max: 1, step: 0.01, default: 1 },
{ key: "edgeFade", label: "Halo falloff", min: 0.1, max: 3, step: 0.015, default: 0.98 }
],
/*
* Six stops: the overall tint, the core the bulge whitens toward, the
* inner and outer arm colours the ramp runs between, the night the ball
* is filled with, and the glass rim.
*/
colors: [
{ key: "tint", label: "Tint", default: "#ffffff" },
{ key: "core", label: "Core", default: "#fff3d6" },
{ key: "inner", label: "Inner arms", default: "#7fb4ff" },
{ key: "outer", label: "Outer arms", default: "#c46bff" },
{ key: "deep", label: "Night", default: "#04050f" },
{ key: "rim", label: "Rim", default: "#8fb0ff" }
],
/*
Staged on the TILT first — each state is a different view of the disc —
and then on the clocks and amplitudes. The tilt glides, and a tipping
disc is the biggest, most legible motion this orb has, so the state
change itself is the tell. The arm count and winding, the turbulence
scale and the star scale all multiply a coordinate and are pinned.
*/
statePresets: {
/*
at rest: the spiral seen about halfway between edge-on and face-on.
A slow turn, the gas barely boiling, a lazy shallow beat on the core.
*/
idle: {
tilt: 0.85,
spin: 0.06,
churn: 0.25,
beat: 0.8,
twinkle: 1.2,
pulse: 0.2,
breathe: 0,
ragged: 3,
armSharp: 2.2,
thick: 0.035,
threshold: 0.35,
density: 14,
core: 5,
absorb: 3.5,
stars: 1.2,
exposure: 1.1,
contrast: 1.15,
saturation: 1.35
},
/*
searching: the disc swings FACE-ON and becomes a whirlpool. The arms
fray to nothing and the gas boils at six times rest on a thicker
disc, a sparser clumping and a heavier absorption, so what is left is
filaments and shadow spinning at seven times rest — face-on, the turn
is fully visible — with the core held down. Cold.
*/
thinking: {
tilt: 1.45,
spin: 0.45,
churn: 1.6,
beat: 2.4,
twinkle: 4.5,
pulse: 0.25,
breathe: 0,
ragged: 8,
armSharp: 1,
thick: 0.07,
threshold: 0.5,
density: 20,
core: 3,
absorb: 6,
stars: 1.8,
exposure: 1.05,
contrast: 1.3,
saturation: 1.2
},
/*
answering: the disc swings FACE-ON and lights up — the full spiral,
arms sharp and wide, the core flaring on a hard beat (depth five
times rest on a clock six times as fast) and the whole disc swelling
outward and drawing back on the same wave. The gas is dense but the
dust is cleared, so all of it glows, at a lower knee. Hot.
*/
speaking: {
tilt: 1.3,
spin: 0.2,
churn: 0.6,
beat: 4.8,
twinkle: 2.4,
pulse: 1,
breathe: 0.45,
ragged: 2,
armSharp: 1.8,
thick: 0.04,
threshold: 0.25,
density: 18,
core: 12,
absorb: 1.6,
stars: 2,
exposure: 0.75,
contrast: 1.05,
saturation: 1.6
}
},
// blue into violet at rest, ice into cyan while searching, gold into rose
// while answering
stateColors: {
idle: { tint: "#ffffff", core: "#fff3d6", inner: "#7fb4ff", outer: "#c46bff", deep: "#04050f", rim: "#8fb0ff" },
thinking: { tint: "#ffffff", core: "#e6f0ff", inner: "#6fb0ff", outer: "#4fe3ff", deep: "#030614", rim: "#7fa8ff" },
speaking: { tint: "#ffffff", core: "#fff4c8", inner: "#ffa63c", outer: "#ff3f8e", deep: "#0a0508", rim: "#ffb98a" }
}
};
export type Shdr32Props = Omit<ShaderOrbProps, "variant">;
export function Shdr32({ size = 280, ...rest }: Shdr32Props) {
return <ShaderOrb variant={shdr32Orb} size={size} {...rest} />;
}
export default Shdr32;
35. components/ui/shdr-33.tsx
/*
* Deliberately not a `"use client"` module — see the note in `shdr-11.tsx`.
*
* Note for editors: the shader lives in a template literal, so its comments
* must not contain backticks.
*/
import { ShaderOrb, type OrbVariant, type ShaderOrbProps } from "@/components/ui/orbkit-core";
/* ----------------------------------------------------------------------------
SHDR-33 — a thermal image, risograph-printed, wrapped on the ball.
The reference is a heat map put through a cheap colour print: blocky
sources glowing white-hot inside nested rectangular contours that cool
from yellow through orange and violet to black, all of it rendered as a
coarse square-dot halftone whose three ink screens do not quite line up,
on grainy paper. Three stages reproduce it:
- THE HEAT. A noise field, not placed sources: three octaves of value
noise, domain-warped by a coarser noise so the pools bend and pinch,
with a threshold window cut out of it — everything below the window
is the cold field, everything above is white-hot, and the window in
between is where the ramp lives. Value noise at low frequency sits on
a lattice, which is what gives the pools their blocky, rectangular
lean without anything being drawn as a rectangle, and the warp is
what keeps them from looking like a grid. The field DRIFTS under the
sample point, and the warp drifts at a different rate, so the pools
travel, merge and split rather than sit. The field is then QUANTIZED
into bands and blended back with the smooth field, which draws the
nested contours: a thermal camera's palette is a lookup with visible
steps, and the steps are the contours.
- THE PALETTE. Five stops climb the heat — near-black, violet-blue,
red-orange, yellow, white — the classic false-colour thermal ramp.
- THE PRINT. The palette colour is separated into cyan, magenta and
yellow ink coverage (1 - channel), and each ink is laid down as a
screen of round dots whose size carries the coverage, mixed in lightly
over the smooth palette so it reads as texture rather than a grid. The three screens sit at
slightly different angles and offsets, so where two overlap you get
the violet (cyan over magenta) and the orange (magenta over yellow) of
the reference, and the misregistration gives the moire that makes it
look printed rather than rendered. Paper white shows through where the
dots are small, which is why the hot cores come out pale.
- GRAIN. Two taps of animated noise as in shdr-17: one folded into the
heat before quantizing, so the band edges dither; one over the final
print, as paper.
The plane is sampled through a stereographic wrap of a rotating dome, so
the print compresses toward the limb and rolls around the ball rather
than sitting flat on a disc. Surface-lit and mask-bounded, so alpha IS
coverage — premultiplied output, as in shdr-14.
---------------------------------------------------------------------------- */
const HEAT_FRAG = `
const float PI = 3.14159265359;
// Volume-reactive values, resolved once per fragment in main().
float heatGainNow;
float heatJitterNow;
float grainNoise(vec2 gpix, float frame, float seed) {
return hash(gpix + vec2(frame * 13.71 + seed, frame * 7.37 - seed));
}
mat2 rot2(float a) {
float c = cos(a);
float s = sin(a);
return mat2(c, -s, s, c);
}
/*
One ink screen. Square dots on a grid at angle a, offset o (the
misregistration), sized by the coverage: coverage 0 is paper, coverage 1
is a solid. Returns how much of this pixel the ink covers.
The grid is laid in SCREEN space, not on the wrapped plane: a print is
flat, and it is the picture that curves round the ball. A screen on the
wrapped coordinates changes pitch toward the limb and beats against the
other two into moire rings.
*/
float screen(vec2 uv, float a, vec2 o, float coverage, float soft) {
vec2 cell = rot2(a) * uv * uP_dots + o;
vec2 f = fract(cell) - 0.5;
float d = length(f); // a round dot: reads as tone, not as a grid
// dot half-size from coverage; sqrt so mid-tones read as mid-tones the
// way a real screen's area does
float size = 0.5 * sqrt(clamp(coverage * uP_dotGain, 0.0, 1.0));
return 1.0 - smoothstep(size - soft, size + soft, d);
}
void main() {
heatGainNow = uP_gain * (1.0 + 0.6 * uOutput);
heatJitterNow = uP_jitter * (1.0 + 1.5 * uInput);
vec2 uv = orbUV();
float rd = length(uv);
float R = uP_radius;
float mask = smoothstep(0.012, -0.012, rd - R);
if (mask <= 0.0) {
gl_FragColor = vec4(0.0);
return;
}
vec2 pl = uv / R;
float r2 = dot(pl, pl);
float z = sqrt(max(1.0 - r2, 0.0));
vec3 n = vec3(pl, z);
// roll the dome about Y on its own integrated clock
float cr = cos(uP_spin);
float sr = sin(uP_spin);
vec3 sp = vec3(n.x * cr - n.z * sr, n.y, n.x * sr + n.z * cr);
float t = uP_speed; // integrated clock: the sources drift
// stereographic wrap of the plane onto the ball
vec2 st = sp.xy / (1.3 + sp.z) * uP_scale;
/*
The heat: a drifting, domain-warped noise field with a threshold window
cut out of it. Two drifts at different rates so the pools travel and
change shape rather than slide as one sheet; the input jitter is a fast
wobble on top.
*/
vec2 p = st * uP_freq + vec2(t * 0.11, -t * 0.07);
vec2 wp = st * uP_freq * 0.55 + vec2(-t * 0.05, t * 0.08);
vec2 warp = vec2(noise(wp + 3.1), noise(wp + 9.4)) - 0.5;
p += warp * uP_warp;
p += vec2(sin(t * 3.7), cos(t * 4.3)) * heatJitterNow;
float field = noise(p) * 0.62 + noise(p * 2.1 + 5.3) * 0.26 + noise(p * 4.2 + 1.7) * 0.12;
float heat = clamp((field - uP_lo) * heatGainNow / max(uP_hi - uP_lo, 0.01), 0.0, 1.0);
// grain tap 1: dither the field before it is banded, so the contour
// edges break up into speckle instead of clean steps
vec2 gpix = floor(gl_FragCoord.xy / max(uP_grainSize, 1.0));
float frame = floor(uTime * 48.0);
heat += (grainNoise(gpix, frame, 3.1) - 0.5) * uP_dither;
// the contours: quantize into bands, blend back with the smooth field
float banded = floor(heat * uP_bands + 0.5) / uP_bands;
heat = clamp(mix(heat, banded, uP_banding), 0.0, 1.0);
heat = pow(heat, uP_contrast);
// the thermal ramp
vec3 base = mix(uC_cold, uC_cool, smoothstep(0.0, 0.3, heat));
base = mix(base, uC_warm, smoothstep(0.3, 0.55, heat));
base = mix(base, uC_hot, smoothstep(0.55, 0.78, heat));
base = mix(base, uC_core, smoothstep(0.78, 0.97, heat));
/*
The print. Separate the palette into CMY coverage and lay each ink down
as its own screen; the paper shows through the gaps. The angles are the
classic offsets, scaled by the misregistration, plus a per-ink shift.
*/
float soft = uP_dotSoft;
float mis = uP_misregister;
float cC = screen(uv, 0.035 * mis, vec2(0.22, 0.12) * mis, 1.0 - base.r, soft);
float cM = screen(uv, -0.03 * mis, vec2(-0.14, 0.2) * mis, 1.0 - base.g, soft);
float cY = screen(uv, 0.0, vec2(0.0), 1.0 - base.b, soft);
vec3 print = uC_paper;
print *= mix(vec3(1.0), vec3(0.05, 0.62, 0.92), cC * uP_ink);
print *= mix(vec3(1.0), vec3(0.92, 0.08, 0.48), cM * uP_ink);
print *= mix(vec3(1.0), vec3(0.98, 0.86, 0.02), cY * uP_ink);
// the unprinted palette is mixed back a little so the blacks stay black
// and the screens never wash the whole ball to paper
vec3 col = mix(base, print, uP_printMix);
// grain tap 2: paper
col *= 1.0 + (grainNoise(gpix, frame, 27.9) - 0.5) * uP_grain;
// dome shading keeps the ball a ball under the print
float lambert = clamp(dot(n, normalize(vec3(-0.45, 0.55, 0.7))), 0.0, 1.0);
col *= 1.0 - uP_light * (1.0 - lambert);
float fres = pow(1.0 - z, 2.5);
col += uC_paper * uP_rim * fres * 0.5;
// Surface orb bounded by a mask: alpha IS coverage, so premultiply — the
// opposite convention from the emissive orbs (see shdr-31).
float a = mask;
gl_FragColor = vec4(max(col, vec3(0.0)) * a, a);
}
`;
// the rest look: warm pools drifting slowly, contours soft
const HEAT_REST = {
speed: 0.5,
spin: 0.05,
gain: 1,
warp: 0.6,
lo: 0.42,
hi: 0.74,
jitter: 0.015,
banding: 0.85,
grain: 0.35,
contrast: 1
};
const HEAT_PALETTE = {
cold: "#0b0a1e",
cool: "#3b2a9a",
warm: "#f05a28",
hot: "#f6b53a",
core: "#fff1e6"
};
export const shdr33Orb: OrbVariant = {
key: "shdr-33",
label: "SHDR-33",
note: "a thermal image, risograph-printed on the ball",
frag: HEAT_FRAG,
params: [
{ key: "speed", label: "Drift", min: 0.015, max: 10, step: 0.05, default: 0.5, integrate: true },
{ key: "spin", label: "Roll", min: 0, max: 5, step: 0.03, default: 0.05, integrate: true },
{ key: "radius", label: "Radius", min: 0.15, max: 3, step: 0.015, default: 0.9 },
{ key: "scale", label: "Zoom", min: 0.3, max: 8, step: 0.05, default: 3 },
{ key: "freq", label: "Pool scale", min: 0.2, max: 6, step: 0.05, default: 1.4 },
{ key: "warp", label: "Warp", min: 0, max: 3, step: 0.02, default: 0.6 },
{ key: "lo", label: "Cold threshold", min: 0, max: 1, step: 0.005, default: 0.42 },
{ key: "hi", label: "Hot threshold", min: 0, max: 1, step: 0.005, default: 0.74 },
{ key: "gain", label: "Heat gain", min: 0.1, max: 6, step: 0.02, default: 1 },
{ key: "jitter", label: "Heat jitter", min: 0, max: 0.5, step: 0.005, default: 0.015 },
{ key: "bands", label: "Contour bands", min: 2, max: 24, step: 1, default: 7 },
{ key: "banding", label: "Contour strength", min: 0, max: 1, step: 0.01, default: 0.85 },
{ key: "contrast", label: "Contrast", min: 0.3, max: 3, step: 0.02, default: 1 },
{ key: "dither", label: "Dither", min: 0, max: 0.6, step: 0.005, default: 0.05 },
{ key: "dots", label: "Screen pitch", min: 4, max: 120, step: 1, default: 46 },
{ key: "dotGain", label: "Dot gain", min: 0.2, max: 2, step: 0.01, default: 1 },
{ key: "dotSoft", label: "Dot softness", min: 0.01, max: 0.3, step: 0.005, default: 0.12 },
{ key: "misregister", label: "Misregistration", min: 0, max: 3, step: 0.02, default: 0.5 },
{ key: "ink", label: "Ink density", min: 0, max: 1, step: 0.01, default: 0.92 },
{ key: "printMix", label: "Print mix", min: 0, max: 1, step: 0.01, default: 0.28 },
{ key: "grain", label: "Paper grain", min: 0, max: 2, step: 0.01, default: 0.35 },
{ key: "grainSize", label: "Grain size", min: 1, max: 8, step: 1, default: 2 },
{ key: "light", label: "Key light", min: 0, max: 1, step: 0.01, default: 0.25 },
{ key: "rim", label: "Rim light", min: 0, max: 3, step: 0.015, default: 0.25 }
],
/*
* Six stops: five up the thermal ramp, and the paper the screens are
* printed on.
*/
colors: [
{ key: "cold", label: "Cold", default: "#0b0a1e" },
{ key: "cool", label: "Cool", default: "#3b2a9a" },
{ key: "warm", label: "Warm", default: "#f05a28" },
{ key: "hot", label: "Hot", default: "#f6b53a" },
{ key: "core", label: "Core", default: "#fff1e6" },
{ key: "paper", label: "Paper", default: "#f4ecdf" }
],
/*
All three states share the rest palette; idle is the rest preset.
Speaking is the rest look set RACING in place — the drift at eighteen
times rest on a roll forty times as fast, the pools slightly finer and
the warp more than doubled, with the window dropped so more of it
reads as hot — without the rescale thinking makes. Thinking is the
rest look zoomed out and set racing: the plane at four times the
zoom with the pools three times finer and the warp tripled, the drift
at twenty times rest on a roll ten times as fast, the window dropped
so more of it reads as hot, on fewer, softer bands and a finer screen.
Note the zoom, the pool scale, the band count and the screen pitch all
multiply a coordinate, so the transition into and out of thinking
glides through a rescale — chosen deliberately.
*/
statePresets: {
idle: HEAT_REST,
thinking: {
...HEAT_REST,
speed: 10,
spin: 0.51,
scale: 4.6,
freq: 3.1,
warp: 1.82,
lo: 0.3,
hi: 0.66,
jitter: 0,
bands: 6,
banding: 0.75,
dots: 42
},
speaking: {
...HEAT_REST,
speed: 8.8,
spin: 2.01,
freq: 1.3,
warp: 1.42,
lo: 0.345,
hi: 0.72,
jitter: 0
}
},
stateColors: {
idle: HEAT_PALETTE,
thinking: HEAT_PALETTE,
speaking: HEAT_PALETTE
}
};
export type Shdr33Props = Omit<ShaderOrbProps, "variant">;
export function Shdr33({ size = 280, ...rest }: Shdr33Props) {
return <ShaderOrb variant={shdr33Orb} size={size} {...rest} />;
}
export default Shdr33;
36. Use it
import { Shdr01 } from "@/components/ui/shdr-01";
<Shdr01 size={280} state="idle" />