Spaces:
Running
Running
File size: 7,720 Bytes
ff36e71 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | 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();
}
}
|