@jjlmoya/utils-science 1.45.0 → 1.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-science",
3
- "version": "1.45.0",
3
+ "version": "1.46.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -78,6 +78,7 @@ const { title, currentLocale = "es", localeUrls = {}, hasSidebar = false } = Ast
78
78
  transition:
79
79
  background-color 0.3s ease,
80
80
  color 0.3s ease;
81
+ font-family: Inter, sans-serif;
81
82
  }
82
83
 
83
84
  main {
@@ -242,21 +242,184 @@
242
242
  color: #f87171;
243
243
  }
244
244
 
245
+ .asteroid-observer-marker {
246
+ position: relative;
247
+ background: transparent;
248
+ border: 0;
249
+ }
250
+
251
+ .asteroid-observer-pulse {
252
+ position: absolute;
253
+ left: 50%;
254
+ bottom: 0.15rem;
255
+ width: 2.35rem;
256
+ height: 2.35rem;
257
+ border: 2px solid rgba(37, 99, 235, 0.42);
258
+ border-radius: 50%;
259
+ background: rgba(59, 130, 246, 0.14);
260
+ transform: translateX(-50%);
261
+ box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.32);
262
+ animation: asteroid-observer-pulse 1.8s ease-out infinite;
263
+ }
264
+
265
+ .asteroid-observer-pin {
266
+ position: absolute;
267
+ left: 50%;
268
+ bottom: 0.62rem;
269
+ width: 1.75rem;
270
+ height: 2.05rem;
271
+ border: 2px solid #fff;
272
+ border-radius: 999px 999px 0.85rem 0.85rem;
273
+ background: #2563eb;
274
+ transform: translateX(-50%);
275
+ box-shadow: 0 10px 22px rgba(15, 23, 42, 0.32);
276
+ display: flex;
277
+ flex-direction: column;
278
+ align-items: center;
279
+ justify-content: center;
280
+ gap: 0.12rem;
281
+ }
282
+
283
+ .asteroid-observer-head {
284
+ width: 0.48rem;
285
+ height: 0.48rem;
286
+ border-radius: 50%;
287
+ background: #fff;
288
+ }
289
+
290
+ .asteroid-observer-body {
291
+ width: 0.72rem;
292
+ height: 0.56rem;
293
+ border-radius: 999px 999px 0.25rem 0.25rem;
294
+ background: #fff;
295
+ }
296
+
297
+ .asteroid-observer-label {
298
+ position: absolute;
299
+ left: 50%;
300
+ bottom: 2.7rem;
301
+ padding: 0.18rem 0.42rem;
302
+ border-radius: var(--asteroid-radius-full);
303
+ background: rgba(15, 23, 42, 0.88);
304
+ color: #fff;
305
+ font-size: 0.55rem;
306
+ font-weight: 900;
307
+ letter-spacing: 0.05em;
308
+ transform: translateX(-50%);
309
+ box-shadow: 0 6px 16px rgba(15, 23, 42, 0.25);
310
+ }
311
+
312
+ @keyframes asteroid-observer-pulse {
313
+ 70% {
314
+ box-shadow: 0 0 0 0.75rem rgba(59, 130, 246, 0);
315
+ }
316
+
317
+ 100% {
318
+ box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
319
+ }
320
+ }
321
+
245
322
  .asteroid-desktop-lab {
246
- display: none;
323
+ position: absolute;
324
+ left: 0.85rem;
325
+ right: 0.85rem;
326
+ bottom: 0;
327
+ z-index: 45;
328
+ display: flex;
329
+ max-height: min(82vh, 43rem);
330
+ padding: 0 0 0.9rem;
331
+ pointer-events: none;
332
+ transform: translateY(calc(100% - 4.85rem));
333
+ transition: transform var(--asteroid-transition-slow);
334
+ }
335
+
336
+ .asteroid-app.controls-open .asteroid-desktop-lab {
337
+ transform: translateY(0);
247
338
  }
248
339
 
