Canvas 2D · 3.7 KB gzip
Waveform Scope
An oscilloscope trace whose amplitude, frequency and shape reflect state: sine, busy signal, stepped wave, green flatline, or red spikes.
waveformoscilloscopeaudioscopestreaming
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/waveform-scope'; <orbux-waveform-scope state="thinking"></orbux-waveform-scope>
Browser-only alternative: Download HTML (self-contained) or load the hosted ESM bundle:
<script type="module" src="http://localhost:4321/orbux/waveform-scope.js"></script> <orbux-waveform-scope state="thinking"></orbux-waveform-scope>
Component source waveform-scope.ts
import { type AgentState, Canvas2DElement, lerp, noise1D, type OrbuxFrameInfo } from '@vikast908/core';
interface Tune {
amp: number; // amplitude fraction
freq: number; // cycles across width
harm: number; // harmonic richness
square: number; // 0 sine .. 1 square
erratic: number; // random spikes
}
const STATE_TUNE: Record<AgentState, Tune> = {
idle: { amp: 0.06, freq: 2, harm: 0, square: 0, erratic: 0 },
thinking: { amp: 0.4, freq: 2.4, harm: 0.2, square: 0, erratic: 0 },
streaming: { amp: 0.8, freq: 3.6, harm: 0.7, square: 0, erratic: 0 },
'tool-calling': { amp: 0.55, freq: 2.2, harm: 0.1, square: 0.85, erratic: 0 },
waiting: { amp: 0.12, freq: 1.6, harm: 0, square: 0, erratic: 0 },
done: { amp: 0.04, freq: 1, harm: 0, square: 0, erratic: 0 },
error: { amp: 0.7, freq: 3, harm: 0.4, square: 0, erratic: 0.9 },
};
/**
* Waveform Scope: an oscilloscope trace whose amplitude, frequency and shape
* reflect state: a gentle sine while thinking, a busy signal while streaming, a
* stepped wave while calling a tool, a green flatline on `done`, red spikes on
* `error`.
*/
export class OrbuxWaveformScope extends Canvas2DElement {
#cur: Tune = { ...STATE_TUNE.thinking };
protected draw(info: OrbuxFrameInfo): void {
const ctx = this.ctx;
if (!ctx) return;
const t = STATE_TUNE[info.state];
const rate = info.state === 'done' || info.state === 'error' ? 18 : 3;
const k = info.dt === 0 ? 1 : 1 - Math.exp(-rate * info.dt);
this.#cur.amp = lerp(this.#cur.amp, t.amp, k);
this.#cur.freq = lerp(this.#cur.freq, t.freq, k);
this.#cur.harm = lerp(this.#cur.harm, t.harm, k);
this.#cur.square = lerp(this.#cur.square, t.square, k);
this.#cur.erratic = lerp(this.#cur.erratic, t.erratic, k);
const color =
info.state === 'done'
? this.cssColor('--orbux-color-success', '#22c55e')
: info.state === 'error'
? this.cssColor('--orbux-color-error', '#ef4444')
: this.cssColor('--orbux-color', '#6366f1');
const w = this.width;
const h = this.height;
const cy = h / 2;
const time = info.elapsed * info.speed * 1.6;
this.clear();
// center axis
ctx.strokeStyle = color;
ctx.globalAlpha = 0.14;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, cy);
ctx.lineTo(w, cy);
ctx.stroke();
// waveform
const samples = Math.max(28, Math.floor(w / 2.5));
ctx.globalAlpha = 1;
ctx.strokeStyle = color;
ctx.lineWidth = Math.max(1.5, h * 0.03);
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.shadowBlur = h * 0.12;
ctx.shadowColor = color;
ctx.beginPath();
for (let i = 0; i < samples; i++) {
const nx = i / (samples - 1);
const phase = nx * this.#cur.freq * Math.PI * 2 + time * 3;
let v = Math.sin(phase);
v += this.#cur.harm * Math.sin(phase * 2.3 + time * 2);
if (this.#cur.square > 0) v = lerp(v, Math.sign(Math.sin(phase)), this.#cur.square);
if (this.#cur.erratic > 0) {
v += noise1D(i + 1, info.elapsed * 12) * this.#cur.erratic;
}
// window the ends so the trace tapers to the axis
const window = Math.sin(nx * Math.PI);
const y = cy - v * this.#cur.amp * (h * 0.42) * window;
if (i === 0) ctx.moveTo(nx * w, y);
else ctx.lineTo(nx * w, y);
}
ctx.stroke();
ctx.shadowBlur = 0;
}
}
if (typeof customElements !== 'undefined' && !customElements.get('orbux-waveform-scope')) {
customElements.define('orbux-waveform-scope', OrbuxWaveformScope);
}
declare global {
interface HTMLElementTagNameMap {
'orbux-waveform-scope': OrbuxWaveformScope;
}
}
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 |
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.