Files
CapaKraken/apps/web/src/components/analytics/computation-graph/node-renderer.ts
T
Hartmut 093e13b88f feat: project cover art with AI generation, branding rename, RBAC fix, computation graph
- Add DALL-E cover art generation for projects (Azure OpenAI + standard OpenAI)
- CoverArtSection component with generate/upload/remove/focus-point controls
- Client-side image compression (10MB input → WebP/JPEG, max 1920px)
- DALL-E settings in admin panel (deployment, endpoint, API key)
- MCP assistant tools for cover art (generate_project_cover, remove_project_cover)
- Rename "Planarchy" → "plANARCHY" across all UI-facing text (13 files)
- Fix hardcoded canEdit={true} on project detail page — now checks user role
- Computation graph visualization (2D/3D) for calculation rules
- OG image and OpenGraph metadata

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-18 11:31:56 +01:00

121 lines
3.7 KiB
TypeScript

import * as THREE from "three";
import type { PositionedNode } from "./graph-data";
// ─── Canvas-based node sprites ──────────────────────────────────────────────
const spriteCache = new Map<string, THREE.Sprite>();
/**
* Creates a Three.js sprite for a graph node: colored circle with value label.
*/
export function createNodeSprite(node: PositionedNode): THREE.Sprite {
const cacheKey = `${node.id}:${node.value}:${node.color}`;
const cached = spriteCache.get(cacheKey);
if (cached) return cached.clone();
const canvas = document.createElement("canvas");
const size = 512;
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d")!;
const cx = size / 2;
const cy = size / 2;
const radius = size / 2 - 16;
// Outer glow ring
const gradient = ctx.createRadialGradient(cx, cy, radius * 0.8, cx, cy, radius);
gradient.addColorStop(0, node.color + "aa");
gradient.addColorStop(1, node.color + "00");
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
ctx.fillStyle = gradient;
ctx.fill();
// Dark background circle for contrast
ctx.beginPath();
ctx.arc(cx, cy, radius * 0.78, 0, Math.PI * 2);
ctx.fillStyle = "#0f172a";
ctx.fill();
// Colored border ring
ctx.beginPath();
ctx.arc(cx, cy, radius * 0.78, 0, Math.PI * 2);
ctx.strokeStyle = node.color;
ctx.lineWidth = 8;
ctx.stroke();
// Subtle inner fill
ctx.beginPath();
ctx.arc(cx, cy, radius * 0.74, 0, Math.PI * 2);
ctx.fillStyle = node.color + "20";
ctx.fill();
// Label (top)
ctx.fillStyle = "#ffffff";
ctx.font = "bold 36px system-ui, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.shadowColor = "#000000";
ctx.shadowBlur = 6;
const label = node.label.length > 16 ? node.label.slice(0, 14) + "..." : node.label;
ctx.fillText(label, cx, cy - 28);
// Value (bottom) — brighter, larger
ctx.fillStyle = node.color;
ctx.font = "bold 44px system-ui, sans-serif";
ctx.shadowBlur = 4;
const valueStr = typeof node.value === "number" ? node.value.toFixed(1) : String(node.value);
const displayValue = valueStr.length > 12 ? valueStr.slice(0, 10) + "..." : valueStr;
ctx.fillText(displayValue, cx, cy + 24);
// Unit (small, below value)
if (node.unit) {
ctx.fillStyle = "#94a3b8";
ctx.font = "24px system-ui, sans-serif";
ctx.shadowBlur = 0;
ctx.fillText(node.unit, cx, cy + 60);
}
const texture = new THREE.CanvasTexture(canvas);
const material = new THREE.SpriteMaterial({
map: texture,
transparent: true,
depthWrite: false,
});
const sprite = new THREE.Sprite(material);
sprite.scale.set(50, 50, 1);
spriteCache.set(cacheKey, sprite);
return sprite;
}
/**
* Creates a dimmed version of a node sprite (for non-highlighted nodes).
*/
export function createDimmedNodeSprite(node: PositionedNode): THREE.Sprite {
const sprite = createNodeSprite({ ...node, color: "#4b5563" });
sprite.material.opacity = 0.3;
return sprite;
}
// ─── Link particle rendering ───────────────────────────────────────────────
/**
* Returns a color for a link based on the source node's domain color.
*/
export function getLinkColor(
link: { source: string | { color?: string }; weight: number },
opacity = 0.4,
): string {
const sourceColor = typeof link.source === "object" && link.source.color
? link.source.color
: "#6b7280";
// Convert hex to rgba
const r = parseInt(sourceColor.slice(1, 3), 16);
const g = parseInt(sourceColor.slice(3, 5), 16);
const b = parseInt(sourceColor.slice(5, 7), 16);
return `rgba(${r},${g},${b},${opacity})`;
}