agent-ecosystem/packages/agent-graph/src/canvas/render-cache.ts
Илия 11bb49c53e
feat(graph): force-directed agent graph visualization with kanban-zone task layout
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
2026-03-28 12:03:42 +02:00

140 lines
4.4 KiB
TypeScript

/**
* Pre-rendered sprite cache for Canvas 2D glow effects.
* Adapted from agent-flow (Apache 2.0).
*/
const glowCache = new Map<string, HTMLCanvasElement>();
const textCache = new Map<string, number>();
const TEXT_CACHE_LIMIT = 2000;
// ─── Color resolution: named colors → hex ───────────────────────────────────
let _resolverCtx: CanvasRenderingContext2D | null = null;
const _hexCache = new Map<string, string>();
/**
* Ensure a color string is in #rrggbb hex format.
* Resolves CSS named colors (purple → #800080), shorthand (#abc → #aabbcc).
*/
function ensureHex(color: string): string {
if (color.startsWith('#') && color.length === 7) return color;
let hex = _hexCache.get(color);
if (hex) return hex;
if (color.startsWith('#') && color.length === 4) {
hex = `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}`;
} else {
// Resolve named/rgb/hsl colors via canvas
_resolverCtx ??= document.createElement('canvas').getContext('2d')!;
_resolverCtx.fillStyle = '#000000';
_resolverCtx.fillStyle = color;
hex = _resolverCtx.fillStyle; // always returns #rrggbb
}
_hexCache.set(color, hex);
return hex;
}
/** Build a hex color with alpha: "#rrggbbaa" — cached for repeated calls */
const _hexAlphaCache = new Map<string, string>();
function hexWithAlpha(color: string, alpha: number): string {
// Quantize alpha to 1/255 steps for cache hit rate
const a = Math.round(Math.max(0, Math.min(1, alpha)) * 255);
const key = `${color}|${a}`;
let result = _hexAlphaCache.get(key);
if (result) return result;
result = ensureHex(color) + ALPHA_LUT[a];
_hexAlphaCache.set(key, result);
if (_hexAlphaCache.size > 500) _hexAlphaCache.clear(); // prevent unbounded growth
return result;
}
// Import-time LUT for alpha hex
const ALPHA_LUT: string[] = [];
for (let i = 0; i < 256; i++) ALPHA_LUT.push(i.toString(16).padStart(2, '0'));
// ─── Glow Sprite Cache ──────────────────────────────────────────────────────
/**
* Get or create a pre-rendered radial gradient glow sprite.
*/
export function getGlowSprite(
color: string,
radius: number,
innerAlpha: number,
outerAlpha: number,
): HTMLCanvasElement {
const hex = ensureHex(color);
const key = `${hex}|${radius}|${innerAlpha}|${outerAlpha}`;
let canvas = glowCache.get(key);
if (canvas) return canvas;
const size = Math.ceil(radius * 2);
canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d')!;
const cx = size / 2;
const grad = ctx.createRadialGradient(cx, cx, 0, cx, cx, radius);
grad.addColorStop(0, hexWithAlpha(hex, innerAlpha));
grad.addColorStop(1, hexWithAlpha(hex, outerAlpha));
ctx.fillStyle = grad;
ctx.fillRect(0, 0, size, size);
glowCache.set(key, canvas);
return canvas;
}
/**
* Get or create a pre-rendered agent glow sprite (inner + outer radius).
*/
export function getAgentGlowSprite(
color: string,
innerRadius: number,
outerRadius: number,
): HTMLCanvasElement {
const hex = ensureHex(color);
const key = `agent|${hex}|${innerRadius}|${outerRadius}`;
let canvas = glowCache.get(key);
if (canvas) return canvas;
const size = Math.ceil(outerRadius * 2);
canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d')!;
const cx = size / 2;
const grad = ctx.createRadialGradient(cx, cx, innerRadius, cx, cx, outerRadius);
grad.addColorStop(0, hexWithAlpha(hex, 0.25));
grad.addColorStop(0.5, hexWithAlpha(hex, 0.08));
grad.addColorStop(1, hexWithAlpha(hex, 0));
ctx.fillStyle = grad;
ctx.fillRect(0, 0, size, size);
glowCache.set(key, canvas);
return canvas;
}
/**
* Cached text width measurement.
*/
export function measureTextCached(ctx: CanvasRenderingContext2D, font: string, text: string): number {
const key = `${font}|${text}`;
let w = textCache.get(key);
if (w !== undefined) return w;
if (textCache.size > TEXT_CACHE_LIMIT) textCache.clear();
const prevFont = ctx.font;
ctx.font = font;
w = ctx.measureText(text).width;
ctx.font = prevFont;
textCache.set(key, w);
return w;
}
/** Exported for use by draw functions that need hex+alpha colors */
export { ensureHex, hexWithAlpha };