All loaders

WebGL · 5.7 KB gzip

Plasma Field

A full-bleed layered-sine plasma for banners and backgrounds, with theme and state controls.

plasmabackgroundfieldgenerativewebgl
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/plasma-field';

<orbux-plasma-field state="thinking" size="160px"></orbux-plasma-field>

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

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

<orbux-plasma-field state="thinking" size="160px"></orbux-plasma-field>

Component source plasma-field.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;

void main(){
  vec2 p=(gl_FragCoord.xy*2.0-uRes)/min(uRes.x,uRes.y);
  float t=uTime;
  float v=0.0;
  v+=sin(p.x*3.0 + t);
  v+=sin(p.y*3.5 - t*0.8);
  v+=sin((p.x+p.y)*2.5 + t*1.2);
  float cx=p.x + 0.5*sin(t*0.4);
  float cy=p.y + 0.5*cos(t*0.5);
  v+=sin(sqrt(cx*cx+cy*cy)*4.0 + t)*(1.0+uActivity);
  v/=4.0;
  float m=0.5+0.5*sin(v*3.14159265 + t*0.2);
  vec3 col=mix(uColorA, uColorB, m);
  col+=0.12*uActivity*sin(v*10.0);
  float vig=smoothstep(1.5,0.25,length(p));
  if(uState>3.5&&uState<4.5) vig*=smoothstep(0.16,0.3,length(p));
  if(uState>2.5&&uState<3.5) vig*=0.78+0.22*step(0.3,abs(sin(atan(p.y,p.x)*4.0)));
  gl_FragColor=vec4(col, vig);
}
`;

type RGB = [number, number, number];
interface Visual {
  act: number;
  a: RGB;
  b: RGB;
}

const STATE_VISUALS: Record<AgentState, Visual> = {
  idle: { act: 0.12, a: [0.12, 0.16, 0.34], b: [0.24, 0.2, 0.44] },
  thinking: { act: 0.5, a: [0.3, 0.28, 0.72], b: [0.55, 0.3, 0.85] },
  streaming: { act: 0.95, a: [0.16, 0.42, 0.85], b: [0.3, 0.82, 0.92] },
  'tool-calling': { act: 0.72, a: [0.08, 0.55, 0.55], b: [0.35, 0.82, 0.45] },
  waiting: { act: 0.2, a: [0.22, 0.26, 0.44], b: [0.32, 0.38, 0.6] },
  done: { act: 0.28, a: [0.1, 0.5, 0.32], b: [0.36, 0.82, 0.5] },
  error: { act: 0.55, a: [0.6, 0.12, 0.2], b: [0.92, 0.42, 0.24] },
};

/**
 * Plasma Field: a full-bleed layered-sine plasma, colored by theme and
 * energized by state. Great as a "generating" banner or background.
 */
export class OrbuxPlasmaField extends ShaderElement {
  #act = 0.5;
  #a: RGB = [0.3, 0.28, 0.72];
  #b: RGB = [0.55, 0.3, 0.85];

  protected fragmentSource(): string {
    return FRAG;
  }
  protected override timeScale(): number {
    return 0.4 + this.#act;
  }
  protected override fallbackBackground(): string {
    return 'border-radius:12px;background:linear-gradient(120deg,var(--orbux-color,#6366f1),var(--orbux-color-2,#a855f7));';
  }

  protected paint(gl: WebGLRenderingContext, info: OrbuxFrameInfo): void {
    const v = STATE_VISUALS[info.state];
    let a = v.a;
    let b = v.b;
    if (info.state === 'done') {
      a = this.cssColorRGB('--orbux-color-success', v.a);
      b = v.b;
    } else if (info.state === 'error') {
      a = this.cssColorRGB('--orbux-color-error', v.a);
      b = v.b;
    } else {
      a = this.cssColorRGB('--orbux-color', v.a);
      b = 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.act, k);
    for (let i = 0; i < 3; i++) {
      this.#a[i] = lerp(this.#a[i] ?? 0, a[i] ?? 0, k);
      this.#b[i] = lerp(this.#b[i] ?? 0, b[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-plasma-field')) {
  customElements.define('orbux-plasma-field', OrbuxPlasmaField);
}

declare global {
  interface HTMLElementTagNameMap {
    'orbux-plasma-field': OrbuxPlasmaField;
  }
}

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
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.