249
340
  @media (min-width: 768px) {
250
341
  .asteroid-desktop-lab {
251
- display: flex;
252
- position: absolute;
253
342
  top: 1.5rem;
254
343
  left: 1.5rem;
344
+ right: auto;
255
345
  bottom: 1.5rem;
256
346
  width: 20rem;
257
347
  z-index: 30;
258
348
  flex-direction: column;
349
+ max-height: none;
350
+ padding: 0;
259
351
  pointer-events: none;
352
+ transform: none;
353
+ transition: none;
354
+ }
355
+ }
356
+
357
+ .asteroid-controls-toggle {
358
+ position: absolute;
359
+ left: 50%;
360
+ bottom: 2.15rem;
361
+ z-index: 50;
362
+ display: inline-flex;
363
+ align-items: center;
364
+ justify-content: center;
365
+ gap: 0.5rem;
366
+ min-height: 2.35rem;
367
+ padding: 0 1.05rem;
368
+ border: 1px solid rgba(148, 163, 184, 0.35);
369
+ border-radius: var(--asteroid-radius-full);
370
+ background: rgba(248, 250, 252, 0.92);
371
+ color: #1e293b;
372
+ box-shadow: 0 10px 24px rgba(15, 23, 42, 0.18);
373
+ backdrop-filter: blur(12px);
374
+ font-size: 0.7rem;
375
+ font-weight: 900;
376
+ text-transform: uppercase;
377
+ letter-spacing: 0.04em;
378
+ cursor: pointer;
379
+ pointer-events: auto;
380
+ transform: translateX(-50%);
381
+ transition: var(--asteroid-transition);
382
+ }
383
+
384
+ .asteroid-controls-toggle:active {
385
+ transform: translateX(-50%) scale(0.97);
386
+ }
387
+
388
+ .theme-dark .asteroid-controls-toggle {
389
+ border-color: rgba(148, 163, 184, 0.45);
390
+ background: rgba(15, 23, 42, 0.88);
391
+ color: #f8fafc;
392
+ }
393
+
394
+ .asteroid-app.controls-open .asteroid-controls-toggle {
395
+ opacity: 0;
396
+ pointer-events: none;
397
+ }
398
+
399
+ @media (min-width: 768px) {
400
+ .asteroid-controls-toggle {
401
+ display: none;
402
+ }
403
+ }
404
+
405
+ .asteroid-controls-backdrop {
406
+ position: absolute;
407
+ inset: 0;
408
+ z-index: 35;
409
+ background: rgba(15, 23, 42, 0.22);
410
+ opacity: 0;
411
+ pointer-events: none;
412
+ transition: opacity var(--asteroid-transition-slow);
413
+ }
414
+
415
+ .asteroid-app.controls-open .asteroid-controls-backdrop {
416
+ opacity: 1;
417
+ pointer-events: auto;
418
+ }
419
+
420
+ @media (min-width: 768px) {
421
+ .asteroid-controls-backdrop {
422
+ display: none;
260
423
  }
261
424
  }
262
425
 
@@ -274,6 +437,15 @@
274
437
  height: 100%;
275
438
  overflow: hidden;
276
439
  color: var(--asteroid-text-primary);
440
+ width: 100%;
441
+ }
442
+
443
+ @media (max-width: 767px) {
444
+ .asteroid-lab-panel {
445
+ max-height: min(82vh, 43rem);
446
+ border-radius: 1.4rem;
447
+ padding: 0.85rem 1rem 1rem;
448
+ }
277
449
  }
278
450
 
279
451
  .asteroid-lab-header {
@@ -285,12 +457,90 @@
285
457
  flex-shrink: 0;
286
458
  }
287
459
 
460
+ @media (max-width: 767px) {
461
+ .asteroid-lab-header {
462
+ min-height: 3.1rem;
463
+ padding-bottom: 0.65rem;
464
+ }
465
+ }
466
+
467
+ .asteroid-controls-close {
468
+ width: 0;
469
+ height: 2.25rem;
470
+ border: 0;
471
+ border-radius: var(--asteroid-radius-sm);
472
+ background: var(--asteroid-bg-light);
473
+ color: var(--asteroid-text-secondary);
474
+ display: inline-flex;
475
+ align-items: center;
476
+ justify-content: center;
477
+ cursor: pointer;
478
+ opacity: 0;
479
+ overflow: hidden;
480
+ pointer-events: none;
481
+ transition: var(--asteroid-transition);
482
+ }
483
+
484
+ .asteroid-app.controls-open .asteroid-controls-close {
485
+ width: 2.25rem;
486
+ border: 1px solid var(--asteroid-border-light);
487
+ opacity: 1;
488
+ pointer-events: auto;
489
+ }
490
+
491
+ .asteroid-controls-close:hover {
492
+ color: var(--asteroid-text-primary);
493
+ border-color: var(--asteroid-primary);
494
+ }
495
+
496
+ @media (min-width: 768px) {
497
+ .asteroid-controls-close {
498
+ display: none;
499
+ }
500
+ }
501
+
288
502
  .asteroid-lab-title {
289
503
  display: flex;
290
504
  align-items: center;
291
505
  gap: 0.5rem;
292
506
  }
