// Reusable bits + service icons const sfT = (lang, en, zh) => lang === 'en' ? en : zh; // Service icon set (Lucide-style, 1.5 stroke, currentColor) const SF_ICONS = { cabling: , fiber: , wifi: , iptel: , cctv: , access: , iot: , serverroom: , fault: , hypervisor: , hci: , cyber: , soc: , sraa: , managed: , erp: , web: , app: , chatbot: , localllm: , workflow: , }; // ── Shared animated particle-network canvas ────────────────────────────────── // Drop as the first child of any position:relative section. // The canvas is position:absolute, inset:0, pointer-events:none so it never // blocks clicks. Give sibling content position:relative; zIndex:1 to float above it. // // Props: // accent — hub-node / edge-highlight color (default: '#E36918') // opacity — canvas element opacity (default: 0.15) // inverse — true on dark/navy sections → uses light node colour const SFNetworkCanvas = ({ accent = '#E36918', opacity = 0.5, inverse = false }) => { const canvasRef = React.useRef(null); const frameRef = React.useRef(null); const nodesRef = React.useRef(null); React.useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); let W = 0, H = 0; const LABELS = [ 'CORE-SW','EDGE-01','EDGE-02','NGFW','AP-1F','AP-2F','AP-3F', 'NVR','SRV-01','SRV-02','UPS','PDU','VPN','SIEM','EDR', 'CAM-01','CAM-02','PBX','VLAN10','VLAN20','BGP','OSPF', 'RADIUS','LDAP','DNS','NTP','SNMP','LLDP','QoS','PoE', ]; const CONNECT_DIST = 170; const SPEED = 0.26; const NODE_COUNT = 38; const makeNode = (i) => ({ x: Math.random() * W, y: Math.random() * H, vx: (Math.random() - 0.5) * SPEED, vy: (Math.random() - 0.5) * SPEED, r: Math.random() < 0.16 ? 3.8 : 2, label: Math.random() < 0.3 ? LABELS[i % LABELS.length] : null, }); const resize = () => { W = canvas.offsetWidth; H = canvas.offsetHeight; if (!W || !H) return; canvas.width = W * devicePixelRatio; canvas.height = H * devicePixelRatio; ctx.scale(devicePixelRatio, devicePixelRatio); if (!nodesRef.current) { nodesRef.current = Array.from({ length: NODE_COUNT }, (_, i) => makeNode(i)); } else { // Keep existing nodes; clamp positions for (const n of nodesRef.current) { n.x = Math.min(n.x, W); n.y = Math.min(n.y, H); } } }; const nodeColor = inverse ? 'rgba(255,255,255,0.55)' : (() => { const s = getComputedStyle(document.documentElement); return s.getPropertyValue('--fg-muted').trim() || '#56627A'; })(); const draw = () => { ctx.clearRect(0, 0, W, H); const nodes = nodesRef.current; if (!nodes) { frameRef.current = requestAnimationFrame(draw); return; } for (const n of nodes) { n.x += n.vx; n.y += n.vy; if (n.x < 0 || n.x > W) { n.vx *= -1; n.x = Math.max(0, Math.min(W, n.x)); } if (n.y < 0 || n.y > H) { n.vy *= -1; n.y = Math.max(0, Math.min(H, n.y)); } } // Edges 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; const dy = nodes[i].y - nodes[j].y; const d = Math.sqrt(dx*dx + dy*dy); if (d < CONNECT_DIST) { ctx.beginPath(); ctx.moveTo(nodes[i].x, nodes[i].y); ctx.lineTo(nodes[j].x, nodes[j].y); ctx.strokeStyle = nodeColor; ctx.globalAlpha = (1 - d / CONNECT_DIST) * 0.55; ctx.lineWidth = 0.65; ctx.stroke(); } } } // Nodes + labels for (const n of nodes) { const isHub = n.r > 2.5; ctx.beginPath(); ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2); ctx.fillStyle = isHub ? accent : nodeColor; ctx.globalAlpha = isHub ? 0.75 : 0.45; ctx.fill(); if (isHub) { ctx.beginPath(); ctx.arc(n.x, n.y, n.r + 4, 0, Math.PI * 2); ctx.strokeStyle = accent; ctx.globalAlpha = 0.22; ctx.lineWidth = 0.9; ctx.stroke(); } if (n.label) { ctx.font = '500 8.5px "JetBrains Mono", monospace'; ctx.fillStyle = nodeColor; ctx.globalAlpha = 0.5; ctx.fillText(n.label, n.x + n.r + 4, n.y + 3); } } ctx.globalAlpha = 1; frameRef.current = requestAnimationFrame(draw); }; const ro = new ResizeObserver(resize); ro.observe(canvas); resize(); const io = new IntersectionObserver(([e]) => { if (e.isIntersecting) { frameRef.current = requestAnimationFrame(draw); } else { cancelAnimationFrame(frameRef.current); } }); io.observe(canvas); return () => { cancelAnimationFrame(frameRef.current); ro.disconnect(); io.disconnect(); }; }, [accent, inverse]); return ( ); }; // Inline SVG logo — auto-adapts to dark/light mode via data-theme attribute const SFLogo = ({ height = 40 }) => { const uid = React.useId().replace(/[^a-z0-9]/gi, ''); const gid = 'sfg' + uid; const [dark, setDark] = React.useState( () => document.documentElement.dataset.theme === 'dark' ); React.useEffect(() => { const mo = new MutationObserver(() => setDark(document.documentElement.dataset.theme === 'dark') ); mo.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); return () => mo.disconnect(); }, []); const navy = dark ? '#FFFFFF' : '#0F2F54'; const slate = dark ? 'rgba(255,255,255,0.7)' : '#1B3A65'; return ( {/* Swoosh — tapered arc, points at both ends */} {/* SUN FOREST */} SUN FOREST {/* LIMITED */} LIMITED ); }; window.sfT = sfT; window.SF_ICONS = SF_ICONS; window.SFNetworkCanvas = SFNetworkCanvas; window.SFLogo = SFLogo;