// GlobeLoader — dot-matrix globe loader for Atlas. // Animation driven imperatively via requestAnimationFrame + transform updates // to avoid per-frame React re-renders of hundreds of dots. function GlobeLoader({ label, fadeOut, retry, onRetry }) { var dotsGroupRef = React.useRef(null); var citiesGroupRef = React.useRef(null); var scatterGroupRef = React.useRef(null); // Pre-generate Fibonacci dot cloud (static) var dots = React.useMemo(function() { var arr = []; var N = 520; var phi = Math.PI * (Math.sqrt(5) - 1); for (var i = 0; i < N; i++) { var y = 1 - (i / (N - 1)) * 2; var rad = Math.sqrt(1 - y * y); var theta = phi * i; arr.push({ x: Math.cos(theta) * rad, y: y, z: Math.sin(theta) * rad, seed: (i * 1337) % 100, scatterAngle: ((i * 1337) % 100) * 0.13, }); } return arr; }, []); var CITIES = React.useMemo(function() { return [ { lat: 40.7, lon: -74, accent: true }, { lat: 51.5, lon: -0.1 }, { lat: 35.7, lon: 139.7 }, { lat: -33.9, lon: 151.2 }, { lat: -23.5, lon: -46.6 }, { lat: -26.2, lon: 28 }, { lat: 19.1, lon: 72.9 }, { lat: 22.3, lon: 114.2 }, ]; }, []); var cx = 240, cy = 160, r = 120; React.useEffect(function() { var start = performance.now(); var raf; function tick(now) { var t = (now - start) / 1000; var rotation = t * 30; var rad = rotation * Math.PI / 180; var cosR = Math.cos(rad), sinR = Math.sin(rad); // Update each dot's transform var children = dotsGroupRef.current ? dotsGroupRef.current.children : null; if (children) { for (var i = 0; i < dots.length; i++) { var d = dots[i]; var xr = d.x * cosR - d.z * sinR; var zr = d.x * sinR + d.z * cosR; var appearAt = d.seed * 0.014; var prog = Math.max(0, Math.min(1, (t - appearAt) * 1.0)); var ease = 1 - Math.pow(1 - prog, 3); var scatter = (1 - ease) * (1 + d.seed * 0.04); var dx = Math.cos(d.scatterAngle) * scatter * 160; var dy = Math.sin(d.scatterAngle) * scatter * 160; var px = cx + xr * r + dx; var py = cy - d.y * r + dy; var front = zr > -0.1; var depth = (zr + 1) / 2; var op = ease * (front ? 0.75 + depth * 0.25 : 0.3); var node = children[i]; node.setAttribute('cx', px.toFixed(2)); node.setAttribute('cy', py.toFixed(2)); node.setAttribute('opacity', op.toFixed(3)); node.setAttribute('r', (front ? 1.3 + depth * 1.5 : 0.8).toFixed(2)); } } // Cities var cityChildren = citiesGroupRef.current ? citiesGroupRef.current.children : null; if (cityChildren) { for (var j = 0; j < CITIES.length; j++) { var city = CITIES[j]; var la = city.lat * Math.PI / 180, lo = city.lon * Math.PI / 180; var p3x = Math.cos(la) * Math.cos(lo); var p3y = Math.sin(la); var p3z = Math.cos(la) * Math.sin(lo); var cxr = p3x * cosR - p3z * sinR; var czr = p3x * sinR + p3z * cosR; var cpx = cx + cxr * r; var cpy = cy - p3y * r; var front2 = czr > 0; var appear = Math.max(0, Math.min(1, (t - 1.2 - j * 0.08) * 2.5)); var pulse = 1 + Math.sin(t * 2.5 + j) * 0.3; var g = cityChildren[j]; if (front2 && appear > 0) { g.style.display = ''; g.setAttribute('opacity', appear.toFixed(3)); g.setAttribute('transform', 'translate(' + cpx.toFixed(2) + ',' + cpy.toFixed(2) + ')'); var ring = g.children[0]; ring.setAttribute('r', (9 * pulse).toFixed(2)); } else { g.style.display = 'none'; } } } raf = requestAnimationFrame(tick); } raf = requestAnimationFrame(tick); return function() { cancelAnimationFrame(raf); }; }, [dots, CITIES]); return (
{dots.map(function(d, i) { return ; })} {CITIES.map(function(city, i) { var color = city.accent ? 'var(--accent)' : 'var(--fg)'; return ( ); })}
{retry ? '✕ ' : '◉ '}{label || 'LOADING MARKETS'}
{retry ? <>Connection interrupted.
Retrying soon. : <>Charting every market,
one tick at a time. }
{retry && onRetry && ( )}
); } window.GlobeLoader = GlobeLoader;