293
507
 
508
+ .asteroid-lab-actions {
509
+ display: flex;
510
+ align-items: center;
511
+ gap: 0.55rem;
512
+ }
513
+
514
+ .asteroid-drop-center-btn {
515
+ display: inline-flex;
516
+ align-items: center;
517
+ justify-content: center;
518
+ gap: 0.45rem;
519
+ min-height: 2.35rem;
520
+ padding: 0 0.9rem;
521
+ border: 1px solid rgba(249, 115, 22, 0.5);
522
+ border-radius: var(--asteroid-radius-full);
523
+ background: linear-gradient(135deg, var(--asteroid-primary) 0%, var(--asteroid-secondary) 100%);
524
+ color: #fff;
525
+ box-shadow: 0 10px 24px rgba(220, 38, 38, 0.28);
526
+ font-size: 0.7rem;
527
+ font-weight: 900;
528
+ text-transform: uppercase;
529
+ letter-spacing: 0.04em;
530
+ cursor: pointer;
531
+ transition: var(--asteroid-transition);
532
+ }
533
+
534
+ .asteroid-drop-center-btn:active {
535
+ transform: scale(0.97);
536
+ }
537
+
538
+ @media (min-width: 768px) {
539
+ .asteroid-drop-center-btn {
540
+ display: none;
541
+ }
542
+ }
543
+
294
544
  .asteroid-lab-icon {
295
545
  width: 2rem;
296
546
  height: 2rem;
@@ -807,4 +1057,4 @@
807
1057
  width: 1px;
808
1058
  background: var(--asteroid-border-light);
809
1059
  margin: 0 0.5rem;
810
- }
1060
+ }
@@ -38,8 +38,15 @@ const { ui } = Astro.props;
38
38
  </div>
39
39
  </div>
40
40
 
41
- <div class="asteroid-desktop-lab">
42
- <div class="asteroid-lab-panel">
41
+ <button id="asteroid-controls-toggle" class="asteroid-controls-toggle" type="button" aria-controls="asteroid-lab-panel" aria-expanded="false">
42
+ <Icon name="mdi:tune-variant" class="w-5 h-5" />
43
+ <span>Controls</span>
44
+ </button>
45
+
46
+ <div id="asteroid-controls-backdrop" class="asteroid-controls-backdrop"></div>
47
+
48
+ <div class="asteroid-desktop-lab" id="asteroid-controls-shell">
49
+ <div class="asteroid-lab-panel" id="asteroid-lab-panel">
43
50
  <div class="asteroid-lab-header">
44
51
  <div class="asteroid-lab-title">
45
52
  <div class="asteroid-lab-icon">
@@ -52,6 +59,12 @@ const { ui } = Astro.props;
52
59
  <span class="asteroid-lab-subtitle">Factory</span>
53
60
  </div>
54
61
  </div>
62
+ <div class="asteroid-lab-actions">
63
+ <button id="asteroid-drop-center-btn" class="asteroid-drop-center-btn" type="button"><Icon name="mdi:meteor" class="w-5 h-5" /><span>Impact</span></button>
64
+ <button id="asteroid-controls-close" class="asteroid-controls-close" type="button" aria-label="Close controls">
65
+ <Icon name="mdi:close" class="w-5 h-5" />
66
+ </button>
67
+ </div>
55
68
  </div>
56
69
 
57
70
  <div class="asteroid-lab-content">
@@ -146,26 +159,10 @@ const { ui } = Astro.props;
146
159
  </div>
147
160
 
148
161
  <div style="display: none;">
