export class Globe3D { constructor(svgElement) { this.svg = svgElement; this.R = 175; this.cx = 200; this.cy = 200; this.rot = 0; // current rotation in radians // Flat paths representing the world (x: 0-600, y: 0-400) const flatContinents = [ "M295,110 L310,102 L330,108 L340,118 L336,140 L325,152 L310,155 L298,148 L288,135 L285,120 Z", // Europe "M300,160 L320,155 L335,175 L340,210 L335,250 L322,268 L308,272 L295,258 L290,225 L288,195 L292,175 Z", // Africa "M350,80 L390,72 L440,80 L475,100 L490,130 L480,155 L455,165 L420,162 L385,155 L360,145 L345,125 L340,105 Z", // Asia "M120,75 L160,62 L200,68 L225,85 L230,110 L218,135 L195,152 L168,158 L142,150 L120,132 L108,108 L110,90 Z", // NA "M185,180 L205,172 L218,190 L215,225 L205,260 L190,278 L175,275 L165,255 L160,225 L165,200 L175,188 Z", // SA "M480,228 L510,222 L530,235 L528,260 L515,272 L495,270 L480,258 L475,242 Z", // Australia "M215,42 L240,38 L255,48 L252,65 L238,72 L220,68 L212,55 Z", // Greenland "M100,330 L200,325 L320,328 L420,325 L500,330 L520,345 L480,355 L350,358 L200,355 L80,350 Z" // Antarctica ]; // Data arcs origins and targets const ox = 310, oy = 125; const flatTargets = [ { x: 160, y: 105 }, { x: 190, y: 230 }, { x: 420, y: 140 }, { x: 465, y: 110 }, { x: 320, y: 260 }, { x: 505, y: 248 }, { x: 140, y: 72 }, { x: 400, y: 80 } ]; // Parse to 3D spherical coordinates this.continents3D = flatContinents.map(path => this.parsePath(path)); this.origin3D = this.toSpherical(ox, oy); this.targets3D = flatTargets.map(t => this.toSpherical(t.x, t.y)); // Create SVG elements this.group = document.createElementNS("http://www.w3.org/2000/svg", "g"); this.group.setAttribute("clip-path", "url(#earth-clip)"); this.pathElements = this.continents3D.map(() => { const p = document.createElementNS("http://www.w3.org/2000/svg", "path"); p.setAttribute("class", "dr-earth-continent"); this.group.appendChild(p); return p; }); this.arcElements = this.targets3D.map(() => { const p = document.createElementNS("http://www.w3.org/2000/svg", "path"); p.setAttribute("class", "dr-arc"); this.group.appendChild(p); return p; }); this.endpointElements = this.targets3D.map(() => { const c = document.createElementNS("http://www.w3.org/2000/svg", "circle"); c.setAttribute("class", "dr-endpoint"); c.setAttribute("r", "3"); this.group.appendChild(c); return c; }); this.originElement = document.createElementNS("http://www.w3.org/2000/svg", "circle"); this.originElement.setAttribute("class", "dr-origin"); this.originElement.setAttribute("r", "5"); this.group.appendChild(this.originElement); this.drawGrid(); this.svg.appendChild(this.group); // Start loop this.animate = this.animate.bind(this); this.raf = requestAnimationFrame(this.animate); } toSpherical(x, y) { const lon = (x / 600) * Math.PI * 2; const lat = ((200 - y) / 200) * (Math.PI / 2); return { lon, lat }; } parsePath(pathStr) { const points = []; const regex = /([ML])\s*([-\d.]+),([-\d.]+)/g; let match; while ((match = regex.exec(pathStr)) !== null) { const cmd = match[1]; const x = parseFloat(match[2]); const y = parseFloat(match[3]); points.push({ cmd, ...this.toSpherical(x, y) }); } const isClosed = /Z$/i.test(pathStr); return { points, isClosed }; } project(lon, lat, currentRot, radiusOffset = 0) { let currentLon = lon - currentRot; // Normalize to -PI to PI while (currentLon > Math.PI) currentLon -= 2 * Math.PI; while (currentLon < -Math.PI) currentLon += 2 * Math.PI; let isFront = true; if (currentLon > Math.PI / 2) { currentLon = Math.PI / 2; isFront = false; } else if (currentLon < -Math.PI / 2) { currentLon = -Math.PI / 2; isFront = false; } const R = this.R + radiusOffset; const x3d = R * Math.cos(lat) * Math.sin(currentLon); const y3d = -R * Math.sin(lat); return { x: this.cx + x3d, y: this.cy + y3d, isFront }; } animate() { this.rot += 0.005; // rotation speed if (this.rot > Math.PI * 2) this.rot -= Math.PI * 2; // Draw continents this.continents3D.forEach((continent, i) => { let d = ""; continent.points.forEach(pt => { const proj = this.project(pt.lon, pt.lat, this.rot); d += `${pt.cmd}${proj.x},${proj.y} `; }); if (continent.isClosed) d += "Z"; this.pathElements[i].setAttribute("d", d); }); // Draw Origin const originProj = this.project(this.origin3D.lon, this.origin3D.lat, this.rot); if (originProj.isFront) { this.originElement.style.display = 'block'; this.originElement.setAttribute("cx", originProj.x); this.originElement.setAttribute("cy", originProj.y); } else { this.originElement.style.display = 'none'; } // Draw Arcs & Endpoints this.targets3D.forEach((target, i) => { const targetProj = this.project(target.lon, target.lat, this.rot); // Update endpoint if (targetProj.isFront) { this.endpointElements[i].style.display = 'block'; this.endpointElements[i].setAttribute("cx", targetProj.x); this.endpointElements[i].setAttribute("cy", targetProj.y); } else { this.endpointElements[i].style.display = 'none'; } // Draw arc (if either start or end is on front) if (originProj.isFront || targetProj.isFront) { let arcD = `M${originProj.x},${originProj.y} `; const segments = 10; for (let s = 1; s <= segments; s++) { const t = s / segments; // Interpolate lon and lat // Handle shortest path across the dateline let dLon = target.lon - this.origin3D.lon; if (dLon > Math.PI) dLon -= 2 * Math.PI; if (dLon < -Math.PI) dLon += 2 * Math.PI; const iLon = this.origin3D.lon + dLon * t; const iLat = this.origin3D.lat + (target.lat - this.origin3D.lat) * t; // Add altitude const alt = Math.sin(t * Math.PI) * 40; const p = this.project(iLon, iLat, this.rot, alt); arcD += `L${p.x},${p.y} `; } this.arcElements[i].style.display = 'block'; this.arcElements[i].setAttribute("d", arcD); } else { this.arcElements[i].style.display = 'none'; } }); this.raf = requestAnimationFrame(this.animate); } drawGrid() { const gridColor = "rgba(122, 186, 255, 0.08)"; const createPath = (d) => { const p = document.createElementNS("http://www.w3.org/2000/svg", "path"); p.setAttribute("d", d); p.setAttribute("fill", "none"); p.setAttribute("stroke", gridColor); p.setAttribute("stroke-width", "0.5"); this.group.insertBefore(p, this.group.firstChild); }; // Equator and Longitude Center createPath(`M25,200 L375,200`); createPath(`M200,25 L200,375`); // Latitudes (Ellipses) createPath(`M 52 100 A 148 50 0 0 0 348 100`); createPath(`M 52 300 A 148 50 0 0 1 348 300`); createPath(`M 28 150 A 172 40 0 0 0 372 150`); createPath(`M 28 250 A 172 40 0 0 1 372 250`); // Longitudes (Ellipses) createPath(`M 200 25 A 60 175 0 0 0 200 375`); createPath(`M 200 25 A 120 175 0 0 0 200 375`); createPath(`M 200 25 A 60 175 0 0 1 200 375`); createPath(`M 200 25 A 120 175 0 0 1 200 375`); } destroy() { cancelAnimationFrame(this.raf); this.group.remove(); } }