WebGL · 5.9 KB gzip
Aurora Orb
A sphere of flowing aurora ribbons. Color and energy shift with state; supports a progress ring.
orbauroragradientherowebglfluid
Interactive preview
Usage
Packages are @vikast908/core and
@vikast908/loaders. Prefer 0.1.1+
(0.1.0 loaders is broken on npm).
npm install @vikast908/loaders@0.1.1 @vikast908/core@0.1.1
import '@vikast908/loaders/aurora-orb'; <orbux-aurora-orb state="thinking" size="160px"></orbux-aurora-orb>
Browser-only alternative: Download HTML (self-contained) or load the hosted ESM bundle:
<script type="module" src="http://localhost:4321/orbux/aurora-orb.js"></script> <orbux-aurora-orb state="thinking" size="160px"></orbux-aurora-orb>
Component source aurora-orb.ts
import { type AgentState, lerp, type OrbuxFrameInfo, ShaderElement } from '@vikast908/core';
const FRAG = /* glsl */ `
precision highp float;
uniform vec2 uRes;
uniform float uTime;
uniform vec3 uColorA;
uniform vec3 uColorB;
uniform float uActivity;
uniform float uState;
uniform float uProgress;
uniform vec3 uProgressColor;
void main() {
vec2 uv = (gl_FragCoord.xy * 2.0 - uRes) / min(uRes.x, uRes.y);
float r = length(uv);
float t = uTime;
// layered aurora ribbons advected over time
float bands = 0.0;
for (int i = 0; i < 4; i++) {
float fi = float(i);
bands += sin(uv.x * (2.5 + fi) + t * (0.5 + fi * 0.35)
+ sin(uv.y * 2.0 - t * 0.4) * (1.0 + uActivity)) * (0.5 / (fi + 1.0));
}
float ang = atan(uv.y, uv.x);
float shade = clamp(0.5 + 0.5 * bands + 0.18 * sin(ang * 3.0 + t), 0.0, 1.0);
vec3 col = mix(uColorA, uColorB, shade);
float body = smoothstep(0.92, 0.6, r);
float glow = smoothstep(1.3, 0.0, r) * (0.3 + 0.45 * uActivity);
float rim = smoothstep(0.92, 0.88, r) - smoothstep(0.88, 0.68, r);
col += rim * 0.4;
vec3 outCol = col * body + mix(uColorA, uColorB, 0.5) * glow;
float alpha = max(body, glow);
if (uState > 3.5 && uState < 4.5) alpha *= smoothstep(0.18, 0.28, r);
if (uState > 2.5 && uState < 3.5) alpha *= 0.78 + 0.22 * step(0.3, abs(sin(ang * 4.0)));
if (uProgress >= 0.0) {
float a01 = mod(atan(uv.x, uv.y) + 6.2831853, 6.2831853) / 6.2831853;
float ring = smoothstep(0.03, 0.0, abs(r - 0.97));
float fill = ring * step(a01, uProgress);
outCol = mix(outCol, uProgressColor, fill * 0.9);
alpha = max(alpha, fill);
}
gl_FragColor = vec4(outCol, alpha);
}
`;
type RGB = [number, number, number];
interface Visual {
activity: number;
a: RGB;
b: RGB;
}
const STATE_VISUALS: Record<AgentState, Visual> = {
idle: { activity: 0.15, a: [0.16, 0.24, 0.44], b: [0.2, 0.5, 0.55] },
thinking: { activity: 0.5, a: [0.36, 0.32, 0.9], b: [0.3, 0.72, 0.86] },
streaming: { activity: 0.95, a: [0.2, 0.72, 0.85], b: [0.5, 0.95, 0.8] },
'tool-calling': { activity: 0.75, a: [0.13, 0.72, 0.5], b: [0.65, 0.9, 0.35] },
waiting: { activity: 0.2, a: [0.28, 0.32, 0.52], b: [0.4, 0.55, 0.68] },
done: { activity: 0.28, a: [0.13, 0.7, 0.42], b: [0.5, 0.92, 0.6] },
error: { activity: 0.55, a: [0.82, 0.2, 0.3], b: [0.95, 0.55, 0.35] },
};
/**
* Aurora Orb: a sphere of flowing aurora ribbons. Color and energy shift with
* state; supports a `progress` ring.
*/
export class OrbuxAuroraOrb extends ShaderElement {
#act = 0.5;
#a: RGB = [0.36, 0.32, 0.9];
#b: RGB = [0.3, 0.72, 0.86];
protected fragmentSource(): string {
return FRAG;
}
protected override timeScale(): number {
return 0.4 + this.#act;
}
protected override fallbackBackground(): string {
return 'background:radial-gradient(circle at 40% 35%,var(--orbux-color-2,#4ade80),var(--orbux-color,#6366f1) 72%);';
}
protected paint(gl: WebGLRenderingContext, info: OrbuxFrameInfo): void {
const v = STATE_VISUALS[info.state];
let ta = v.a;
let tb = v.b;
if (info.state === 'done') {
ta = this.cssColorRGB('--orbux-color-success', v.a);
tb = v.b;
} else if (info.state === 'error') {
ta = this.cssColorRGB('--orbux-color-error', v.a);
tb = v.b;
} else {
ta = this.cssColorRGB('--orbux-color', v.a);
tb = this.cssColorRGB('--orbux-color-2', v.b);
}
const rate = info.state === 'done' || info.state === 'error' ? 18 : 3.5;
const k = info.dt === 0 ? 1 : 1 - Math.exp(-rate * info.dt);
this.#act = lerp(this.#act, v.activity, k);
for (let i = 0; i < 3; i++) {
this.#a[i] = lerp(this.#a[i] ?? 0, ta[i] ?? 0, k);
this.#b[i] = lerp(this.#b[i] ?? 0, tb[i] ?? 0, k);
}
gl.uniform3fv(this.uniform('uColorA'), this.#a);
gl.uniform3fv(this.uniform('uColorB'), this.#b);
gl.uniform1f(this.uniform('uActivity'), this.#act);
}
}
if (typeof customElements !== 'undefined' && !customElements.get('orbux-aurora-orb')) {
customElements.define('orbux-aurora-orb', OrbuxAuroraOrb);
}
declare global {
interface HTMLElementTagNameMap {
'orbux-aurora-orb': OrbuxAuroraOrb;
}
}
Depends on @vikast908/core. Paste the source, add the base
element, and set state from your agent code.
Attributes & properties
| Name | Type | Description |
|---|---|---|
state | AgentState | idle · thinking · streaming · tool-calling · waiting · done · error |
progress | number (0 to 1) | Determinate progress from 0 to 1. |
size | CSS length | Overall square size. |
--orbux-width | CSS length | Optional inline-size override. |
--orbux-height | CSS length | Optional block-size override. |
speed | number | Animation speed multiplier (1 = default). |
label | string | Accessible label (defaults per state). |
paused | boolean | Freeze the animation. |
Respects prefers-reduced-motion, exposes
status/progressbar semantics with a live label, and auto-pauses (sets
data-paused) when offscreen or the tab is hidden.