149
- <div id="icon-safe">
150
- <svg class="w-6 h-6" style="color: #34d399;" fill="currentColor" viewBox="0 0 24 24">
151
- <path d="M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1M10,17L6,13L7.41,11.59L10,14.17L16.59,7.58L18,9L10,17Z"></path>
152
- </svg>
153
- </div>
154
- <div id="icon-shock">
155
- <svg class="w-6 h-6" style="color: #60a5fa;" fill="currentColor" viewBox="0 0 24 24">
156
- <path d="M3,12A9,9 0 0,0 12,21A9,9 0 0,0 21,12A9,9 0 0,0 12,3A9,9 0 0,0 3,12M5,12A7,7 0 0,1 12,5A7,7 0 0,1 19,12A7,7 0 0,1 12,19A7,7 0 0,1 5,12M11,17H13V15H11V17M11,13H13V7H11V13Z"></path>
157
- </svg>
158
- </div>
159
- <div id="icon-burn">
160
- <svg class="w-6 h-6" style="color: #fb923c;" fill="currentColor" viewBox="0 0 24 24">
161
- <path d="M17.66,11.2C17.43,10.9 17.15,10.64 16.89,10.38C16.22,9.78 15.46,9.35 14.82,8.72C13.33,7.26 13,4.85 13.95,3C13,3.23 12.17,3.75 11.46,4.32C8.87,6.4 7.85,10.07 9.07,13.22C9.11,13.32 9.15,13.42 9.15,13.55C9.15,13.77 9,13.97 8.8,14.05C8.57,14.15 8.33,14.09 8.14,13.93C8.08,13.88 8.04,13.83 8,13.76C6.87,12.33 6.69,10.28 7.45,8.64C5.78,10 4.87,12.3 5,14.47C5.06,14.97 5.12,15.47 5.29,15.97C5.43,16.57 5.7,17.17 6,17.7C7.08,19.43 8.95,20.67 10.96,20.92C13.1,21.19 15.39,20.8 17.03,19.32C18.86,17.66 19.5,15 18.56,12.72L18.43,12.46C18.22,12 17.66,11.2 17.66,11.2Z"></path>
162
- </svg>
163
- </div>
164
- <div id="icon-death">
165
- <svg class="w-6 h-6" style="color: #f87171;" fill="currentColor" viewBox="0 0 24 24">
166
- <path d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"></path>
167
- </svg>
168
- </div>
162
+ <div id="icon-safe"><svg class="w-6 h-6" style="color: #34d399;" fill="currentColor" viewBox="0 0 24 24"><path d="M12,1L3,5V11C3,16.55 6.84,21.74 12,23C17.16,21.74 21,16.55 21,11V5L12,1M10,17L6,13L7.41,11.59L10,14.17L16.59,7.58L18,9L10,17Z"></path></svg></div>
163
+ <div id="icon-shock"><svg class="w-6 h-6" style="color: #60a5fa;" fill="currentColor" viewBox="0 0 24 24"><path d="M3,12A9,9 0 0,0 12,21A9,9 0 0,0 21,12A9,9 0 0,0 12,3A9,9 0 0,0 3,12M5,12A7,7 0 0,1 12,5A7,7 0 0,1 19,12A7,7 0 0,1 12,19A7,7 0 0,1 5,12M11,17H13V15H11V17M11,13H13V7H11V13Z"></path></svg></div>
164
+ <div id="icon-burn"><svg class="w-6 h-6" style="color: #fb923c;" fill="currentColor" viewBox="0 0 24 24"><path d="M17.66,11.2C17.43,10.9 17.15,10.64 16.89,10.38C16.22,9.78 15.46,9.35 14.82,8.72C13.33,7.26 13,4.85 13.95,3C13,3.23 12.17,3.75 11.46,4.32C8.87,6.4 7.85,10.07 9.07,13.22C9.11,13.32 9.15,13.42 9.15,13.55C9.15,13.77 9,13.97 8.8,14.05C8.57,14.15 8.33,14.09 8.14,13.93C8.08,13.88 8.04,13.83 8,13.76C6.87,12.33 6.69,10.28 7.45,8.64C5.78,10 4.87,12.3 5,14.47C5.06,14.97 5.12,15.47 5.29,15.97C5.43,16.57 5.7,17.17 6,17.7C7.08,19.43 8.95,20.67 10.96,20.92C13.1,21.19 15.39,20.8 17.03,19.32C18.86,17.66 19.5,15 18.56,12.72L18.43,12.46C18.22,12 17.66,11.2 17.66,11.2Z"></path></svg></div>
165
+ <div id="icon-death"><svg class="w-6 h-6" style="color: #f87171;" fill="currentColor" viewBox="0 0 24 24"><path d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"></path></svg></div>
169
166
  </div>
