/* Herb Immortal — visual components: animated globe, botanical illustrations, image placeholders */ // ======================== ANIMATED GLOBE ======================== const AnimatedGlobe = () => { const canvasRef = React.useRef(null); React.useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); const dpr = window.devicePixelRatio || 1; const size = 600; canvas.width = size * dpr; canvas.height = size * dpr; ctx.scale(dpr, dpr); const cx = size / 2, cy = size / 2, r = size * 0.42; const pins = [ { lat: 0.32, lng: 0.2 }, // India { lat: 0.45, lng: 0.55 }, // China { lat: 0.6, lng: 0.7 }, // SE Asia { lat: 0.35, lng: 0.85 }, // Japan { lat: 0.7, lng: 0.3 }, // Africa { lat: 0.25, lng: -0.3 }, // Toronto ]; let frame; const accent = getComputedStyle(document.documentElement).getPropertyValue("--accent-deep").trim() || "#2F3F2C"; const lineCol = getComputedStyle(document.documentElement).getPropertyValue("--line").trim() || "rgba(20,24,16,0.1)"; function draw(t) { ctx.clearRect(0, 0, size, size); const rot = t * 0.00008; ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.strokeStyle = lineCol; ctx.lineWidth = 1; ctx.stroke(); for (let i = -2; i <= 2; i++) { const y = cy + (i / 3) * r; const rx = Math.sqrt(r * r - (i / 3 * r) ** 2); ctx.beginPath(); ctx.ellipse(cx, y, rx, rx * 0.06, 0, 0, Math.PI * 2); ctx.strokeStyle = lineCol; ctx.lineWidth = 0.6; ctx.stroke(); } for (let i = 0; i < 5; i++) { const angle = rot + (i / 5) * Math.PI; const scaleX = Math.cos(angle); ctx.beginPath(); ctx.ellipse(cx, cy, Math.abs(scaleX) * r, r, 0, 0, Math.PI * 2); ctx.strokeStyle = lineCol; ctx.lineWidth = 0.6; ctx.stroke(); } pins.forEach((pin, idx) => { const pAngle = rot + pin.lng * Math.PI; const x = cx + Math.cos(pAngle) * r * 0.85 * (0.3 + pin.lat * 0.7); const y = cy - r * 0.7 + pin.lat * r * 1.4; const facing = Math.cos(pAngle); if (facing < -0.2) return; const opacity = 0.4 + facing * 0.6; const pulse = (Math.sin(t * 0.003 + idx * 1.2) + 1) / 2; const pulseR = 4 + pulse * 14; ctx.beginPath(); ctx.arc(x, y, pulseR, 0, Math.PI * 2); ctx.fillStyle = `rgba(47, 63, 44, ${opacity * pulse * 0.25})`; ctx.fill(); ctx.beginPath(); ctx.arc(x, y, 3.5, 0, Math.PI * 2); ctx.fillStyle = `rgba(47, 63, 44, ${opacity})`; ctx.fill(); }); ctx.strokeStyle = `rgba(47, 63, 44, 0.08)`; ctx.lineWidth = 0.8; const positions = pins.map((pin) => { const pAngle = rot + pin.lng * Math.PI; return { x: cx + Math.cos(pAngle) * r * 0.85 * (0.3 + pin.lat * 0.7), y: cy - r * 0.7 + pin.lat * r * 1.4, facing: Math.cos(pAngle) }; }).filter(p => p.facing > -0.2); for (let i = 0; i < positions.length; i++) { for (let j = i + 1; j < positions.length; j++) { const dx = positions[i].x - positions[j].x; const dy = positions[i].y - positions[j].y; if (Math.sqrt(dx*dx + dy*dy) < r * 0.8) { ctx.beginPath(); ctx.moveTo(positions[i].x, positions[i].y); ctx.lineTo(positions[j].x, positions[j].y); ctx.stroke(); } } } frame = requestAnimationFrame(draw); } frame = requestAnimationFrame(draw); return () => cancelAnimationFrame(frame); }, []); return (