/* Brand New Edge — 3D spinning globe (canvas, no deps). Orthographic projection of a rotating unit sphere with graticule, great-circle arcs from BNE markets converging on São Paulo, and traveling pulses. Brand purple palette. Honors reduced-motion. */ const { useRef, useEffect } = React; // lat/lon in degrees const BNE_HUB = { lat:-23.55, lon:-46.63, name:'São Paulo' }; const BNE_NODES = [ { lat:40.71, lon:-74.0 }, // New York (USA) { lat:37.77, lon:-122.42 }, // San Francisco (USA) { lat:43.65, lon:-79.38 }, // Toronto (Canada) { lat:51.51, lon:-0.13 }, // London (UK) { lat:38.72, lon:-9.14 }, // Lisbon (Portugal) ]; function llToVec(lat, lon) { const phi = lat * Math.PI/180, lam = lon * Math.PI/180; return [Math.cos(phi)*Math.sin(lam), Math.sin(phi), Math.cos(phi)*Math.cos(lam)]; } function rotY([x,y,z], a){ const c=Math.cos(a),s=Math.sin(a); return [x*c+z*s, y, -x*s+z*c]; } function rotX([x,y,z], a){ const c=Math.cos(a),s=Math.sin(a); return [x, y*c-z*s, y*s+z*c]; } function norm([x,y,z]){ const m=Math.hypot(x,y,z)||1; return [x/m,y/m,z/m]; } // spherical linear interpolation between two unit vectors function slerp(a,b,t){ let d=a[0]*b[0]+a[1]*b[1]+a[2]*b[2]; d=Math.max(-1,Math.min(1,d)); const o=Math.acos(d); if(o<1e-4) return a; const s=Math.sin(o), w1=Math.sin((1-t)*o)/s, w2=Math.sin(t*o)/s; return [a[0]*w1+b[0]*w2, a[1]*w1+b[1]*w2, a[2]*w1+b[2]*w2]; } // Continent outlines as [lon,lat] polygons — higher-detail, recognizable coasts. const CONTINENTS = [ // North America (mainland + Alaska) [[-166,66],[-168,68],[-163,71],[-156,71],[-148,70],[-140,70],[-132,69],[-124,70],[-114,69],[-104,68],[-95,69],[-88,70],[-82,73],[-78,68],[-80,62],[-78,57],[-92,57],[-79,52],[-64,60],[-56,53],[-53,47],[-60,46],[-66,44],[-70,43],[-70,41],[-74,40],[-75,37],[-76,35],[-81,31],[-80,27],[-80,25],[-82,28],[-84,30],[-88,30],[-90,29],[-94,29],[-97,26],[-97,22],[-95,18],[-105,22],[-110,24],[-113,27],[-115,30],[-118,33],[-121,35],[-122,38],[-124,40],[-124,43],[-124,46],[-123,48],[-128,51],[-132,55],[-138,58],[-146,60],[-152,59],[-158,57],[-162,59],[-166,66]], // Central America [[-95,18],[-93,15],[-90,14],[-86,12],[-83,10],[-79,8],[-76,7],[-78,10],[-81,12],[-85,15],[-89,16],[-92,17],[-95,18]], // Greenland [[-46,60],[-30,60],[-20,68],[-22,76],[-30,82],[-45,83],[-58,80],[-62,76],[-54,68],[-50,64],[-46,60]], // Iceland [[-24,64],[-21,66],[-15,66],[-14,64],[-19,63],[-24,64]], // South America [[-78,9],[-75,11],[-71,12],[-64,11],[-60,10],[-52,5],[-50,1],[-48,-1],[-44,-3],[-40,-3],[-35,-6],[-35,-9],[-38,-13],[-39,-16],[-41,-22],[-48,-25],[-48,-28],[-53,-34],[-58,-35],[-57,-39],[-62,-40],[-65,-45],[-69,-50],[-69,-53],[-74,-53],[-73,-48],[-74,-43],[-73,-37],[-71,-30],[-71,-24],[-70,-18],[-76,-14],[-78,-8],[-81,-6],[-81,-2],[-80,2],[-78,9]], // Africa [[-16,15],[-17,21],[-13,28],[-10,31],[-6,36],[0,36],[10,37],[11,33],[20,33],[25,32],[32,31],[34,28],[37,22],[39,18],[43,12],[44,11],[51,12],[51,7],[48,5],[44,2],[42,-2],[40,-5],[40,-10],[40,-15],[35,-20],[33,-26],[28,-32],[20,-35],[18,-34],[16,-29],[14,-23],[13,-17],[12,-10],[10,-2],[9,3],[5,5],[0,5],[-5,5],[-8,4],[-12,8],[-16,15]], // Madagascar [[43,-12],[47,-15],[50,-20],[49,-25],[45,-25],[43,-18],[43,-12]], // Europe (Iberia, France, Italy, Balkans, eastward) [[-9,37],[-9,41],[-9,43],[-2,44],[-2,48],[0,49],[4,51],[8,54],[8,57],[11,58],[10,54],[13,54],[19,54],[20,56],[24,57],[26,60],[30,60],[30,55],[28,50],[28,46],[30,46],[34,46],[38,47],[40,46],[42,43],[38,42],[34,42],[28,41],[26,40],[23,40],[20,42],[18,42],[16,42],[18,41],[16,38],[15,38],[13,42],[8,44],[7,44],[3,43],[-2,40],[-6,38],[-9,37]], // Scandinavia [[5,58],[6,62],[10,64],[15,67],[20,69],[25,71],[28,70],[26,66],[23,66],[20,64],[16,62],[12,60],[8,58],[5,58]], // Great Britain [[-5,50],[-6,52],[-5,55],[-3,58],[-2,57],[0,53],[-1,51],[-5,50]], // Ireland [[-10,52],[-10,54],[-7,55],[-6,53],[-9,51],[-10,52]], // Eurasia bridge (Russia / Central Asia — connects Europe to Asia) [[28,48],[32,55],[40,60],[50,63],[62,65],[74,67],[84,67],[90,62],[86,55],[78,51],[68,49],[58,48],[48,48],[40,48],[32,47],[28,48]], // Asia (Middle East to Siberia to China) [[40,40],[44,40],[48,44],[50,47],[55,52],[60,55],[68,55],[60,62],[68,68],[78,72],[90,74],[105,75],[120,73],[135,72],[150,70],[160,68],[170,67],[178,66],[170,60],[162,58],[156,52],[143,52],[140,48],[135,44],[131,43],[130,40],[127,40],[122,40],[121,37],[122,31],[121,28],[118,24],[110,21],[108,16],[106,10],[104,9],[100,13],[98,16],[94,16],[90,22],[88,22],[80,15],[77,8],[73,16],[68,24],[66,25],[62,25],[57,25],[56,27],[50,30],[48,30],[48,25],[44,29],[40,36],[36,36],[36,40],[40,40]], // Sumatra [[95,5],[98,3],[100,0],[102,-3],[105,-5],[103,-5],[100,-2],[97,2],[95,5]], // Java [[105,-6],[110,-7],[114,-8],[112,-8],[108,-7],[105,-6]], // Borneo [[109,2],[114,4],[118,3],[119,-1],[116,-3],[111,-3],[109,0],[109,2]], // Sulawesi [[120,1],[123,1],[125,-2],[123,-5],[120,-3],[119,0],[120,1]], // New Guinea [[131,-1],[138,-3],[144,-4],[150,-7],[146,-8],[140,-7],[134,-4],[131,-1]], // Philippines [[120,5],[122,8],[124,11],[126,14],[125,18],[121,18],[120,13],[120,5]], // Japan [[129,31],[130,34],[133,35],[136,37],[139,39],[142,42],[144,45],[146,44],[144,41],[142,38],[140,36],[137,34],[134,33],[131,32],[129,31]], // Australia [[114,-22],[114,-26],[116,-31],[118,-34],[123,-34],[129,-32],[131,-31],[134,-33],[138,-35],[141,-38],[144,-38],[147,-38],[150,-37],[153,-31],[153,-27],[151,-24],[146,-19],[142,-11],[137,-12],[136,-15],[130,-13],[126,-14],[122,-17],[114,-22]], // New Zealand [[166,-46],[168,-44],[171,-42],[173,-41],[175,-39],[177,-38],[175,-41],[172,-43],[170,-45],[167,-47],[166,-46]], ]; function pointInPoly(lon, lat, poly){ let inside=false; for(let i=0,j=poly.length-1;ilat)!==(yj>lat)) && (lon < (xj-xi)*(lat-yi)/(yj-yi)+xi)) inside=!inside; } return inside; } function isLand(lon, lat){ for(const p of CONTINENTS) if(pointInPoly(lon,lat,p)) return true; return false; } function Globe({ size = 520 }) { const ref = useRef(null); useEffect(() => { const canvas = ref.current; if (!canvas) return; const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const ctx = canvas.getContext('2d'); let dpr = Math.min(window.devicePixelRatio || 1, 2); let W = size, H = size; const setup = () => { W = canvas.clientWidth || size; H = W; canvas.width = W*dpr; canvas.height = H*dpr; ctx.setTransform(dpr,0,0,dpr,0,0); }; setup(); window.addEventListener('resize', setup); let ro; if (window.ResizeObserver){ ro = new ResizeObserver(() => setup()); ro.observe(canvas); } const baseLon = 0.9; // bring Americas/Atlantic to front let theta = 0; // spin angle (horizontal) let tilt = 0; // no tilt — equator centered, neither up nor down let velTheta = 0; // inertia after drag let dragging = false, lastX = 0, lastY = 0, idleSpin = true; const R = () => Math.min(W,H)*0.40; const C = () => [W/2, H/2]; // pointer drag to spin const onDown = (e) => { dragging = true; idleSpin = false; velTheta = 0; lastX = e.clientX; lastY = e.clientY; canvas.style.cursor = 'grabbing'; if (canvas.setPointerCapture && e.pointerId!=null) { try{canvas.setPointerCapture(e.pointerId);}catch(_){} } }; const onMove = (e) => { if (!dragging) return; const dx = e.clientX - lastX, dy = e.clientY - lastY; lastX = e.clientX; lastY = e.clientY; theta += dx * 0.006; velTheta = dx * 0.006; tilt = Math.max(-1.15, Math.min(0.6, tilt - dy * 0.005)); if (e.cancelable) e.preventDefault(); }; const onUp = () => { if (!dragging) return; dragging = false; idleSpin = true; canvas.style.cursor = 'grab'; }; canvas.style.cursor = 'grab'; canvas.addEventListener('pointerdown', onDown); window.addEventListener('pointermove', onMove, { passive:false }); window.addEventListener('pointerup', onUp); // precompute graticule polylines (lat/lon grids) const grat = []; for (let lon=-180; lon<180; lon+=30){ const line=[]; for(let lat=-80;lat<=80;lat+=4) line.push(llToVec(lat,lon)); grat.push(line); } for (let lat=-60; lat<=60; lat+=30){ const line=[]; for(let lon=-180;lon<=180;lon+=4) line.push(llToVec(lat,lon)); grat.push(line); } // precompute land dots (continents) — dense dotted-globe look const landDots = []; for (let lat=-80; lat<=82; lat+=2){ const rowStep = 2 / Math.max(0.15, Math.cos(lat*Math.PI/180)); for (let lon=-180; lon<180; lon+=rowStep){ if (isLand(lon, lat)) landDots.push(llToVec(lat, lon)); } } // arcs: market node -> hub, lifted off surface const hubV = llToVec(BNE_HUB.lat, BNE_HUB.lon); const arcs = BNE_NODES.map((n,i) => { const a = llToVec(n.lat, n.lon); const pts=[]; const N=64; for(let k=0;k<=N;k++){ const t=k/N; const v=norm(slerp(a,hubV,t)); const lift=1+0.18*Math.sin(Math.PI*t); pts.push([v[0]*lift,v[1]*lift,v[2]*lift]); } return { pts, a, phase: i/BNE_NODES.length }; }); const project = (v) => { let p = rotY(v, theta + baseLon); p = rotX(p, tilt); const [cx,cy] = C(); const r = R(); return { x: cx + p[0]*r, y: cy - p[1]*r, z: p[2] }; }; let raf, t0 = performance.now(); const draw = (now) => { const dt = now - t0; t0 = now; if (dragging) { // rotation controlled by pointer } else if (Math.abs(velTheta) > 0.0002) { theta += velTheta; velTheta *= 0.94; // inertia after release if (Math.abs(velTheta) <= 0.0002) idleSpin = true; } else if (idleSpin && !reduce) { theta += dt * 0.0001; // slow auto-spin } const [cx,cy] = C(); const r = R(); ctx.clearRect(0,0,W,H); // glassy white sphere (transparent) let sg = ctx.createRadialGradient(cx-r*0.36, cy-r*0.40, r*0.1, cx, cy, r*1.05); sg.addColorStop(0,'rgba(255,255,255,0.17)'); sg.addColorStop(0.6,'rgba(255,255,255,0.06)'); sg.addColorStop(1,'rgba(255,255,255,0.02)'); ctx.fillStyle = sg; ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2); ctx.fill(); // limb ring ctx.lineWidth = 1.1; ctx.strokeStyle = 'rgba(255,255,255,0.45)'; ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2); ctx.stroke(); // graticule (front-facing segments, subtle) ctx.lineWidth = 1; for (const line of grat){ for (let k=0;k 0.02){ ctx.fillStyle = `rgba(255,255,255,${0.4 + 0.6*p.z})`; ctx.beginPath(); ctx.arc(p.x, p.y, dotR, 0, Math.PI*2); ctx.fill(); } else { // far side — faint, so you can see it through the globe while it spins ctx.fillStyle = `rgba(214,196,240,${0.07 + 0.08*(p.z+1)})`; ctx.beginPath(); ctx.arc(p.x, p.y, dotR*0.8, 0, Math.PI*2); ctx.fill(); } } // arcs + traveling pulses for (const arc of arcs){ // arc line ctx.lineWidth = 1.6; for (let k=0;k0){ ctx.fillStyle = `rgba(255,255,255,${0.4+0.5*np.z})`; ctx.beginPath(); ctx.arc(np.x,np.y, 2.6, 0, Math.PI*2); ctx.fill(); } // traveling pulse — market → Brazil const speed = reduce ? 0 : 0.00016; const tt = ((now*speed)+arc.phase) % 1; const idx = Math.floor(tt*(arc.pts.length-1)); const pp = project(arc.pts[idx]); if (pp.z>0){ ctx.shadowColor = 'rgba(123,47,190,0.9)'; ctx.shadowBlur = 10; ctx.fillStyle = '#9B55D6'; ctx.beginPath(); ctx.arc(pp.x,pp.y, 2.8, 0, Math.PI*2); ctx.fill(); ctx.shadowBlur = 0; } // traveling pulse — Brazil → market (reverse, staggered) const tt2 = ((now*speed)+arc.phase+0.5) % 1; const idx2 = Math.floor((1-tt2)*(arc.pts.length-1)); const pp2 = project(arc.pts[idx2]); if (pp2.z>0){ ctx.shadowColor = 'rgba(198,163,236,0.9)'; ctx.shadowBlur = 9; ctx.fillStyle = '#E4D2F7'; ctx.beginPath(); ctx.arc(pp2.x,pp2.y, 2.4, 0, Math.PI*2); ctx.fill(); ctx.shadowBlur = 0; } } // hub (São Paulo) — pulsing const hp = project(hubV); if (hp.z>0){ const pulse = reduce ? 0.5 : (0.5+0.5*Math.sin(now*0.004)); ctx.strokeStyle = `rgba(155,85,214,${0.5*(1-pulse)+0.1})`; ctx.lineWidth = 1.4; ctx.beginPath(); ctx.arc(hp.x,hp.y, 6+pulse*9, 0, Math.PI*2); ctx.stroke(); ctx.shadowColor = 'rgba(155,85,214,0.95)'; ctx.shadowBlur = 16; ctx.fillStyle = '#9B55D6'; ctx.beginPath(); ctx.arc(hp.x,hp.y, 4.6, 0, Math.PI*2); ctx.fill(); ctx.shadowBlur = 0; ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(hp.x,hp.y, 1.8, 0, Math.PI*2); ctx.fill(); } raf = requestAnimationFrame(draw); }; raf = requestAnimationFrame(draw); return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', setup); if (ro) ro.disconnect(); canvas.removeEventListener('pointerdown', onDown); window.removeEventListener('pointermove', onMove); window.removeEventListener('pointerup', onUp); }; }, [size]); return ; } Object.assign(window, { Globe }); /* live timezone helpers */ function zoneOffsetHours(zone){ const now = new Date(); const inv = new Date(now.toLocaleString('en-US', { timeZone: zone })); return (inv - now) / 3600000; } function fmtTime(zone){ try { return new Date().toLocaleTimeString('en-US', { timeZone: zone, hour:'numeric', minute:'2-digit', hour12:true }); } catch(e){ return '--:--'; } } function diffLabel(tz, hubTz){ const d = Math.round(zoneOffsetHours(tz) - zoneOffsetHours(hubTz)); if (d === 0) return '±0h'; return (d > 0 ? '+' : '') + d + 'h'; } function RouteRow({ rt, hubTz }){ const [, tick] = useState(0); useEffect(() => { const id = setInterval(() => tick(x => x+1), 20000); return () => clearInterval(id); }, []); const zones = rt.zones || []; const multi = zones.length > 1; const sub = multi ? zones.map(z => `${z.label} ${fmtTime(z.tz)}`).join(' · ') : `${zones[0].label ? zones[0].label + ' ' : ''}${fmtTime(zones[0].tz)}${zones[0].label ? '' : ' local'}`; return (
{rt.flags.map((f,j)=>0?-7:0 }}>)}
{rt.label}
{sub}
{zones.map((z,i) => ( {multi ? `${z.label} ` : ''}{diffLabel(z.tz, hubTz)} ))}
); } /* Section: text + routes (left, constrained) · large cropped globe bleeding off the right. */ function GlobeSection({ t }) { const g = t.globe; return (
{/* large globe, cropped off the right edge — stays behind content, keeps spinning */}
{g.eyebrow}

{g.title.split('\n').map((l,i)=>{i>0&&
}{l}
)}

{g.sub}

{g.localCap}
{g.routes.map((rt, i) => )}
Hub: {g.hub} ·
); } function HubClock({ hubTz }){ const [, tick] = useState(0); useEffect(() => { const id = setInterval(() => tick(x => x+1), 20000); return () => clearInterval(id); }, []); return {fmtTime(hubTz)}; } Object.assign(window, { GlobeSection });