hlarcher HF Staff commited on
Commit
ad35d80
·
unverified ·
1 Parent(s): b8952af

Add animated Alpha Jet marker to flight path

Browse files

- Jet follows flight path during play mode with proper bearing
- Maintains heading at destination (no abrupt orientation reset)
- Linear animation for consistent speed throughout path

Files changed (2) hide show
  1. src/styles/main.css +10 -0
  2. src/webgl/FlightPathLayer.js +102 -5
src/styles/main.css CHANGED
@@ -305,3 +305,13 @@ body {
305
  stroke: var(--color-white);
306
  stroke-width: 2;
307
  }
 
 
 
 
 
 
 
 
 
 
 
305
  stroke: var(--color-white);
306
  stroke-width: 2;
307
  }
308
+
309
+ /* Jet marker */
310
+ .jet-marker {
311
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
312
+ z-index: 100;
313
+ }
314
+
315
+ .jet-marker svg {
316
+ display: block;
317
+ }
src/webgl/FlightPathLayer.js CHANGED
@@ -1,3 +1,4 @@
 
1
  import { COLORS } from '../config.js';
2
 
3
  export class FlightPathLayer {
@@ -8,6 +9,7 @@ export class FlightPathLayer {
8
  this.animationFrame = null;
9
  this.fullPath = null;
10
  this.layersAdded = false;
 
11
  }
12
 
13
  createArcPath(start, end, numPoints = 50) {
@@ -31,6 +33,81 @@ export class FlightPathLayer {
31
  return points;
32
  }
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  ensureLayers() {
35
  if (this.layersAdded) return;
36
 
@@ -117,35 +194,53 @@ export class FlightPathLayer {
117
  // Show full static path
118
  showPath(from, to) {
119
  this.stopAnimation();
 
120
  this.ensureLayers();
121
 
122
  this.fullPath = this.createArcPath(from, to);
123
  this.updatePath(this.fullPath);
124
  }
125
 
126
- // Animate path being traced progressively
127
  animatePath(from, to, duration, onComplete) {
128
  this.stopAnimation();
129
  this.ensureLayers();
130
 
131
  this.fullPath = this.createArcPath(from, to);
132
  const totalPoints = this.fullPath.length;
 
 
 
 
 
 
 
 
 
 
133
  const startTime = performance.now();
134
 
135
  const animate = (currentTime) => {
136
  const elapsed = currentTime - startTime;
137
  const progress = Math.min(elapsed / duration, 1);
138
 
139
- // Ease out cubic for smooth deceleration
140
- const eased = 1 - Math.pow(1 - progress, 3);
141
- const pointCount = Math.max(2, Math.floor(eased * totalPoints));
 
 
142
 
143
- this.updatePath(this.fullPath.slice(0, pointCount));
 
 
 
 
144
 
145
  if (progress < 1) {
146
  this.animationFrame = requestAnimationFrame(animate);
147
  } else {
148
  this.animationFrame = null;
 
149
  if (onComplete) onComplete();
150
  }
151
  };
@@ -158,6 +253,7 @@ export class FlightPathLayer {
158
  cancelAnimationFrame(this.animationFrame);
159
  this.animationFrame = null;
160
  }
 
161
  }
162
 
163
  hide() {
@@ -168,6 +264,7 @@ export class FlightPathLayer {
168
 
169
  remove() {
170
  this.hide();
 
171
 
172
  ['glow', 'main', 'blue', 'red'].forEach(suffix => {
173
  if (this.map.getLayer(`${this.id}-${suffix}`)) {
 
1
+ import mapboxgl from 'mapbox-gl';
2
  import { COLORS } from '../config.js';
3
 
4
  export class FlightPathLayer {
 
9
  this.animationFrame = null;
10
  this.fullPath = null;
11
  this.layersAdded = false;
12
+ this.jetMarker = null;
13
  }
14
 
15
  createArcPath(start, end, numPoints = 50) {
 
33
  return points;
34
  }
35
 
36
+ createJetMarker() {
37
+ const el = document.createElement('div');
38
+ el.className = 'jet-marker';
39
+ // SVG jet pointing UP (north) - rotation 0 = north
40
+ el.innerHTML = `
41
+ <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
42
+ <!-- Fuselage pointing up -->
43
+ <path d="M16 2 L18 8 L18 22 L20 26 L20 28 L16 26 L12 28 L12 26 L14 22 L14 8 Z" fill="${COLORS.white}" stroke="${COLORS.blue}" stroke-width="0.5"/>
44
+ <!-- Left wing (blue) -->
45
+ <path d="M14 12 L4 18 L4 20 L14 17 Z" fill="${COLORS.blue}"/>
46
+ <!-- Right wing (red) -->
47
+ <path d="M18 12 L28 18 L28 20 L18 17 Z" fill="${COLORS.red}"/>
48
+ <!-- Left tail -->
49
+ <path d="M14 22 L10 26 L10 27 L14 24 Z" fill="${COLORS.blue}"/>
50
+ <!-- Right tail -->
51
+ <path d="M18 22 L22 26 L22 27 L18 24 Z" fill="${COLORS.red}"/>
52
+ <!-- Cockpit -->
53
+ <ellipse cx="16" cy="7" rx="1.5" ry="2.5" fill="${COLORS.blue}" opacity="0.6"/>
54
+ </svg>
55
+ `;
56
+
57
+ this.jetMarker = new mapboxgl.Marker({
58
+ element: el,
59
+ anchor: 'center',
60
+ rotationAlignment: 'map'
61
+ });
62
+
63
+ return this.jetMarker;
64
+ }
65
+
66
+ updateJetPosition(coords, nextCoords) {
67
+ if (!this.jetMarker) return;
68
+
69
+ this.jetMarker.setLngLat(coords);
70
+
71
+ // Calculate bearing/rotation based on direction of travel
72
+ // Only update if next coords are different (avoid resetting at destination)
73
+ if (nextCoords && (nextCoords[0] !== coords[0] || nextCoords[1] !== coords[1])) {
74
+ const bearing = this.calculateBearing(coords, nextCoords);
75
+ this.jetMarker.setRotation(bearing);
76
+ }
77
+ }
78
+
79
+ calculateBearing(start, end) {
80
+ const startLat = start[1] * Math.PI / 180;
81
+ const startLng = start[0] * Math.PI / 180;
82
+ const endLat = end[1] * Math.PI / 180;
83
+ const endLng = end[0] * Math.PI / 180;
84
+
85
+ const dLng = endLng - startLng;
86
+
87
+ const x = Math.sin(dLng) * Math.cos(endLat);
88
+ const y = Math.cos(startLat) * Math.sin(endLat) - Math.sin(startLat) * Math.cos(endLat) * Math.cos(dLng);
89
+
90
+ const bearing = Math.atan2(x, y) * 180 / Math.PI;
91
+ return (bearing + 360) % 360;
92
+ }
93
+
94
+ showJet(initialCoords) {
95
+ if (!this.jetMarker) {
96
+ this.createJetMarker();
97
+ }
98
+ // Set position before adding to map
99
+ if (initialCoords) {
100
+ this.jetMarker.setLngLat(initialCoords);
101
+ }
102
+ this.jetMarker.addTo(this.map);
103
+ }
104
+
105
+ hideJet() {
106
+ if (this.jetMarker) {
107
+ this.jetMarker.remove();
108
+ }
109
+ }
110
+
111
  ensureLayers() {
112
  if (this.layersAdded) return;
113
 
 
194
  // Show full static path
195
  showPath(from, to) {
196
  this.stopAnimation();
197
+ this.hideJet();
198
  this.ensureLayers();
199
 
200
  this.fullPath = this.createArcPath(from, to);
201
  this.updatePath(this.fullPath);
202
  }
203
 
204
+ // Animate path being traced progressively with jet
205
  animatePath(from, to, duration, onComplete) {
206
  this.stopAnimation();
207
  this.ensureLayers();
208
 
209
  this.fullPath = this.createArcPath(from, to);
210
  const totalPoints = this.fullPath.length;
211
+
212
+ // Show jet at start position with initial rotation
213
+ const initialCoords = this.fullPath[0];
214
+ const nextCoords = this.fullPath[1];
215
+ this.showJet(initialCoords);
216
+
217
+ // Set initial bearing
218
+ const initialBearing = this.calculateBearing(initialCoords, nextCoords);
219
+ this.jetMarker.setRotation(initialBearing);
220
+
221
  const startTime = performance.now();
222
 
223
  const animate = (currentTime) => {
224
  const elapsed = currentTime - startTime;
225
  const progress = Math.min(elapsed / duration, 1);
226
 
227
+ // Linear progression for consistent speed
228
+ const pointIndex = Math.max(0, Math.min(Math.floor(progress * totalPoints), totalPoints - 1));
229
+
230
+ // Update path
231
+ this.updatePath(this.fullPath.slice(0, pointIndex + 1));
232
 
233
+ // Update jet position and rotation
234
+ const currentCoords = this.fullPath[pointIndex];
235
+ const lookAheadIndex = Math.min(pointIndex + 1, totalPoints - 1);
236
+ const lookAheadCoords = this.fullPath[lookAheadIndex];
237
+ this.updateJetPosition(currentCoords, lookAheadCoords);
238
 
239
  if (progress < 1) {
240
  this.animationFrame = requestAnimationFrame(animate);
241
  } else {
242
  this.animationFrame = null;
243
+ this.hideJet();
244
  if (onComplete) onComplete();
245
  }
246
  };
 
253
  cancelAnimationFrame(this.animationFrame);
254
  this.animationFrame = null;
255
  }
256
+ this.hideJet();
257
  }
258
 
259
  hide() {
 
264
 
265
  remove() {
266
  this.hide();
267
+ this.hideJet();
268
 
269
  ['glow', 'main', 'blue', 'red'].forEach(suffix => {
270
  if (this.map.getLayer(`${this.id}-${suffix}`)) {