/* ── v4 Charts: NodeNetwork, SkillRadar, Dashboard Charts ── */ /* ── Node Network Canvas ── */ function NodeNetwork({ style }) { const canvasRef = React.useRef(null); const animRef = React.useRef(null); const nodesRef = React.useRef([]); const mouseRef = React.useRef({ x: -1000, y: -1000 }); const isMobile = typeof window !== 'undefined' && window.innerWidth < 768; React.useEffect(() => { if (isMobile) return; // skip on mobile const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; let cw, ch; const resize = () => { cw = canvas.offsetWidth; ch = canvas.offsetHeight; canvas.width = cw * dpr; canvas.height = ch * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); }; const initNodes = () => { const count = Math.min(70, Math.floor((cw * ch) / 14000)); nodesRef.current = Array.from({ length: count }, () => ({ x: Math.random() * cw, y: Math.random() * ch, vx: (Math.random() - 0.5) * 0.35, vy: (Math.random() - 0.5) * 0.35, r: Math.random() * 2 + 1, pulse: Math.random() * Math.PI * 2, })); }; const draw = () => { ctx.clearRect(0, 0, cw, ch); const nodes = nodesRef.current; const mx = mouseRef.current.x, my = mouseRef.current.y; nodes.forEach(n => { n.x += n.vx; n.y += n.vy; n.pulse += 0.02; if (n.x < 0 || n.x > cw) n.vx *= -1; if (n.y < 0 || n.y > ch) n.vy *= -1; const dxm = n.x - mx, dym = n.y - my, dm = Math.sqrt(dxm*dxm + dym*dym); if (dm < 150) { const f = (150-dm)/150*0.8; n.x += (dxm/dm)*f; n.y += (dym/dm)*f; } }); for (let i = 0; i < nodes.length; i++) { for (let j = i+1; j < nodes.length; j++) { const dx = nodes[i].x-nodes[j].x, dy = nodes[i].y-nodes[j].y, d = Math.sqrt(dx*dx+dy*dy); if (d < 130) { ctx.beginPath(); ctx.moveTo(nodes[i].x, nodes[i].y); ctx.lineTo(nodes[j].x, nodes[j].y); ctx.strokeStyle = `rgba(124,58,237,${(1-d/130)*0.14})`; ctx.lineWidth = 0.5; ctx.stroke(); } } const dxm = nodes[i].x-mx, dym = nodes[i].y-my, dm = Math.sqrt(dxm*dxm+dym*dym); if (dm < 200) { ctx.beginPath(); ctx.moveTo(nodes[i].x, nodes[i].y); ctx.lineTo(mx, my); ctx.strokeStyle = `rgba(45,212,191,${(1-dm/200)*0.3})`; ctx.lineWidth = 0.8; ctx.stroke(); } } nodes.forEach(n => { const g = Math.sin(n.pulse)*0.3+0.7; ctx.beginPath(); ctx.arc(n.x, n.y, n.r*g, 0, Math.PI*2); ctx.fillStyle = `rgba(124,58,237,${0.4+g*0.3})`; ctx.fill(); }); animRef.current = requestAnimationFrame(draw); }; resize(); initNodes(); draw(); const onMove = e => { const r = canvas.getBoundingClientRect(); mouseRef.current = { x: e.clientX-r.left, y: e.clientY-r.top }; }; const onLeave = () => { mouseRef.current = { x: -1000, y: -1000 }; }; canvas.addEventListener('mousemove', onMove); canvas.addEventListener('mouseleave', onLeave); const onVis = () => { if (document.hidden) cancelAnimationFrame(animRef.current); else draw(); }; document.addEventListener('visibilitychange', onVis); const onResize = () => { resize(); initNodes(); }; window.addEventListener('resize', onResize); return () => { cancelAnimationFrame(animRef.current); canvas.removeEventListener('mousemove', onMove); canvas.removeEventListener('mouseleave', onLeave); document.removeEventListener('visibilitychange', onVis); window.removeEventListener('resize', onResize); }; }, [isMobile]); if (isMobile) return null; return