import { useEffect, useRef } from 'react';
interface Node {
x: number;
y: number;
vx: number;
vy: number;
radius: number;
color: string;
}
interface Edge {
from: number;
to: number;
}
const COLORS = [
'hsl(130, 100%, 50%)', // acid green
'hsl(180, 100%, 50%)', // cyan
'hsl(300, 100%, 60%)', // magenta
'hsl(60, 100%, 50%)', // yellow
'hsl(30, 100%, 50%)', // orange
];
export const KnowledgeGraph = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const animationRef = useRef<number>();
const nodesRef = useRef<Node[]>([]);
const edgesRef = useRef<Edge[]>([]);
const mouseRef = useRef({ x: 0, y: 0 });
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const resize = () => {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
initNodes();
};
const initNodes = () => {
const nodeCount = Math.min(200, Math.floor((canvas.width * canvas.height) / 6000));
nodesRef.current = [];
edgesRef.current = [];
for (let i = 0; i < nodeCount; i++) {
nodesRef.current.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
vx: (Math.random() - 0.5) * 0.5,
vy: (Math.random() - 0.5) * 0.5,
radius: Math.random() * 3 + 2,
color: COLORS[Math.floor(Math.random() * COLORS.length)],
});
}
// Create edges between nearby nodes
for (let i = 0; i < nodeCount; i++) {
for (let j = i + 1; j < nodeCount; j++) {
if (Math.random() < 0.1) {
edgesRef.current.push({ from: i, to: j });
}
}
}
};
const handleMouseMove = (e: MouseEvent) => {
mouseRef.current = { x: e.clientX, y: e.clientY };
};
const animate = () => {
if (!ctx || !canvas) return;
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const nodes = nodesRef.current;
const edges = edgesRef.current;
const mouse = mouseRef.current;
// Update and draw nodes
nodes.forEach((node, i) => {
// Mouse attraction
const dx = mouse.x - node.x;
const dy = mouse.y - node.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 200 && dist > 0) {
node.vx += (dx / dist) * 0.02;
node.vy += (dy / dist) * 0.02;
}
// Update position
node.x += node.vx;
node.y += node.vy;
// Damping
node.vx *= 0.99;
node.vy *= 0.99;
// Bounce off edges
if (node.x < 0 || node.x > canvas.width) node.vx *= -1;
if (node.y < 0 || node.y > canvas.height) node.vy *= -1;
// Keep in bounds
node.x = Math.max(0, Math.min(canvas.width, node.x));
node.y = Math.max(0, Math.min(canvas.height, node.y));
});
// Draw edges
edges.forEach((edge) => {
const from = nodes[edge.from];
const to = nodes[edge.to];
const dist = Math.sqrt((from.x - to.x) ** 2 + (from.y - to.y) ** 2);
if (dist < 200) {
const alpha = (1 - dist / 200) * 0.3;
ctx.strokeStyle = `rgba(0, 255, 65, ${alpha})`;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.lineTo(to.x, to.y);
ctx.stroke();
}
});
// Draw nodes with glow
nodes.forEach((node) => {
// Glow
const gradient = ctx.createRadialGradient(
node.x, node.y, 0,
node.x, node.y, node.radius * 4
);
gradient.addColorStop(0, node.color);
gradient.addColorStop(0.5, node.color.replace(')', ', 0.3)').replace('hsl', 'hsla'));
gradient.addColorStop(1, 'transparent');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(node.x, node.y, node.radius * 4, 0, Math.PI * 2);
ctx.fill();
// Core
ctx.fillStyle = node.color;
ctx.beginPath();
ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2);
ctx.fill();
});
animationRef.current = requestAnimationFrame(animate);
};
resize();
window.addEventListener('resize', resize);
window.addEventListener('mousemove', handleMouseMove);
animate();
return () => {
window.removeEventListener('resize', resize);
window.removeEventListener('mousemove', handleMouseMove);
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, []);
return (
<canvas
ref={canvasRef}
className="absolute inset-0 w-full h-full"
style={{ background: 'transparent' }}
/>
);
};