@jjlmoya/utils-science 1.1.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 +64 -0
- package/src/category/i18n/en.ts +97 -0
- package/src/category/i18n/es.ts +97 -0
- package/src/category/i18n/fr.ts +96 -0
- package/src/category/index.ts +17 -0
- package/src/category/seo.astro +15 -0
- package/src/components/PreviewNavSidebar.astro +116 -0
- package/src/components/PreviewToolbar.astro +143 -0
- package/src/data.ts +11 -0
- package/src/env.d.ts +5 -0
- package/src/index.ts +24 -0
- package/src/layouts/PreviewLayout.astro +117 -0
- package/src/pages/[locale]/[slug].astro +146 -0
- package/src/pages/[locale].astro +251 -0
- package/src/pages/index.astro +4 -0
- package/src/tests/faq_count.test.ts +19 -0
- package/src/tests/locale_completeness.test.ts +42 -0
- package/src/tests/mocks/astro_mock.js +2 -0
- package/src/tests/no_h1_in_components.test.ts +48 -0
- package/src/tests/seo_length.test.ts +22 -0
- package/src/tests/tool_validation.test.ts +17 -0
- package/src/tool/asteroid-impact/AsteroidImpact.css +799 -0
- package/src/tool/asteroid-impact/bibliography.astro +14 -0
- package/src/tool/asteroid-impact/component.astro +436 -0
- package/src/tool/asteroid-impact/constants.ts +67 -0
- package/src/tool/asteroid-impact/helpers.ts +17 -0
- package/src/tool/asteroid-impact/i18n/en.ts +153 -0
- package/src/tool/asteroid-impact/i18n/es.ts +153 -0
- package/src/tool/asteroid-impact/i18n/fr.ts +153 -0
- package/src/tool/asteroid-impact/index.ts +24 -0
- package/src/tool/asteroid-impact/logic/impactPhysics.ts +86 -0
- package/src/tool/asteroid-impact/script.ts +256 -0
- package/src/tool/asteroid-impact/seo.astro +15 -0
- package/src/tool/asteroid-impact/ui-helpers.ts +56 -0
- package/src/tool/asteroid-impact/ui.ts +69 -0
- package/src/tool/asteroid-impact/utils.ts +17 -0
- package/src/tool/cellular-renewal/CellularRenewal.css +1 -0
- package/src/tool/cellular-renewal/bibliography.astro +14 -0
- package/src/tool/cellular-renewal/component.astro +387 -0
- package/src/tool/cellular-renewal/i18n/en.ts +170 -0
- package/src/tool/cellular-renewal/i18n/es.ts +170 -0
- package/src/tool/cellular-renewal/i18n/fr.ts +170 -0
- package/src/tool/cellular-renewal/index.ts +24 -0
- package/src/tool/cellular-renewal/logic/CellularRenewalEngine.ts +50 -0
- package/src/tool/cellular-renewal/seo.astro +14 -0
- package/src/tool/colony-counter/ColonyCounter.css +473 -0
- package/src/tool/colony-counter/bibliography.astro +14 -0
- package/src/tool/colony-counter/component.astro +358 -0
- package/src/tool/colony-counter/i18n/en.ts +151 -0
- package/src/tool/colony-counter/i18n/es.ts +151 -0
- package/src/tool/colony-counter/i18n/fr.ts +151 -0
- package/src/tool/colony-counter/index.ts +27 -0
- package/src/tool/colony-counter/seo.astro +15 -0
- package/src/tool/microwave-detector/MicrowaveDetector.css +122 -0
- package/src/tool/microwave-detector/bibliography.astro +14 -0
- package/src/tool/microwave-detector/component.astro +650 -0
- package/src/tool/microwave-detector/i18n/en.ts +155 -0
- package/src/tool/microwave-detector/i18n/es.ts +155 -0
- package/src/tool/microwave-detector/i18n/fr.ts +155 -0
- package/src/tool/microwave-detector/index.ts +24 -0
- package/src/tool/microwave-detector/logic/MicrowaveEngine.ts +89 -0
- package/src/tool/microwave-detector/seo.astro +14 -0
- package/src/tool/simulation-probability/SimulationProbability.css +1 -0
- package/src/tool/simulation-probability/bibliography.astro +14 -0
- package/src/tool/simulation-probability/component.astro +348 -0
- package/src/tool/simulation-probability/i18n/en.ts +184 -0
- package/src/tool/simulation-probability/i18n/es.ts +184 -0
- package/src/tool/simulation-probability/i18n/fr.ts +184 -0
- package/src/tool/simulation-probability/index.ts +24 -0
- package/src/tool/simulation-probability/logic/SimulationEngine.ts +42 -0
- package/src/tool/simulation-probability/seo.astro +14 -0
- package/src/tools.ts +15 -0
- package/src/types.ts +72 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { asteroidImpact } from './index';
|
|
4
|
+
import type { KnownLocale } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'es' } = Astro.props;
|
|
11
|
+
const content = await asteroidImpact.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && <Bibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
---
|
|
2
|
+
import "./AsteroidImpact.css";
|
|
3
|
+
import "leaflet/dist/leaflet.css";
|
|
4
|
+
import { Icon } from "astro-icon/components";
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
<div class="asteroid-app" id="asteroid-app">
|
|
8
|
+
<div id="asteroid-game-map" class="asteroid-game-map" style="touch-action: none;"></div>
|
|
9
|
+
|
|
10
|
+
<div id="asteroid-map-target-overlay" class="asteroid-map-target-overlay">
|
|
11
|
+
<div class="asteroid-target-crosshair"></div>
|
|
12
|
+
<div class="asteroid-target-crosshair vertical"></div>
|
|
13
|
+
<div class="asteroid-target-box"></div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div class="asteroid-top-bar">
|
|
17
|
+
<div>
|
|
18
|
+
<button id="asteroid-gps-btn" class="asteroid-gps-btn">
|
|
19
|
+
<div id="asteroid-gps-dot" class="asteroid-gps-dot"></div>
|
|
20
|
+
<span id="asteroid-gps-text">Activar GPS</span>
|
|
21
|
+
</button>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div id="asteroid-verdict-pill" class="asteroid-verdict-pill">
|
|
25
|
+
<div id="asteroid-verdict-container" class="asteroid-verdict-container">
|
|
26
|
+
<div id="asteroid-verdict-icon" class="asteroid-verdict-icon"></div>
|
|
27
|
+
<div class="asteroid-verdict-text">
|
|
28
|
+
<div id="asteroid-verdict-label" class="asteroid-verdict-label">Análisis</div>
|
|
29
|
+
<div id="asteroid-verdict-value" class="asteroid-verdict-value">--</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div class="asteroid-desktop-lab">
|
|
36
|
+
<div class="asteroid-lab-panel">
|
|
37
|
+
<div class="asteroid-lab-header">
|
|
38
|
+
<div class="asteroid-lab-title">
|
|
39
|
+
<div class="asteroid-lab-icon">
|
|
40
|
+
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
41
|
+
<path d="M12,2L11,7H13L12,2M6.5,5.27L5.77,6L9.5,9.73L10.23,9L6.5,5.27M17.5,5.27L13.77,9L14.5,9.73L18.23,6L17.5,5.27M2,11V13H7V11H2M17,11V13H22V11H17M9.5,14.27L5.77,18L6.5,18.73L10.23,15L9.5,14.27M14.5,14.27L13.77,15L17.5,18.73L18.23,18L14.5,14.27M11,17L12,22L13,17H11Z"></path>
|
|
42
|
+
</svg>
|
|
43
|
+
</div>
|
|
44
|
+
<div>
|
|
45
|
+
<h2 class="asteroid-lab-name">Asteroid</h2>
|
|
46
|
+
<span class="asteroid-lab-subtitle">Factory</span>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="asteroid-lab-content">
|
|
52
|
+
<div class="asteroid-drag-source" id="drag-source-desktop">
|
|
53
|
+
<div class="asteroid-drag-bg"></div>
|
|
54
|
+
<div class="asteroid-drag-tooltip">
|
|
55
|
+
<span class="asteroid-drag-tooltip-text">ARRASTRA AL MAPA</span>
|
|
56
|
+
</div>
|
|
57
|
+
<div class="asteroid-params-badge">
|
|
58
|
+
<div id="asteroid-param-size" class="asteroid-param-item">Diametro: <span id="asteroid-param-size-val">50m</span></div>
|
|
59
|
+
<div id="asteroid-param-vel" class="asteroid-param-item">Velocidad: <span id="asteroid-param-vel-val">15 km/s</span></div>
|
|
60
|
+
<div id="asteroid-param-type" class="asteroid-param-item">Tipo: <span id="asteroid-param-type-val">Roca</span></div>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="asteroid-drag-icon-parent">
|
|
63
|
+
<Icon name="mdi:drag" class="w-6 h-6" />
|
|
64
|
+
</div>
|
|
65
|
+
<div class="asteroid-drag-visual">
|
|
66
|
+
<div class="asteroid-drag-grid"></div>
|
|
67
|
+
<div id="asteroid-visual" class="asteroid-visual">
|
|
68
|
+
<div id="asteroid-surface" class="asteroid-surface">
|
|
69
|
+
<div class="asteroid-surface-gradient"></div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div>
|
|
76
|
+
<div class="asteroid-control-label" style="margin-bottom: 1rem;">
|
|
77
|
+
<span class="asteroid-control-text">Datos Históricos</span>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="asteroid-presets">
|
|
80
|
+
<button class="asteroid-preset-btn" data-dia="20" data-vel="19" data-comp="rock">
|
|
81
|
+
<div class="asteroid-preset-title">CHELYABINSK</div>
|
|
82
|
+
<div class="asteroid-preset-subtitle">20m · Aéreo</div>
|
|
83
|
+
</button>
|
|
84
|
+
<button class="asteroid-preset-btn" data-dia="50" data-vel="15" data-comp="rock">
|
|
85
|
+
<div class="asteroid-preset-title">TUNGUSKA</div>
|
|
86
|
+
<div class="asteroid-preset-subtitle">50m · Bosque</div>
|
|
87
|
+
</button>
|
|
88
|
+
<button class="asteroid-preset-btn" data-dia="200" data-vel="25" data-comp="ice">
|
|
89
|
+
<div class="asteroid-preset-title">3I ATLAS</div>
|
|
90
|
+
<div class="asteroid-preset-subtitle">200m · Cometa</div>
|
|
91
|
+
</button>
|
|
92
|
+
<button class="asteroid-preset-btn" data-dia="10000" data-vel="20" data-comp="rock">
|
|
93
|
+
<div class="asteroid-preset-title">CHICXULUB</div>
|
|
94
|
+
<div class="asteroid-preset-subtitle">10km · E.L.E.</div>
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div class="asteroid-control-group">
|
|
100
|
+
<div class="asteroid-control-label">
|
|
101
|
+
<span class="asteroid-control-text">Diámetro</span>
|
|
102
|
+
<span id="display-size" class="asteroid-control-value">100m</span>
|
|
103
|
+
</div>
|
|
104
|
+
<input type="range" id="input-size" min="10" max="5000" step="10" value="5000" class="asteroid-slider" />
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div class="asteroid-control-group">
|
|
108
|
+
<div class="asteroid-control-label">
|
|
109
|
+
<span class="asteroid-control-text">Velocidad</span>
|
|
110
|
+
<span id="display-velocity" class="asteroid-control-value">20 km/s</span>
|
|
111
|
+
</div>
|
|
112
|
+
<input type="range" id="input-velocity" min="10" max="70" step="1" value="20" class="asteroid-slider" />
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div>
|
|
116
|
+
<div class="asteroid-control-text" style="margin-bottom: 0.5rem; display: block;">Composición</div>
|
|
117
|
+
<div class="asteroid-material-buttons">
|
|
118
|
+
<button class="asteroid-material-btn active" data-type="rock">
|
|
119
|
+
<div class="asteroid-material-dot" style="background: #64748b;"></div>
|
|
120
|
+
<span class="asteroid-material-name">Roca</span>
|
|
121
|
+
</button>
|
|
122
|
+
<button class="asteroid-material-btn" data-type="iron">
|
|
123
|
+
<div class="asteroid-material-dot" style="background: #1e293b;"></div>
|
|
124
|
+
<span class="asteroid-material-name">Hierro</span>
|
|
125
|
+
</button>
|
|
126
|
+
<button class="asteroid-material-btn" data-type="ice">
|
|
127
|
+
<div class="asteroid-material-dot" style="background: #67e8f9;"></div>
|
|
128
|
+
<span class="asteroid-material-name">Hielo</span>
|
|
129
|
+
</button>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<button id="clear-btn" class="asteroid-clear-btn">
|
|
134
|
+
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
|
135
|
+
<path d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"></path>
|
|
136
|
+
</svg>
|
|
137
|
+
Limpiar todo
|
|
138
|
+
</button>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div style="display: none;">
|
|
145
|
+
<div id="icon-safe">
|
|
146
|
+
<svg class="w-6 h-6" style="color: #34d399;" fill="currentColor" viewBox="0 0 24 24">
|
|
147
|
+
<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>
|
|
148
|
+
</svg>
|
|
149
|
+
</div>
|
|
150
|
+
<div id="icon-shock">
|
|
151
|
+
<svg class="w-6 h-6" style="color: #60a5fa;" fill="currentColor" viewBox="0 0 24 24">
|
|
152
|
+
<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>
|
|
153
|
+
</svg>
|
|
154
|
+
</div>
|
|
155
|
+
<div id="icon-burn">
|
|
156
|
+
<svg class="w-6 h-6" style="color: #fb923c;" fill="currentColor" viewBox="0 0 24 24">
|
|
157
|
+
<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>
|
|
158
|
+
</svg>
|
|
159
|
+
</div>
|
|
160
|
+
<div id="icon-death">
|
|
161
|
+
<svg class="w-6 h-6" style="color: #f87171;" fill="currentColor" viewBox="0 0 24 24">
|
|
162
|
+
<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>
|
|
163
|
+
</svg>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<script>
|
|
168
|
+
import L from "leaflet";
|
|
169
|
+
import { ImpactPhysics, type Composition } from "./logic/impactPhysics";
|
|
170
|
+
import { VERDICT_CONFIGS } from "./constants";
|
|
171
|
+
import { getCompositionColor } from "./utils";
|
|
172
|
+
import { updateControlUI } from "./ui-helpers";
|
|
173
|
+
|
|
174
|
+
let map: L.Map;
|
|
175
|
+
|
|
176
|
+
interface ImpactData {
|
|
177
|
+
craterDiameter: number;
|
|
178
|
+
thermalRadius: number;
|
|
179
|
+
shockwave1psi: number;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const impacts: {
|
|
183
|
+
id: string;
|
|
184
|
+
circles: L.LayerGroup;
|
|
185
|
+
marker: L.Marker;
|
|
186
|
+
data: ImpactData;
|
|
187
|
+
config: typeof config;
|
|
188
|
+
}[] = [];
|
|
189
|
+
|
|
190
|
+
const config = {
|
|
191
|
+
diameter: 10000,
|
|
192
|
+
velocity: 20,
|
|
193
|
+
composition: "rock" as Composition,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
function init() {
|
|
197
|
+
map = L.map("asteroid-game-map", {
|
|
198
|
+
zoomControl: false,
|
|
199
|
+
attributionControl: false,
|
|
200
|
+
dragging: true,
|
|
201
|
+
}).setView([40.4168, -3.7038], 6);
|
|
202
|
+
|
|
203
|
+
L.tileLayer("https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.webp", {
|
|
204
|
+
subdomains: "abcd",
|
|
205
|
+
maxZoom: 20,
|
|
206
|
+
attribution: "© OpenStreetMap © CARTO",
|
|
207
|
+
}).addTo(map);
|
|
208
|
+
|
|
209
|
+
setupControls();
|
|
210
|
+
setupGhostDrag();
|
|
211
|
+
setupGPS();
|
|
212
|
+
|
|
213
|
+
map.on("click", () => {
|
|
214
|
+
map.dragging.enable();
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function setupControls() {
|
|
219
|
+
const sizeInput = document.getElementById("input-size") as HTMLInputElement;
|
|
220
|
+
const velInput = document.getElementById("input-velocity") as HTMLInputElement;
|
|
221
|
+
const matBtns = document.querySelectorAll(".asteroid-material-btn");
|
|
222
|
+
const presetBtns = document.querySelectorAll(".asteroid-preset-btn");
|
|
223
|
+
const clearBtn = document.getElementById("clear-btn");
|
|
224
|
+
|
|
225
|
+
sizeInput?.addEventListener("input", (e) => {
|
|
226
|
+
config.diameter = parseInt((e.target as HTMLInputElement).value);
|
|
227
|
+
updateControlUI();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
velInput?.addEventListener("input", (e) => {
|
|
231
|
+
config.velocity = parseInt((e.target as HTMLInputElement).value);
|
|
232
|
+
updateControlUI();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
matBtns.forEach((btn) => {
|
|
236
|
+
btn.addEventListener("click", () => {
|
|
237
|
+
config.composition = btn.getAttribute("data-type") as Composition;
|
|
238
|
+
updateControlUI();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
presetBtns.forEach((btn) => {
|
|
243
|
+
btn.addEventListener("click", () => {
|
|
244
|
+
config.diameter = parseInt(btn.getAttribute("data-dia")!);
|
|
245
|
+
config.velocity = parseInt(btn.getAttribute("data-vel")!);
|
|
246
|
+
config.composition = btn.getAttribute("data-comp") as Composition;
|
|
247
|
+
updateControlUI();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
clearBtn?.addEventListener("click", clearAll);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function setupGhostDrag() {
|
|
255
|
+
const source = document.getElementById("drag-source-desktop");
|
|
256
|
+
if (!source) return;
|
|
257
|
+
|
|
258
|
+
const start = (e: MouseEvent | TouchEvent) => {
|
|
259
|
+
e.preventDefault();
|
|
260
|
+
const targetOverlay = document.getElementById("asteroid-map-target-overlay");
|
|
261
|
+
if (targetOverlay) targetOverlay.classList.add("active");
|
|
262
|
+
|
|
263
|
+
const onMove = (moveEvent: MouseEvent | TouchEvent) => {
|
|
264
|
+
moveEvent.preventDefault();
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const onEnd = (endEvent: MouseEvent | TouchEvent) => {
|
|
268
|
+
if (targetOverlay) targetOverlay.classList.remove("active");
|
|
269
|
+
|
|
270
|
+
const clientX = (endEvent as MouseEvent).clientX || (endEvent as TouchEvent).changedTouches[0].clientX;
|
|
271
|
+
const clientY = (endEvent as MouseEvent).clientY || (endEvent as TouchEvent).changedTouches[0].clientY;
|
|
272
|
+
|
|
273
|
+
const point = map.mouseEventToContainerPoint({ clientX, clientY } as MouseEvent);
|
|
274
|
+
const latlng = map.containerPointToLatLng(point);
|
|
275
|
+
spawnImpact(latlng);
|
|
276
|
+
|
|
277
|
+
window.removeEventListener("mousemove", onMove);
|
|
278
|
+
window.removeEventListener("mouseup", onEnd);
|
|
279
|
+
window.removeEventListener("touchmove", onMove);
|
|
280
|
+
window.removeEventListener("touchend", onEnd);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
window.addEventListener("mousemove", onMove);
|
|
284
|
+
window.addEventListener("mouseup", onEnd);
|
|
285
|
+
window.addEventListener("touchmove", onMove, { passive: false });
|
|
286
|
+
window.addEventListener("touchend", onEnd);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
source.addEventListener("mousedown", start);
|
|
290
|
+
source.addEventListener("touchstart", start);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function createMarkerIcon(color: string): L.DivIcon {
|
|
294
|
+
return L.divIcon({
|
|
295
|
+
className: "asteroid-marker",
|
|
296
|
+
iconSize: [32, 32],
|
|
297
|
+
iconAnchor: [16, 16],
|
|
298
|
+
html: `<div style="width: 2rem; height: 2rem; border-radius: 50%; background-color: ${color}; border: 2px solid white; box-shadow: 0 4px 8px rgba(0,0,0,0.2);"></div>`,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function setupMarkerDragHandlers(marker: L.Marker, group: L.LayerGroup, physics: ImpactData) {
|
|
303
|
+
marker.on("dragstart", () => {
|
|
304
|
+
map.dragging.disable();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
marker.on("drag", (e: L.LeafletEvent) => {
|
|
308
|
+
const newPos = (e.target as L.Marker).getLatLng();
|
|
309
|
+
group.clearLayers();
|
|
310
|
+
renderCircles(newPos, physics, group);
|
|
311
|
+
updateVerdict();
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
marker.on("dragend", (e: L.LeafletEvent) => {
|
|
315
|
+
map.dragging.enable();
|
|
316
|
+
const newPos = (e.target as L.Marker).getLatLng();
|
|
317
|
+
group.clearLayers();
|
|
318
|
+
renderCircles(newPos, physics, group);
|
|
319
|
+
updateVerdict();
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function spawnImpact(latlng: L.LatLng) {
|
|
324
|
+
const physics = ImpactPhysics.calculate({
|
|
325
|
+
diameter: config.diameter,
|
|
326
|
+
velocity: config.velocity * 1000,
|
|
327
|
+
composition: config.composition,
|
|
328
|
+
distance: 0,
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const group = L.layerGroup();
|
|
332
|
+
addCircleLayers(latlng, physics, group);
|
|
333
|
+
group.addTo(map);
|
|
334
|
+
|
|
335
|
+
const markerColor = getCompositionColor(config.composition);
|
|
336
|
+
const marker = L.marker(latlng, {
|
|
337
|
+
draggable: true,
|
|
338
|
+
icon: createMarkerIcon(markerColor),
|
|
339
|
+
}).addTo(map);
|
|
340
|
+
|
|
341
|
+
impacts.push({ id: Date.now().toString(), circles: group, marker, data: physics, config });
|
|
342
|
+
setupMarkerDragHandlers(marker, group, physics);
|
|
343
|
+
updateVerdict();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function addCircleLayers(latlng: L.LatLng, physics: ImpactData, group: L.LayerGroup) {
|
|
347
|
+
L.circle(latlng, {
|
|
348
|
+
radius: physics.craterDiameter / 2,
|
|
349
|
+
color: "#ef4444",
|
|
350
|
+
fillColor: "#ef4444",
|
|
351
|
+
fillOpacity: 0.6,
|
|
352
|
+
weight: 0,
|
|
353
|
+
}).addTo(group);
|
|
354
|
+
|
|
355
|
+
L.circle(latlng, {
|
|
356
|
+
radius: physics.thermalRadius,
|
|
357
|
+
color: "#f97316",
|
|
358
|
+
fillColor: "#f97316",
|
|
359
|
+
fillOpacity: 0.15,
|
|
360
|
+
weight: 1,
|
|
361
|
+
dashArray: "5 5",
|
|
362
|
+
}).addTo(group);
|
|
363
|
+
|
|
364
|
+
L.circle(latlng, {
|
|
365
|
+
radius: physics.shockwave1psi,
|
|
366
|
+
color: "#3b82f6",
|
|
367
|
+
fillColor: "#3b82f6",
|
|
368
|
+
fillOpacity: 0.05,
|
|
369
|
+
weight: 1,
|
|
370
|
+
}).addTo(group);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function renderCircles(latlng: L.LatLng, physics: ImpactData, group: L.LayerGroup) {
|
|
374
|
+
addCircleLayers(latlng, physics, group);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function setupGPS() {
|
|
378
|
+
const btn = document.getElementById("asteroid-gps-btn");
|
|
379
|
+
btn?.addEventListener("click", () => {
|
|
380
|
+
const gpsText = document.getElementById("asteroid-gps-text");
|
|
381
|
+
if (gpsText) gpsText.textContent = "Buscando...";
|
|
382
|
+
|
|
383
|
+
navigator.geolocation.getCurrentPosition(
|
|
384
|
+
(pos) => {
|
|
385
|
+
const ll = new L.LatLng(pos.coords.latitude, pos.coords.longitude);
|
|
386
|
+
if (gpsText) gpsText.textContent = "GPS Activo";
|
|
387
|
+
map.flyTo(ll, 9);
|
|
388
|
+
},
|
|
389
|
+
() => {
|
|
390
|
+
if (gpsText) gpsText.textContent = "Error GPS";
|
|
391
|
+
}
|
|
392
|
+
);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function updateVerdictUI(verdict: string) {
|
|
397
|
+
const label = document.getElementById("asteroid-verdict-label");
|
|
398
|
+
const value = document.getElementById("asteroid-verdict-value");
|
|
399
|
+
const icon = document.getElementById("asteroid-verdict-icon");
|
|
400
|
+
const container = document.getElementById("asteroid-verdict-container");
|
|
401
|
+
|
|
402
|
+
const c = VERDICT_CONFIGS[verdict];
|
|
403
|
+
if (value) value.textContent = c.text;
|
|
404
|
+
if (label) label.textContent = c.label;
|
|
405
|
+
if (container) container.className = `asteroid-verdict-container ${c.class}`;
|
|
406
|
+
|
|
407
|
+
const iconSource = document.getElementById(c.iconId);
|
|
408
|
+
if (icon && iconSource) icon.innerHTML = iconSource.innerHTML;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function updateVerdict() {
|
|
412
|
+
const pill = document.getElementById("asteroid-verdict-pill");
|
|
413
|
+
if (impacts.length === 0) {
|
|
414
|
+
pill?.classList.remove("active");
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
pill?.classList.add("active");
|
|
419
|
+
updateVerdictUI("safe");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function clearAll() {
|
|
423
|
+
impacts.forEach((i) => {
|
|
424
|
+
map.removeLayer(i.circles);
|
|
425
|
+
map.removeLayer(i.marker);
|
|
426
|
+
});
|
|
427
|
+
impacts.length = 0;
|
|
428
|
+
updateVerdict();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (document.readyState === "loading") {
|
|
432
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
433
|
+
} else {
|
|
434
|
+
init();
|
|
435
|
+
}
|
|
436
|
+
</script>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export interface VerdictConfig {
|
|
2
|
+
text: string;
|
|
3
|
+
label: string;
|
|
4
|
+
iconId: string;
|
|
5
|
+
class: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const VERDICT_CONFIGS: Record<string, VerdictConfig> = {
|
|
9
|
+
safe: {
|
|
10
|
+
text: "ZONA SEGURA",
|
|
11
|
+
label: "Sin amenazas",
|
|
12
|
+
iconId: "icon-safe",
|
|
13
|
+
class: "asteroid-verdict-safe",
|
|
14
|
+
},
|
|
15
|
+
shook: {
|
|
16
|
+
text: "ONDA DE CHOQUE",
|
|
17
|
+
label: "Daños estructurales",
|
|
18
|
+
iconId: "icon-shock",
|
|
19
|
+
class: "asteroid-verdict-shock",
|
|
20
|
+
},
|
|
21
|
+
burned: {
|
|
22
|
+
text: "RADIACIÓN TÉRMICA",
|
|
23
|
+
label: "Peligro extremo",
|
|
24
|
+
iconId: "icon-burn",
|
|
25
|
+
class: "asteroid-verdict-burned",
|
|
26
|
+
},
|
|
27
|
+
vaporized: {
|
|
28
|
+
text: "ZONA CERO",
|
|
29
|
+
label: "Impacto directo",
|
|
30
|
+
iconId: "icon-death",
|
|
31
|
+
class: "asteroid-verdict-vaporized",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export interface Labels {
|
|
36
|
+
searching: string;
|
|
37
|
+
gpsActive: string;
|
|
38
|
+
gpsError: string;
|
|
39
|
+
verdictSafe: string;
|
|
40
|
+
verdictSafeSub: string;
|
|
41
|
+
verdictShock: string;
|
|
42
|
+
verdictShockSub: string;
|
|
43
|
+
verdictBurned: string;
|
|
44
|
+
verdictBurnedSub: string;
|
|
45
|
+
verdictVaporized: string;
|
|
46
|
+
verdictVaporizedSub: string;
|
|
47
|
+
rock: string;
|
|
48
|
+
iron: string;
|
|
49
|
+
ice: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const DEFAULT_LABELS: Labels = {
|
|
53
|
+
searching: "Searching...",
|
|
54
|
+
gpsActive: "GPS Active",
|
|
55
|
+
gpsError: "GPS Error",
|
|
56
|
+
verdictSafe: "SAFE",
|
|
57
|
+
verdictSafeSub: "No threat",
|
|
58
|
+
verdictShock: "SHOCK",
|
|
59
|
+
verdictShockSub: "Damage",
|
|
60
|
+
verdictBurned: "BURNED",
|
|
61
|
+
verdictBurnedSub: "Extreme",
|
|
62
|
+
verdictVaporized: "ZERO",
|
|
63
|
+
verdictVaporizedSub: "Vaporized",
|
|
64
|
+
rock: "Rock",
|
|
65
|
+
iron: "Iron",
|
|
66
|
+
ice: "Ice",
|
|
67
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Composition } from "./logic/impactPhysics";
|
|
2
|
+
|
|
3
|
+
export function formatSize(diameter: number): string {
|
|
4
|
+
return diameter >= 1000 ? `${(diameter / 1000).toFixed(1)} km` : `${diameter}m`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getTypeLabel(composition: Composition): string {
|
|
8
|
+
if (composition === "ice") return "Hielo";
|
|
9
|
+
if (composition === "iron") return "Hierro";
|
|
10
|
+
return "Roca";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getCompositionColor(composition: Composition): string {
|
|
14
|
+
if (composition === "ice") return "#67e8f9";
|
|
15
|
+
if (composition === "iron") return "#1e293b";
|
|
16
|
+
return "#64748b";
|
|
17
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
2
|
+
|
|
3
|
+
export const content: ToolLocaleContent = {
|
|
4
|
+
slug: 'asteroid-impact-simulator',
|
|
5
|
+
title: 'Asteroid Impact Simulator: Apocalypse Calculator',
|
|
6
|
+
description: 'Simulate asteroid impacts with real physics. Calculate energy, crater, thermal radiation and shockwave. Would you survive Chicxulub?',
|
|
7
|
+
faqTitle: 'Frequently Asked Questions',
|
|
8
|
+
bibliographyTitle: 'Bibliography',
|
|
9
|
+
ui: {
|
|
10
|
+
copied: 'Copied',
|
|
11
|
+
noHistory: 'No history',
|
|
12
|
+
load: 'Load',
|
|
13
|
+
delete: 'Delete',
|
|
14
|
+
activateGPS: 'Activate GPS',
|
|
15
|
+
analysisLabel: 'Analysis',
|
|
16
|
+
dragToMap: 'DRAG TO MAP',
|
|
17
|
+
diameterLabel: 'Diameter',
|
|
18
|
+
velocityLabel: 'Velocity',
|
|
19
|
+
typeLabel: 'Type',
|
|
20
|
+
historicalData: 'Historical Data',
|
|
21
|
+
composition: 'Composition',
|
|
22
|
+
rock: 'Rock',
|
|
23
|
+
iron: 'Iron',
|
|
24
|
+
ice: 'Ice',
|
|
25
|
+
clearAll: 'Clear all',
|
|
26
|
+
searching: 'Searching...',
|
|
27
|
+
gpsActive: 'GPS Active',
|
|
28
|
+
gpsError: 'GPS Error',
|
|
29
|
+
verdictSafe: 'SAFE ZONE',
|
|
30
|
+
verdictSafeSub: 'No threats',
|
|
31
|
+
verdictShock: 'SHOCK WAVE',
|
|
32
|
+
verdictShockSub: 'Structural damage',
|
|
33
|
+
verdictBurned: 'THERMAL RADIATION',
|
|
34
|
+
verdictBurnedSub: 'Extreme danger',
|
|
35
|
+
verdictVaporized: 'GROUND ZERO',
|
|
36
|
+
verdictVaporizedSub: 'Direct impact',
|
|
37
|
+
},
|
|
38
|
+
seo: [
|
|
39
|
+
{
|
|
40
|
+
type: 'title',
|
|
41
|
+
text: 'When the Sky Falls: The Physics of Cosmic Apocalypse',
|
|
42
|
+
level: 2,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
type: 'paragraph',
|
|
46
|
+
html: 'Asteroids are not just space rocks. They are cosmic bullets traveling at 20 km/s, capable of releasing more energy than all nuclear weapons on the planet combined. This simulator translates abstract physics into tangible human consequences.',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: 'title',
|
|
50
|
+
text: 'The Doomsday Equation',
|
|
51
|
+
level: 3,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'paragraph',
|
|
55
|
+
html: 'It all starts with kinetic energy: <strong>E = ½mv²</strong>. A 100-meter asteroid traveling at 20 km/s releases approximately 0.5 megatons of TNT. For context, the Hiroshima bomb was 0.015 megatons.',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: 'paragraph',
|
|
59
|
+
html: 'But size scales exponentially. An object 10 times larger has 1,000 times more volume (and mass), releasing energy equivalent to <strong>500 megatons</strong>. Chicxulub, the dinosaur killer, released the equivalent of <strong>100 million megatons</strong>.',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
type: 'paragraph',
|
|
63
|
+
html: 'A 1 km asteroid striking Earth would release more energy than all of the planet\'s nuclear weapons detonated simultaneously.',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
type: 'title',
|
|
67
|
+
text: 'Anatomy of Destruction: Concentric Layers of Apocalypse',
|
|
68
|
+
level: 3,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
type: 'list',
|
|
72
|
+
items: [
|
|
73
|
+
'<strong>The Crater (Ground Zero):</strong> The crater diameter scales with E^0.3. A 1-megaton impact creates a ~1 km crater. Everything inside is instantly vaporized.',
|
|
74
|
+
'<strong>Thermal Radiation (The Flash):</strong> The fireball emits intense infrared radiation. At distances of E^0.41 km, clothing ignites and skin suffers third-degree burns.',
|
|
75
|
+
'<strong>Shockwave (The Hammer):</strong> The overpressure wave travels at supersonic speed. At 1 psi, glass shatters. At 5 psi, buildings collapse.',
|
|
76
|
+
'<strong>Earthquake (The Seismic Echo):</strong> The impact generates global seismic waves. Chicxulub caused a magnitude 11 earthquake, breaking the Richter scale.',
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: 'title',
|
|
81
|
+
text: 'Historical Impacts: Lessons from the Past',
|
|
82
|
+
level: 3,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: 'table',
|
|
86
|
+
headers: ['Location & Year', 'Size', 'Energy', 'Effects'],
|
|
87
|
+
rows: [
|
|
88
|
+
['Chelyabinsk, Russia (2013)', '20 meters', '500 kilotons', 'Shockwave at 100 km, 1,500 injured, broken windows'],
|
|
89
|
+
['Tunguska, Siberia (1908)', '50-60 meters', '10-15 megatons', 'Flattened 2,000 km² of forest, 80 million trees felled'],
|
|
90
|
+
['Chicxulub, Gulf of Mexico (66 M years)', '10 km', '100 million megatons', 'Extinction of 75% of life on Earth'],
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
faq: [
|
|
95
|
+
{
|
|
96
|
+
question: 'How is impact energy calculated?',
|
|
97
|
+
answer: 'Primary energy is kinetic: (1/2) * mass * velocity². We use realistic densities (e.g. 3000 kg/m³ for rocky asteroids) and typical atmospheric entry velocities (11 to 72 km/s). The resulting energy is measured in Megatons of TNT.',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
question: 'What is a thermal shockwave?',
|
|
101
|
+
answer: 'Upon entering the atmosphere, the asteroid compresses the air so violently that it creates a fireball a thousand times brighter than the Sun. The resulting thermal radiation can cause third-degree burns and set forests ablaze miles from the impact.',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
question: 'Why don\'t some asteroids create craters?',
|
|
105
|
+
answer: 'Smaller rocks (<50m) usually fragment and explode in the atmosphere due to air pressure (Airburst), as happened in Chelyabinsk. The energy is released as a powerful pressure shockwave, but it doesn\'t hit the ground as a solid body.',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
question: 'What is the real probability of an impact?',
|
|
109
|
+
answer: 'Small impacts (like Russia in 2013) happen every decade. Catastrophic impacts (Tunguska-style) every few centuries. A global extinction event like Chicxulub happens approximately every 100 million years.',
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
bibliography: [
|
|
113
|
+
{
|
|
114
|
+
name: 'Collins, G. S., et al. (2005). Earth Impact Effects Program: A Web-based computer program for calculating the regional environmental consequences of a meteoroid impact on Earth.',
|
|
115
|
+
url: 'https://impact.ese.ic.ac.uk/ImpactEarth/',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
name: 'Toon, O. B., et al. (1997). Environmental perturbations caused by the impacts of asteroids and comets. Reviews of Geophysics.',
|
|
119
|
+
url: 'https://agupubs.onlinelibrary.wiley.com/doi/abs/10.1029/96RG03038',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'Chapman, C. R., & Morrison, D. (1994). Impacts on the Earth by asteroids and comets: assessing the hazard. Nature.',
|
|
123
|
+
url: 'https://www.nature.com/articles/367033a0',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'Schulte, P., et al. (2010). The Chicxulub Asteroid Impact and Mass Extinction at the Cretaceous-Paleogene Boundary. Science.',
|
|
127
|
+
url: 'https://www.science.org/doi/10.1126/science.1177265',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'Brown, P., et al. (2013). A 500-kiloton airburst over Chelyabinsk and an enhanced hazard from small impactors. Nature.',
|
|
131
|
+
url: 'https://www.nature.com/articles/nature12741',
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
howTo: [
|
|
135
|
+
{
|
|
136
|
+
name: 'Choose projectile size',
|
|
137
|
+
text: 'Enter the asteroid diameter, from a small 10-meter meteorite to a 10-kilometer planet killer.',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'Configure velocity and angle',
|
|
141
|
+
text: 'Adjust the approach velocity and entry angle (45° is the statistically most likely value).',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'Define asteroid nature',
|
|
145
|
+
text: 'Select whether the asteroid is made of rock, iron, or ice to calculate the crater depth correctly.',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: 'Analyze survival verdict',
|
|
149
|
+
text: 'Drag the asteroid onto the map and indicate how far away you are to see the effects of radiation, earthquake, and shockwave.',
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
schemas: [],
|
|
153
|
+
};
|