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();
  }
}