All loaders

WebGL · 6.0 KB gzip

Liquid Metal

A reflective, chrome-like orb with sharp specular streaks that ripple with activity; supports a progress ring.

orbmetalchromeliquidwebglpremium
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/liquid-metal';

<orbux-liquid-metal state="thinking" size="160px"></orbux-liquid-metal>

Browser-only alternative: Download HTML (self-contained) or load the hosted ESM bundle:

<script type="module" src="http://localhost:4321/orbux/liquid-metal.js"></script>

<orbux-liquid-metal state="thinking" size="160px"></orbux-liquid-metal>

Component source liquid-metal.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;   // highlight tint
uniform vec3  uColorB;   // dark base metal
uniform float uActivity;
uniform float uState;
uniform float uProgress;
uniform vec3  uProgressColor;

float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453); }
float noise(vec2 p){
  vec2 i=floor(p), f=fract(p);
  float a=hash(i), b=hash(i+vec2(1,0)), c=hash(i+vec2(0,1)), d=hash(i+vec2(1,1));
  vec2 u=f*f*(3.0-2.0*f);
  return mix(a,b,u.x)+(c-a)*u.y*(1.0-u.x)+(d-b)*u.x*u.y;
}
float fbm(vec2 p){ float v=0.0,a=0.5; for(int i=0;i<5;i++){ v+=a*noise(p); p*=2.0; a*=0.5;} return v; }
mat2 rot(float a){ float c=cos(a),s=sin(a); return mat2(c,-s,s,c); }

void main(){
  vec2 uv=(gl_FragCoord.xy*2.0-uRes)/min(uRes.x,uRes.y);
  float r=length(uv); float t=uTime;
  vec2 q=uv*rot(t*0.1);
  float n=fbm(q*3.0 + fbm(q*2.0 + t*0.25)*(1.0+uActivity));
  // metallic banding -> sharp specular streaks
  float bands=sin(n*16.0 + t*2.0);
  float hi=pow(0.5+0.5*bands, 3.0);
  vec3 col=mix(uColorB, uColorA, hi);
  float rim=smoothstep(0.86,0.98,r);
  col=mix(col, uColorA, rim*0.5);
  float body=smoothstep(0.92,0.6,r);
  float glow=smoothstep(1.2,0.0,r)*0.28*uActivity;

  vec3 outCol=col*body + uColorA*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(atan(uv.y,uv.x)*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.96));
    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];
const BASE: RGB = [0.05, 0.06, 0.09];

const STATE_HI: Record<AgentState, { act: number; hi: RGB }> = {
  idle: { act: 0.15, hi: [0.4, 0.45, 0.7] },
  thinking: { act: 0.5, hi: [0.52, 0.52, 0.96] },
  streaming: { act: 0.95, hi: [0.42, 0.82, 1.0] },
  'tool-calling': { act: 0.72, hi: [0.32, 0.92, 0.82] },
  waiting: { act: 0.22, hi: [0.46, 0.5, 0.72] },
  done: { act: 0.3, hi: [0.32, 0.92, 0.52] },
  error: { act: 0.55, hi: [1.0, 0.36, 0.36] },
};

/** Liquid Metal: a reflective, chrome-like orb with sharp specular streaks. */
export class OrbuxLiquidMetal extends ShaderElement {
  #act = 0.5;
  #hi: RGB = [0.52, 0.52, 0.96];

  protected fragmentSource(): string {
    return FRAG;
  }
  protected override timeScale(): number {
    return 0.4 + this.#act;
  }
  protected override fallbackBackground(): string {
    return 'background:radial-gradient(circle at 38% 32%,color-mix(in srgb,var(--orbux-color,#818cf8) 45%,white),#0b0d14 72%);';
  }

  protected paint(gl: WebGLRenderingContext, info: OrbuxFrameInfo): void {
    const v = STATE_HI[info.state];
    let hi = v.hi;
    if (info.state === 'done') {
      hi = this.cssColorRGB('--orbux-color-success', v.hi);
    } else if (info.state === 'error') {
      hi = this.cssColorRGB('--orbux-color-error', v.hi);
    } else {
      hi = this.cssColorRGB('--orbux-color', v.hi);
    }
    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.act, k);
    for (let i = 0; i < 3; i++) this.#hi[i] = lerp(this.#hi[i] ?? 0, hi[i] ?? 0, k);
    gl.uniform3fv(this.uniform('uColorA'), this.#hi);
    gl.uniform3fv(this.uniform('uColorB'), BASE);
    gl.uniform1f(this.uniform('uActivity'), this.#act);
  }
}

if (typeof customElements !== 'undefined' && !customElements.get('orbux-liquid-metal')) {
  customElements.define('orbux-liquid-metal', OrbuxLiquidMetal);
}

declare global {
  interface HTMLElementTagNameMap {
    'orbux-liquid-metal': OrbuxLiquidMetal;
  }
}

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.