APP-Backend / src /utils /globe3d.js
Luca448's picture
Update independent app to newest state with LFS and binary removals
ff36e71
Raw History Blame Contribute Delete
7.72 kB
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();
}
}