// Animated globe for hero — canvas-based, rotating wireframe with glowing nodes const Globe = ({ accent = '#2FB380', stroke = '#1a1a1a', bg = 'transparent', animate = true }) => { const canvasRef = React.useRef(null); const rafRef = React.useRef(null); React.useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; const resize = () => { const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); }; resize(); const ro = new ResizeObserver(resize); ro.observe(canvas); // Node positions (lat, lng in degrees) — represent production chain nodes const nodes = [ { lat: -15, lng: -47, label: 'BR' }, // Brasília { lat: 40, lng: -74, label: 'NY' }, // New York { lat: 51, lng: 0, label: 'LDN' }, // London { lat: 35, lng: 139, label: 'TKY' }, // Tokyo { lat: 1, lng: 103, label: 'SGP' }, // Singapore { lat: -33, lng: 151, label: 'SYD' }, // Sydney { lat: 52, lng: 13, label: 'BER' }, // Berlin { lat: 19, lng: 72, label: 'MUM' }, // Mumbai { lat: -23, lng: -46, label: 'SP' }, // São Paulo { lat: 30, lng: 31, label: 'CAI' }, // Cairo { lat: -34, lng: 18, label: 'CPT' }, // Cape Town { lat: 37, lng: -122, label: 'SF' }, // San Francisco ]; // Arcs (connections) const arcs = [ [0, 8], [8, 1], [1, 2], [2, 6], [6, 3], [3, 4], [4, 5], [0, 11], [6, 7], [7, 4], [10, 5], [2, 9], [11, 1], [9, 7] ]; let rotation = 0; let t = 0; const project = (lat, lng, R, cx, cy, rot) => { const phi = (lat * Math.PI) / 180; const lambda = ((lng + rot) * Math.PI) / 180; const x = Math.cos(phi) * Math.sin(lambda); const y = -Math.sin(phi); const z = Math.cos(phi) * Math.cos(lambda); return { x: cx + x * R, y: cy + y * R, z, visible: z > -0.1, }; }; const draw = () => { const w = canvas.width / dpr; const h = canvas.height / dpr; ctx.clearRect(0, 0, w, h); const cx = w / 2; const cy = h / 2; const R = Math.min(w, h) * 0.42; // Outer soft glow const grad = ctx.createRadialGradient(cx, cy, R * 0.6, cx, cy, R * 1.15); grad.addColorStop(0, 'rgba(47, 179, 128, 0)'); grad.addColorStop(1, 'rgba(47, 179, 128, 0.08)'); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(cx, cy, R * 1.15, 0, Math.PI * 2); ctx.fill(); // Sphere fill — very subtle const sphereGrad = ctx.createRadialGradient(cx - R * 0.3, cy - R * 0.3, R * 0.1, cx, cy, R); sphereGrad.addColorStop(0, 'rgba(26, 26, 26, 0.03)'); sphereGrad.addColorStop(1, 'rgba(26, 26, 26, 0.10)'); ctx.fillStyle = sphereGrad; ctx.beginPath(); ctx.arc(cx, cy, R, 0, Math.PI * 2); ctx.fill(); // Parallels (latitude lines) ctx.strokeStyle = 'rgba(26, 26, 26, 0.14)'; ctx.lineWidth = 0.6; for (let lat = -75; lat <= 75; lat += 15) { ctx.beginPath(); const phi = (lat * Math.PI) / 180; const ry = Math.sin(phi) * R; const rx = Math.cos(phi) * R; ctx.ellipse(cx, cy - ry, rx, rx * 0.15, 0, 0, Math.PI * 2); ctx.stroke(); } // Meridians (longitude lines) for (let lng = 0; lng < 180; lng += 15) { const points = []; for (let lat = -90; lat <= 90; lat += 3) { const p = project(lat, lng, R, cx, cy, rotation); if (p.visible) points.push(p); else if (points.length) break; } if (points.length > 1) { ctx.beginPath(); ctx.moveTo(points[0].x, points[0].y); for (let i = 1; i < points.length; i++) { ctx.lineTo(points[i].x, points[i].y); } ctx.stroke(); } } // Equator — slightly stronger ctx.strokeStyle = 'rgba(26, 26, 26, 0.22)'; ctx.lineWidth = 0.8; ctx.beginPath(); ctx.ellipse(cx, cy, R, R * 0.04, 0, 0, Math.PI * 2); ctx.stroke(); // Main outline ctx.strokeStyle = 'rgba(26, 26, 26, 0.28)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(cx, cy, R, 0, Math.PI * 2); ctx.stroke(); // Arcs connecting nodes arcs.forEach(([a, b], idx) => { const pa = project(nodes[a].lat, nodes[a].lng, R, cx, cy, rotation); const pb = project(nodes[b].lat, nodes[b].lng, R, cx, cy, rotation); if (!pa.visible || !pb.visible) return; // Curved arc — control point above midpoint const mx = (pa.x + pb.x) / 2; const my = (pa.y + pb.y) / 2; const dx = pb.x - pa.x; const dy = pb.y - pa.y; const dist = Math.sqrt(dx * dx + dy * dy); const lift = dist * 0.25; const nx = -dy / dist; const ny = dx / dist; const cpx = mx + nx * lift; const cpy = my + ny * lift - lift * 0.3; // Animated dash offset for flow effect const phase = (t * 0.02 + idx * 0.3) % 1; ctx.strokeStyle = `rgba(47, 179, 128, ${0.35 * Math.min(pa.z, pb.z) + 0.15})`; ctx.lineWidth = 1; ctx.setLineDash([4, 6]); ctx.lineDashOffset = -phase * 10; ctx.beginPath(); ctx.moveTo(pa.x, pa.y); ctx.quadraticCurveTo(cpx, cpy, pb.x, pb.y); ctx.stroke(); ctx.setLineDash([]); }); // Nodes — glowing dots nodes.forEach((n, i) => { const p = project(n.lat, n.lng, R, cx, cy, rotation); if (!p.visible) return; const pulse = 0.7 + 0.3 * Math.sin(t * 0.03 + i); const size = 2 + p.z * 1.5; // Halo const hg = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, size * 4); hg.addColorStop(0, `rgba(47, 179, 128, ${0.5 * pulse * Math.max(0.3, p.z)})`); hg.addColorStop(1, 'rgba(47, 179, 128, 0)'); ctx.fillStyle = hg; ctx.beginPath(); ctx.arc(p.x, p.y, size * 4, 0, Math.PI * 2); ctx.fill(); // Core dot ctx.fillStyle = accent; ctx.beginPath(); ctx.arc(p.x, p.y, size, 0, Math.PI * 2); ctx.fill(); }); if (animate) { rotation = (rotation + 0.08) % 360; t += 1; } rafRef.current = requestAnimationFrame(draw); }; draw(); return () => { cancelAnimationFrame(rafRef.current); ro.disconnect(); }; }, [accent, stroke, animate]); return ( ); }; window.Globe = Globe;