All loaders

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

Download HTML

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

NameTypeDescription
stateAgentState idle · thinking · streaming · tool-calling · waiting · done · error
progressnumber (0 to 1) Determinate progress from 0 to 1.
sizeCSS lengthOverall square size.
--orbux-widthCSS lengthOptional inline-size override.
--orbux-heightCSS lengthOptional block-size override.
speednumberAnimation speed multiplier (1 = default).
labelstringAccessible label (defaults per state).
pausedbooleanFreeze 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.