// Geographic outlines: Natural Earth, public domain (ne_110m_land). const canvas=document.querySelector('.connected-globe'); const ctx=canvas?.getContext('2d'); const rad=Math.PI/180; export const destinations=[ {city:'SÃO PAULO',country:'BRASIL',lat:-23.5505,lon:-46.6333}, {city:'MADRI',country:'ESPANHA',lat:40.4168,lon:-3.7038}, {city:'ROMA',country:'ITÁLIA',lat:41.9028,lon:12.4964}, {city:'LONDRES',country:'REINO UNIDO',lat:51.5074,lon:-.1278}, {city:'LISBOA',country:'PORTUGAL',lat:38.7223,lon:-9.1393}, {city:'CIDADE DO MÉXICO',country:'MÉXICO',lat:19.4326,lon:-99.1332}, {city:'BUENOS AIRES',country:'ARGENTINA',lat:-34.6037,lon:-58.3816}, {city:'WASHINGTON',country:'ESTADOS UNIDOS',lat:38.9072,lon:-77.0369}, {city:'OTTAWA',country:'CANADÁ',lat:45.4215,lon:-75.6972}, {city:'PEQUIM',country:'CHINA',lat:39.9042,lon:116.4074} ]; export function coordinate(value,lat){const total=Math.round(Math.abs(value)*60);return `${Math.floor(total/60)}°${String(total%60).padStart(2,'0')}′ ${lat?(value<0?'S':'N'):(value<0?'W':'E')}`;} function vector(lon,lat){return [Math.cos(lat*rad)*Math.cos(lon*rad),Math.sin(lat*rad),Math.cos(lat*rad)*Math.sin(lon*rad)];} function location(v){return [Math.atan2(v[2],v[0])/rad,Math.atan2(v[1],Math.hypot(v[0],v[2]))/rad];} function slerp(a,b,t){const angle=Math.acos(Math.max(-1,Math.min(1,a.reduce((s,x,i)=>s+x*b[i],0))));if(angle<.0001)return a;return a.map((x,i)=>(Math.sin((1-t)*angle)*x+Math.sin(t*angle)*b[i])/Math.sin(angle));} if(ctx){ const origin=destinations[0],ov=vector(origin.lon,origin.lat),reduced=matchMedia('(prefers-reduced-motion: reduce)').matches; const routes=destinations.map(d=>{const v=vector(d.lon,d.lat);return Array.from({length:65},(_,i)=>{const t=i/64;return [...location(slerp(ov,v,t)),1+.055*Math.sin(t*Math.PI)];});}); const centers=destinations.map(d=>location(slerp(ov,vector(d.lon,d.lat),.5))); let land=[],size=330,radius=148,index=-1,visible=true,started=performance.now(),frame; function resize(){size=canvas.clientWidth||330;const dpr=Math.min(devicePixelRatio||1,2);canvas.width=Math.round(size*dpr);canvas.height=Math.round(size*dpr);ctx.setTransform(dpr,0,0,dpr,0,0);radius=size*.445;} function paint(now){ const elapsed=reduced?0:now-started,phase=Math.floor(elapsed/4200)%destinations.length,progress=(elapsed%4200)/4200; if(phase!==index){index=phase;const d=destinations[index];document.querySelector('#connection-coordinates').textContent=`${coordinate(d.lat,true)} · ${coordinate(d.lon,false)}`;document.querySelector('#connection-destination').textContent=d.country;} const previous=centers[(index+centers.length-1)%centers.length],target=centers[index],turn=Math.min(1,progress*3),ease=turn*turn*(3-2*turn); const center=elapsed<4200?centers[0]:location(slerp(vector(...previous),vector(...target),ease)); const [clon,clat]=center.map(x=>x*rad),sin=Math.sin(clat),cos=Math.cos(clat); function project(lon,lat,lift=1){const a=lon*rad-clon,b=lat*rad,z=sin*Math.sin(b)+cos*Math.cos(b)*Math.cos(a);return [size/2+radius*lift*Math.cos(b)*Math.sin(a),size/2-radius*lift*(cos*Math.sin(b)-sin*Math.cos(b)*Math.cos(a)),z];} function line(points,color,width=1){ctx.beginPath();let pen=false;for(const p of points){const [x,y,z]=project(...p);if(z<.015){pen=false;continue;}if(pen)ctx.lineTo(x,y);else ctx.moveTo(x,y);pen=true;}ctx.strokeStyle=color;ctx.lineWidth=width;ctx.stroke();} ctx.clearRect(0,0,size,size); ctx.save();ctx.beginPath();ctx.arc(size/2,size/2,radius,0,Math.PI*2);ctx.clip(); const fill=ctx.createRadialGradient(size*.32,size*.26,0,size/2,size/2,radius);fill.addColorStop(0,'#7138ae42');fill.addColorStop(1,'#36105c55');ctx.fillStyle=fill;ctx.fillRect(0,0,size,size); for(let lat=-60;lat<=60;lat+=30)line(Array.from({length:121},(_,i)=>[-180+i*3,lat]),'#b58ce325',.65); for(let lon=-180;lon<180;lon+=30)line(Array.from({length:61},(_,i)=>[lon,-90+i*3]),'#b58ce325',.65); for(const ring of land)line(ring,'#c797ff80',.8); ctx.restore(); for(let i=1;i0){const count=Math.floor(Math.min(1,progress*1.8)*64)+1;ctx.shadowColor='#b573ff';ctx.shadowBlur=10;line(routes[index].slice(0,count),'#e2bdff',1.8);ctx.shadowBlur=0;} destinations.forEach((d,i)=>{const [x,y,z]=project(d.lon,d.lat);if(z<.02)return;const active=i===index||i===0;ctx.beginPath();ctx.arc(x,y,active?3.4:2,0,Math.PI*2);ctx.fillStyle=active?'#e0b1ff':'#ae73ec';ctx.shadowColor='#b066ff';ctx.shadowBlur=active?12:3;ctx.fill();ctx.shadowBlur=0;if(active){const pulse=reduced?0:(elapsed%1800)/1800;ctx.beginPath();ctx.arc(x,y,5+pulse*9,0,Math.PI*2);ctx.strokeStyle=`rgba(183,113,255,${.6*(1-pulse)})`;ctx.lineWidth=1;ctx.stroke();}}); if(visible&&!reduced)frame=requestAnimationFrame(paint); } resize();fetch('assets/world-land.json').then(r=>{if(!r.ok)throw Error('map');return r.json();}).then(data=>{land=data;if(reduced)paint(performance.now());}).catch(()=>{}); const observer=new ResizeObserver(()=>{resize();if(reduced)paint(performance.now());});observer.observe(canvas); if('IntersectionObserver' in window)new IntersectionObserver(entries=>{visible=entries[0].isIntersecting;cancelAnimationFrame(frame);if(visible)paint(performance.now());}).observe(canvas); else paint(performance.now()); document.addEventListener('visibilitychange',()=>{cancelAnimationFrame(frame);if(!document.hidden&&visible)paint(performance.now());}); }