170
167
 
171
168
  <script>
@@ -177,25 +174,13 @@ const { ui } = Astro.props;
177
174
 
178
175
  let map: L.Map;
179
176
 
180
- interface ImpactData {
181
- craterDiameter: number;
182
- thermalRadius: number;
183
- shockwave1psi: number;
184
- }
177
+ interface ImpactData { craterDiameter: number; thermalRadius: number; shockwave1psi: number; }
178
+
179
+ const impacts: { id: string; circles: L.LayerGroup; marker: L.Marker; data: ImpactData; latlng: L.LatLng; config: typeof config; }[] = [];
185
180
 
186
- const impacts: {
187
- id: string;
188
- circles: L.LayerGroup;
189
- marker: L.Marker;
190
- data: ImpactData;
191
- config: typeof config;
192
- }[] = [];
181
+ let observerLatLng: L.LatLng, observerMarker: L.Marker;
193
182
 
194
- const config = {
195
- diameter: 10000,
196
- velocity: 20,
197
- composition: "rock" as Composition,
198
- };
183
+ const config = { diameter: 10000, velocity: 20, composition: "rock" as Composition };
199
184
 
200
185
  function init() {
201
186
  map = L.map("asteroid-game-map", {
@@ -210,15 +195,33 @@ const { ui } = Astro.props;
210
195
  attribution: "&copy; OpenStreetMap &copy; CARTO",
211
196
  }).addTo(map);
212
197
 
198
+ setObserver(map.getCenter());
213
199
  setupControls();
214
200
  setupGhostDrag();
215
201
  setupGPS();
202
+ setupMobileControls();
216
203
 
217
204
  map.on("click", () => {
218
205
  map.dragging.enable();
219
206
  });
220
207
  }
221
208
 
209
+ function setControlsOpen(open: boolean) {
210
+ document.getElementById("asteroid-app")?.classList.toggle("controls-open", open);
211
+ document.getElementById("asteroid-controls-toggle")?.setAttribute("aria-expanded", open ? "true" : "false");
212
+ }
213
+
214
+ function setupMobileControls() {
215
+ const toggle = document.getElementById("asteroid-controls-toggle");
216
+ const close = document.getElementById("asteroid-controls-close");
217
+ const backdrop = document.getElementById("asteroid-controls-backdrop"), dropCenterBtn = document.getElementById("asteroid-drop-center-btn");
218
+
219
+ toggle?.addEventListener("click", () => setControlsOpen(!document.getElementById("asteroid-app")?.classList.contains("controls-open")));
220
+ const closeControls = () => setControlsOpen(false);
221
+ close?.addEventListener("click", closeControls); backdrop?.addEventListener("click", closeControls);
222
+ dropCenterBtn?.addEventListener("click", () => { spawnImpact(map.getCenter()); closeControls(); });
223
+ }
224
+
222
225
  function setupControls() {
223
226
  const sizeInput = document.getElementById("input-size") as HTMLInputElement;
224
227
  const velInput = document.getElementById("input-velocity") as HTMLInputElement;
@@ -308,6 +311,15 @@ const { ui } = Astro.props;
308
311
  });
309
312
  }
310
313
 
