Force-directed graph visualization for agent teams. Package: @claude-teams/agent-graph (isolated workspace package) - Space theme: bloom, particles, hex grid, depth stars - Members as hexagonal nodes with breathing glow - Tasks as pill cards in kanban columns (todo/wip/done/review/approved) per owner - Message particles along edges (real-time only) - Deterministic layout, Figma-style pan, scroll/pinch zoom - Clean Architecture: ports/adapters/strategies, ES #private classes Integration: features/agent-graph/ (adapter + overlay + tab) - Full-screen overlay (Cmd+Shift+G) + Pin as Tab - Graph button in Team section header - Frustum culling, zero per-frame allocations, adaptive fps - Performance overlay via ?perf query param Also: CI runs on all PR branches, features/CLAUDE.md architecture guide
70 lines
2.1 KiB
TypeScript
70 lines
2.1 KiB
TypeScript
/**
|
|
* Post-processing bloom effect.
|
|
* Adapted from agent-flow's bloom-renderer.ts (Apache 2.0).
|
|
* Zero imports — pure Canvas 2D.
|
|
*/
|
|
|
|
export class BloomRenderer {
|
|
#bloomCanvas: HTMLCanvasElement;
|
|
#bloomCtx: CanvasRenderingContext2D;
|
|
#tempCanvas: HTMLCanvasElement;
|
|
#tempCtx: CanvasRenderingContext2D;
|
|
#intensity: number;
|
|
#w = 0;
|
|
#h = 0;
|
|
|
|
constructor(intensity = 0.6) {
|
|
this.#intensity = intensity;
|
|
this.#bloomCanvas = document.createElement('canvas');
|
|
this.#bloomCtx = this.#bloomCanvas.getContext('2d')!;
|
|
this.#tempCanvas = document.createElement('canvas');
|
|
this.#tempCtx = this.#tempCanvas.getContext('2d')!;
|
|
}
|
|
|
|
resize(w: number, h: number): void {
|
|
const hw = Math.ceil(w / 2);
|
|
const hh = Math.ceil(h / 2);
|
|
if (this.#w === hw && this.#h === hh) return;
|
|
this.#w = hw;
|
|
this.#h = hh;
|
|
this.#bloomCanvas.width = hw;
|
|
this.#bloomCanvas.height = hh;
|
|
this.#tempCanvas.width = hw;
|
|
this.#tempCanvas.height = hh;
|
|
}
|
|
|
|
setIntensity(v: number): void {
|
|
this.#intensity = Math.max(0, Math.min(1, v));
|
|
}
|
|
|
|
apply(source: HTMLCanvasElement, targetCtx: CanvasRenderingContext2D): void {
|
|
if (this.#intensity <= 0 || this.#w === 0) return;
|
|
|
|
this.#bloomCtx.clearRect(0, 0, this.#w, this.#h);
|
|
this.#bloomCtx.drawImage(source, 0, 0, this.#w, this.#h);
|
|
|
|
const radii = [8, 6, 4];
|
|
for (const r of radii) {
|
|
this.#tempCtx.clearRect(0, 0, this.#w, this.#h);
|
|
this.#tempCtx.filter = `blur(${r}px)`;
|
|
this.#tempCtx.drawImage(this.#bloomCanvas, 0, 0);
|
|
this.#tempCtx.filter = 'none';
|
|
|
|
this.#bloomCtx.clearRect(0, 0, this.#w, this.#h);
|
|
this.#bloomCtx.drawImage(this.#tempCanvas, 0, 0);
|
|
}
|
|
|
|
const prevOp = targetCtx.globalCompositeOperation;
|
|
const prevAlpha = targetCtx.globalAlpha;
|
|
targetCtx.globalCompositeOperation = 'lighter';
|
|
targetCtx.globalAlpha = this.#intensity;
|
|
targetCtx.drawImage(this.#bloomCanvas, 0, 0, source.width, source.height);
|
|
targetCtx.globalCompositeOperation = prevOp;
|
|
targetCtx.globalAlpha = prevAlpha;
|
|
}
|
|
|
|
[Symbol.dispose](): void {
|
|
this.#w = 0;
|
|
this.#h = 0;
|
|
}
|
|
}
|