314
+ function createObserverIcon(): L.DivIcon {
315
+ return L.divIcon({ className: "asteroid-observer-marker", iconSize: [42, 52], iconAnchor: [21, 48], html: '<div class="asteroid-observer-pulse"></div><div class="asteroid-observer-pin"><div class="asteroid-observer-head"></div><div class="asteroid-observer-body"></div></div><div class="asteroid-observer-label">YOU</div>' });
316
+ }
317
+
318
+ function setObserver(latlng: L.LatLng) {
319
+ observerLatLng = latlng; if (observerMarker) observerMarker.setLatLng(latlng);
320
+ else observerMarker = L.marker(latlng, { icon: createObserverIcon(), zIndexOffset: 1000 }).addTo(map); updateVerdict();
321
+ }
322
+
311
323
  function setupMarkerDragHandlers(marker: L.Marker, group: L.LayerGroup, physics: ImpactData) {
312
324
  marker.on("dragstart", () => {
313
325
  map.dragging.disable();
@@ -315,6 +327,7 @@ const { ui } = Astro.props;
315
327
 
316
328
  marker.on("drag", (e: L.LeafletEvent) => {
317
329
  const newPos = (e.target as L.Marker).getLatLng();
330
+ const impact = impacts.find((item) => item.marker === marker); if (impact) impact.latlng = newPos;
318
331
  group.clearLayers();
319
332
  renderCircles(newPos, physics, group);
320
333
  updateVerdict();
@@ -323,6 +336,7 @@ const { ui } = Astro.props;
323
336
  marker.on("dragend", (e: L.LeafletEvent) => {
324
337
  map.dragging.enable();
325
338
  const newPos = (e.target as L.Marker).getLatLng();
339
+ const impact = impacts.find((item) => item.marker === marker); if (impact) impact.latlng = newPos;
326
340
  group.clearLayers();
327
341
  renderCircles(newPos, physics, group);
328
342
  updateVerdict();
@@ -347,7 +361,7 @@ const { ui } = Astro.props;
347
361
  icon: createMarkerIcon(markerColor),
348
362
  }).addTo(map);
349
363
 
350
- impacts.push({ id: Date.now().toString(), circles: group, marker, data: physics, config });
364
+ impacts.push({ id: Date.now().toString(), circles: group, marker, data: physics, latlng, config });
351
365
  setupMarkerDragHandlers(marker, group, physics);
352
366
  updateVerdict();
353
367
  }
@@ -379,9 +393,7 @@ const { ui } = Astro.props;
379
393
  }).addTo(group);
380
394
  }
381
395
 
382
- function renderCircles(latlng: L.LatLng, physics: ImpactData, group: L.LayerGroup) {
383
- addCircleLayers(latlng, physics, group);
384
- }
396
+ function renderCircles(latlng: L.LatLng, physics: ImpactData, group: L.LayerGroup) { addCircleLayers(latlng, physics, group); }
385
397
 
386
398
  function setupGPS() {
387
399
  const btn = document.getElementById("asteroid-gps-btn");
@@ -392,6 +404,7 @@ const { ui } = Astro.props;
392
404
  navigator.geolocation.getCurrentPosition(
393
405
  (pos) => {
394
406
  const ll = new L.LatLng(pos.coords.latitude, pos.coords.longitude);
407
+ setObserver(ll);
395
408
  if (gpsText) gpsText.textContent = "GPS Activo";
396
409
  map.flyTo(ll, 9);
397
410
  },
@@ -421,13 +434,20 @@ const { ui } = Astro.props;
421
434
 
422
435
  function updateVerdict() {
423
436
  const pill = document.getElementById("asteroid-verdict-pill");
424
- if (impacts.length === 0) {
425
- pill?.classList.remove("active");
426
- return;
427
- }
437
+ if (impacts.length === 0) { pill?.classList.remove("active"); return; }
438
+ pill?.classList.add("active"); updateVerdictUI(getWorstVerdict());
439
+ }
428
440
 
429
- pill?.classList.add("active");
430
- updateVerdictUI("safe");
441
+ function getWorstVerdict() {
442
+ const observer = observerLatLng;
443
+ const severity = { safe: 0, shook: 1, burned: 2, vaporized: 3 };
444
+ return impacts.reduce<keyof typeof severity>((worst, impact) => {
445
+ const distance = map.distance(observer, impact.latlng); let verdict: keyof typeof severity = "safe";
446
+ if (distance < impact.data.shockwave1psi) verdict = "shook";
447
+ if (distance < impact.data.thermalRadius) verdict = "burned";
448
+ if (distance < impact.data.craterDiameter / 2) verdict = "vaporized";
449
+ return severity[verdict] > severity[worst] ? verdict : worst;
450
+ }, "safe");
431
451
  }
432
452
 
433
453
  function clearAll() {