@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.
Files changed (73) hide show
  1. package/package.json +64 -0
  2. package/src/category/i18n/en.ts +97 -0
  3. package/src/category/i18n/es.ts +97 -0
  4. package/src/category/i18n/fr.ts +96 -0
  5. package/src/category/index.ts +17 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +24 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/seo_length.test.ts +22 -0
  21. package/src/tests/tool_validation.test.ts +17 -0
  22. package/src/tool/asteroid-impact/AsteroidImpact.css +799 -0
  23. package/src/tool/asteroid-impact/bibliography.astro +14 -0
  24. package/src/tool/asteroid-impact/component.astro +436 -0
  25. package/src/tool/asteroid-impact/constants.ts +67 -0
  26. package/src/tool/asteroid-impact/helpers.ts +17 -0
  27. package/src/tool/asteroid-impact/i18n/en.ts +153 -0
  28. package/src/tool/asteroid-impact/i18n/es.ts +153 -0
  29. package/src/tool/asteroid-impact/i18n/fr.ts +153 -0
  30. package/src/tool/asteroid-impact/index.ts +24 -0
  31. package/src/tool/asteroid-impact/logic/impactPhysics.ts +86 -0
  32. package/src/tool/asteroid-impact/script.ts +256 -0
  33. package/src/tool/asteroid-impact/seo.astro +15 -0
  34. package/src/tool/asteroid-impact/ui-helpers.ts +56 -0
  35. package/src/tool/asteroid-impact/ui.ts +69 -0
  36. package/src/tool/asteroid-impact/utils.ts +17 -0
  37. package/src/tool/cellular-renewal/CellularRenewal.css +1 -0
  38. package/src/tool/cellular-renewal/bibliography.astro +14 -0
  39. package/src/tool/cellular-renewal/component.astro +387 -0
  40. package/src/tool/cellular-renewal/i18n/en.ts +170 -0
  41. package/src/tool/cellular-renewal/i18n/es.ts +170 -0
  42. package/src/tool/cellular-renewal/i18n/fr.ts +170 -0
  43. package/src/tool/cellular-renewal/index.ts +24 -0
  44. package/src/tool/cellular-renewal/logic/CellularRenewalEngine.ts +50 -0
  45. package/src/tool/cellular-renewal/seo.astro +14 -0
  46. package/src/tool/colony-counter/ColonyCounter.css +473 -0
  47. package/src/tool/colony-counter/bibliography.astro +14 -0
  48. package/src/tool/colony-counter/component.astro +358 -0
  49. package/src/tool/colony-counter/i18n/en.ts +151 -0
  50. package/src/tool/colony-counter/i18n/es.ts +151 -0
  51. package/src/tool/colony-counter/i18n/fr.ts +151 -0
  52. package/src/tool/colony-counter/index.ts +27 -0
  53. package/src/tool/colony-counter/seo.astro +15 -0
  54. package/src/tool/microwave-detector/MicrowaveDetector.css +122 -0
  55. package/src/tool/microwave-detector/bibliography.astro +14 -0
  56. package/src/tool/microwave-detector/component.astro +650 -0
  57. package/src/tool/microwave-detector/i18n/en.ts +155 -0
  58. package/src/tool/microwave-detector/i18n/es.ts +155 -0
  59. package/src/tool/microwave-detector/i18n/fr.ts +155 -0
  60. package/src/tool/microwave-detector/index.ts +24 -0
  61. package/src/tool/microwave-detector/logic/MicrowaveEngine.ts +89 -0
  62. package/src/tool/microwave-detector/seo.astro +14 -0
  63. package/src/tool/simulation-probability/SimulationProbability.css +1 -0
  64. package/src/tool/simulation-probability/bibliography.astro +14 -0
  65. package/src/tool/simulation-probability/component.astro +348 -0
  66. package/src/tool/simulation-probability/i18n/en.ts +184 -0
  67. package/src/tool/simulation-probability/i18n/es.ts +184 -0
  68. package/src/tool/simulation-probability/i18n/fr.ts +184 -0
  69. package/src/tool/simulation-probability/index.ts +24 -0
  70. package/src/tool/simulation-probability/logic/SimulationEngine.ts +42 -0
  71. package/src/tool/simulation-probability/seo.astro +14 -0
  72. package/src/tools.ts +15 -0
  73. package/src/types.ts +72 -0
@@ -0,0 +1,153 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+
3
+ export const content: ToolLocaleContent = {
4
+ slug: 'simulador-impacto-asteroide',
5
+ title: 'Simulador de Impacto de Asteroides: Calculadora de Apocalipsis',
6
+ description: 'Simula el impacto de asteroides con física real. Calcula energía, cráter, radiación térmica y onda de choque. ¿Sobrevivirías a Chicxulub?',
7
+ faqTitle: 'Preguntas Frecuentes',
8
+ bibliographyTitle: 'Bibliografía',
9
+ ui: {
10
+ copied: 'Copiado',
11
+ noHistory: 'Sin historial',
12
+ load: 'Cargar',
13
+ delete: 'Eliminar',
14
+ activateGPS: 'Activar GPS',
15
+ analysisLabel: 'Análisis',
16
+ dragToMap: 'ARRASTRA AL MAPA',
17
+ diameterLabel: 'Diámetro',
18
+ velocityLabel: 'Velocidad',
19
+ typeLabel: 'Tipo',
20
+ historicalData: 'Datos Históricos',
21
+ composition: 'Composición',
22
+ rock: 'Roca',
23
+ iron: 'Hierro',
24
+ ice: 'Hielo',
25
+ clearAll: 'Limpiar todo',
26
+ searching: 'Buscando...',
27
+ gpsActive: 'GPS Activo',
28
+ gpsError: 'Error GPS',
29
+ verdictSafe: 'ZONA SEGURA',
30
+ verdictSafeSub: 'Sin amenazas',
31
+ verdictShock: 'ONDA DE CHOQUE',
32
+ verdictShockSub: 'Daños estructurales',
33
+ verdictBurned: 'RADIACIÓN TÉRMICA',
34
+ verdictBurnedSub: 'Peligro extremo',
35
+ verdictVaporized: 'ZONA CERO',
36
+ verdictVaporizedSub: 'Impacto directo',
37
+ },
38
+ seo: [
39
+ {
40
+ type: 'title',
41
+ text: 'Cuando el Cielo Cae: La Física del Apocalipsis Cósmico',
42
+ level: 2,
43
+ },
44
+ {
45
+ type: 'paragraph',
46
+ html: 'Los asteroides no son solo rocas espaciales. Son balas cósmicas viajando a 20 km/s, capaces de liberar más energía que todas las armas nucleares del planeta combinadas. Este simulador traduce la física abstracta en consecuencias humanas tangibles.',
47
+ },
48
+ {
49
+ type: 'title',
50
+ text: 'La Ecuación del Fin del Mundo',
51
+ level: 3,
52
+ },
53
+ {
54
+ type: 'paragraph',
55
+ html: 'Todo comienza con la energía cinética: <strong>E = ½mv²</strong>. Un asteroide de 100 metros viajando a 20 km/s libera aproximadamente 0.5 megatones de TNT. Para contexto, la bomba de Hiroshima fue de 0.015 megatones.',
56
+ },
57
+ {
58
+ type: 'paragraph',
59
+ html: 'Pero el tamaño escala exponencialmente. Un objeto 10 veces más grande tiene 1,000 veces más volumen (y masa), liberando energía equivalente a <strong>500 megatones</strong>. Chicxulub, el asesino de dinosaurios, liberó el equivalente a <strong>100 millones de megatones</strong>.',
60
+ },
61
+ {
62
+ type: 'paragraph',
63
+ html: 'Un asteroide de 1 km impactando la Tierra liberaría más energía que todas las armas nucleares del planeta detonadas simultáneamente.',
64
+ },
65
+ {
66
+ type: 'title',
67
+ text: 'Anatomía de la Destrucción: Las Capas Concéntricas del Apocalipsis',
68
+ level: 3,
69
+ },
70
+ {
71
+ type: 'list',
72
+ items: [
73
+ '<strong>El Cráter (Zona Cero):</strong> El diámetro del cráter escala con E^0.3. Un impacto de 1 megatón crea un cráter de ~1 km. Todo dentro es vaporizado instantáneamente.',
74
+ '<strong>Radiación Térmica (El Flash):</strong> La bola de fuego emite radiación infrarroja intensa. A distancias de E^0.41 km, la ropa se incendia y la piel sufre quemaduras de tercer grado.',
75
+ '<strong>Onda de Choque (El Martillo):</strong> La onda de sobrepresión viaja a velocidad supersónica. A 1 psi, los cristales estallan. A 5 psi, los edificios colapsan.',
76
+ '<strong>Terremoto (El Eco Sísmico):</strong> El impacto genera ondas sísmicas globales. Chicxulub causó un terremoto de magnitud 11, rompiendo la escala de Richter.',
77
+ ],
78
+ },
79
+ {
80
+ type: 'title',
81
+ text: 'Impactos Históricos: Lecciones del Pasado',
82
+ level: 3,
83
+ },
84
+ {
85
+ type: 'table',
86
+ headers: ['Ubicación & Año', 'Tamaño', 'Energía', 'Efectos'],
87
+ rows: [
88
+ ['Chelyabinsk, Rusia (2013)', '20 metros', '500 kilotones', 'Onda de choque a 100 km, 1,500 heridos, ventanas rotas'],
89
+ ['Tunguska, Siberia (1908)', '50-60 metros', '10-15 megatones', 'Arrasó 2,000 km² de bosque, 80 millones de árboles derribados'],
90
+ ['Chicxulub, Golfo de México (66 M años)', '10 km', '100 millones de megatones', 'Extinción del 75% de la vida en la Tierra'],
91
+ ],
92
+ },
93
+ ],
94
+ faq: [
95
+ {
96
+ question: '¿Cómo se calcula la energía de un impacto?',
97
+ answer: 'La energía principal es cinética: (1/2) * masa * velocidad². Usamos densidades realistas (ej. 3000 kg/m³ para asteroides rocosos) y velocidades típicas de entrada atmosférica (11 a 72 km/s). La energía resultante se mide en Megatones de TNT.',
98
+ },
99
+ {
100
+ question: '¿Qué es la onda de choque térmica?',
101
+ answer: 'Al entrar en la atmósfera, el asteroide comprime el aire de forma tan violenta que crea una bola de fuego mil veces más brillante que el Sol. La radiación térmica resultante puede causar quemaduras de tercer grado e incendiar bosques a kilómetros del impacto.',
102
+ },
103
+ {
104
+ question: '¿Por qué algunos asteroides no crean cráter?',
105
+ answer: 'Las rocas más pequeñas (<50m) suelen fragmentarse y explotar en la atmósfera debido a la presión del aire (Airburst), como ocurrió en Cheliábinsk. La energía se libera como una potente onda de choque de presión, pero no llega a tocar suelo como un cuerpo sólido.',
106
+ },
107
+ {
108
+ question: '¿Qué probabilidad hay de un impacto real?',
109
+ answer: 'Impactos pequeños (como el de Rusia en 2013) ocurren cada década. Impactos catastróficos (tipo Tunguska) cada pocos siglos. Un evento de extinción global como Chicxulub ocurre aproximadamente cada 100 millones de años.',
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: 'Elegir el tamaño del proyectil',
137
+ text: 'Introduce el diámetro del asteroide. Desde un pequeño meteorito de 10 metros hasta un destructor de planetas de 10 kilómetros.',
138
+ },
139
+ {
140
+ name: 'Configurar velocidad y ángulo',
141
+ text: 'Ajusta la velocidad de aproximación y el ángulo de entrada (45° es el valor estadístico más probable).',
142
+ },
143
+ {
144
+ name: 'Definir naturaleza del asteroide',
145
+ text: 'Selecciona si el asteroide es de roca, hierro o hielo para calcular la profundidad del cráter correctamente.',
146
+ },
147
+ {
148
+ name: 'Analizar el veredicto de supervivencia',
149
+ text: 'Arrastra el asteroide al mapa e indica a qué distancia te encuentras para ver los efectos de la radiación, el terremoto, la onda de choque.',
150
+ },
151
+ ],
152
+ schemas: [],
153
+ };
@@ -0,0 +1,153 @@
1
+ import type { ToolLocaleContent } from '../../../types';
2
+
3
+ export const content: ToolLocaleContent = {
4
+ slug: 'simulateur-impact-asteroide',
5
+ title: 'Simulateur d\'Impact d\'Astéroïde : Calculateur d\'Apocalypse',
6
+ description: 'Simulez l\'impact d\'astéroïdes avec la physique réelle. Calculez l\'énergie, le cratère, les radiations thermiques et l\'onde de choc. Survivriez-vous à Chicxulub ?',
7
+ faqTitle: 'Questions Fréquemment Posées',
8
+ bibliographyTitle: 'Bibliographie',
9
+ ui: {
10
+ copied: 'Copié',
11
+ noHistory: 'Pas d\'historique',
12
+ load: 'Charger',
13
+ delete: 'Supprimer',
14
+ activateGPS: 'Activer GPS',
15
+ analysisLabel: 'Analyse',
16
+ dragToMap: 'FAITES GLISSER SUR LA CARTE',
17
+ diameterLabel: 'Diamètre',
18
+ velocityLabel: 'Vitesse',
19
+ typeLabel: 'Type',
20
+ historicalData: 'Données Historiques',
21
+ composition: 'Composition',
22
+ rock: 'Roche',
23
+ iron: 'Fer',
24
+ ice: 'Glace',
25
+ clearAll: 'Tout effacer',
26
+ searching: 'Recherche...',
27
+ gpsActive: 'GPS Actif',
28
+ gpsError: 'Erreur GPS',
29
+ verdictSafe: 'ZONE SÛRE',
30
+ verdictSafeSub: 'Aucune menace',
31
+ verdictShock: 'ONDE DE CHOC',
32
+ verdictShockSub: 'Dommages structurels',
33
+ verdictBurned: 'RADIATIONS THERMIQUES',
34
+ verdictBurnedSub: 'Danger extrême',
35
+ verdictVaporized: 'ZONE ZÉRO',
36
+ verdictVaporizedSub: 'Impact direct',
37
+ },
38
+ seo: [
39
+ {
40
+ type: 'title',
41
+ text: 'Quand le Ciel Tombe : La Physique de l\'Apocalypse Cosmique',
42
+ level: 2,
43
+ },
44
+ {
45
+ type: 'paragraph',
46
+ html: 'Les astéroïdes ne sont pas de simples rochers de l\'espace. Ce sont des balles cosmiques voyageant à 20 km/s, capables de libérer plus d\'énergie que toutes les armes nucléaires de la planète réunies. Ce simulateur traduit la physique abstraite en conséquences humaines tangibles.',
47
+ },
48
+ {
49
+ type: 'title',
50
+ text: 'L\'Équation du Jugement Dernier',
51
+ level: 3,
52
+ },
53
+ {
54
+ type: 'paragraph',
55
+ html: 'Tout commence par l\'énergie cinétique : <strong>E = ½mv²</strong>. Un astéroïde de 100 mètres voyageant à 20 km/s libère environ 0,5 mégatonne de TNT. Pour comparaison, la bombe d\'Hiroshima était de 0,015 mégatonne.',
56
+ },
57
+ {
58
+ type: 'paragraph',
59
+ html: 'Mais la taille augmente de façon exponentielle. Un objet 10 fois plus grand a 1 000 fois plus de volume (et de masse), libérant une énergie équivalente à <strong>500 mégatonnes</strong>. Chicxulub, le tueur de dinosaures, a libéré l\'équivalent de <strong>100 millions de mégatonnes</strong>.',
60
+ },
61
+ {
62
+ type: 'paragraph',
63
+ html: 'Un astéroïde d\'un kilomètre frappant la Terre libérerait plus d\'énergie que toutes les armes nucléaires du monde détonées simultanément.',
64
+ },
65
+ {
66
+ type: 'title',
67
+ text: 'Anatomie de la Destruction : Les Couches Concentriques de l\'Apocalypse',
68
+ level: 3,
69
+ },
70
+ {
71
+ type: 'list',
72
+ items: [
73
+ '<strong>Le Cratère (Zone Zéro) :</strong> Le diamètre du cratère augmente selon E^0,3. Un impact d\'une mégatonne crée un cratère de ~1 km. Tout à l\'intérieur est instantanément vaporisé.',
74
+ '<strong>Radiations Thermiques (Le Flash) :</strong> La boule de feu émet un rayonnement infrarouge intense. À une distance de E^0,41 km, les vêtements s\'enflamment et la peau subit des brûlures au troisième degré.',
75
+ '<strong>Onde de Choc (Le Marteau) :</strong> L\'onde de surpression voyage à une vitesse supersonique. À 1 psi, les fenêtres explosent. À 5 psi, les bâtiments s\'effondrent.',
76
+ '<strong>Tremblement de Terre (L\'Écho Sismique) :</strong> L\'impact génère des ondes sismiques mondiales. Chicxulub a provoqué un séisme de magnitude 11, brisant l\'échelle de Richter.',
77
+ ],
78
+ },
79
+ {
80
+ type: 'title',
81
+ text: 'Impacts Historiques : Les Leçons du Passé',
82
+ level: 3,
83
+ },
84
+ {
85
+ type: 'table',
86
+ headers: ['Lieu & Année', 'Taille', 'Énergie', 'Effets'],
87
+ rows: [
88
+ ['Chelyabinsk, Russie (2013)', '20 mètres', '500 kilotonnes', 'Onde de choc à 100 km, 1 500 blessés, vitres brisées'],
89
+ ['Tunguska, Sibérie (1908)', '50-60 mètres', '10-15 mégatonnes', '2 000 km² de forêt rasés, 80 millions d\'arbres abattus'],
90
+ ['Chicxulub, Golfe du Mexique (66 M années)', '10 km', '100 millions de mégatonnes', 'Extinction de 75 % de la vie sur Terre'],
91
+ ],
92
+ },
93
+ ],
94
+ faq: [
95
+ {
96
+ question: 'Comment l\'énergie d\'un impact est-elle calculée ?',
97
+ answer: 'L\'énergie principale est cinétique : (1/2) * masse * vitesse². Nous utilisons des densités réalistes (ex. 3000 kg/m³ pour les astéroïdes rocheux) et des vitesses d\'entrée atmosphérique typiques (11 à 72 km/s). L\'énergie résultante est mesurée en mégatonnes de TNT.',
98
+ },
99
+ {
100
+ question: 'Qu\'est-ce que l\'onde de choc thermique ?',
101
+ answer: 'En entrant dans l\'atmosphère, l\'astéroïde comprime l\'air si violemment qu\'il crée une boule de feu mille fois plus brillante que le Soleil. Le rayonnement thermique qui en résulte peut causer des brûlures au troisième degré et enflammer des forêts à des kilomètres de l\'impact.',
102
+ },
103
+ {
104
+ question: 'Pourquoi certains astéroïdes ne créent-ils pas de cratère ?',
105
+ answer: 'Les roches plus petites (<50m) se fragmentent et explosent généralement dans l\'atmosphère à cause de la pression de l\'air (Explosion aérienne), comme à Chelyabinsk. L\'énergie est libérée sous forme d\'une puissante onde de choc, mais elle ne touche pas le sol en un seul corps solide.',
106
+ },
107
+ {
108
+ question: 'Quelle est la probabilité réelle d\'un impact ?',
109
+ answer: 'Les petits impacts (comme en Russie en 2013) se produisent chaque décennie. Les impacts catastrophiques (type Tunguska) tous les quelques siècles. Un événement d\'extinction mondiale comme Chicxulub se produit environ tous les 100 millions d\'années.',
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: 'Choisir la taille du projectile',
137
+ text: 'Entrez le diamètre de l\'astéroïde. D\'un petit météore de 10 mètres à un destructeur de planète de 10 kilomètres.',
138
+ },
139
+ {
140
+ name: 'Régler la vitesse et l\'angle',
141
+ text: 'Ajustez la vitesse d\'approche et l\'angle d\'entrée (45° est la valeur statistique la plus probable).',
142
+ },
143
+ {
144
+ name: 'Définir la nature de l\'astéroïde',
145
+ text: 'Sélectionnez si l\'astéroïde est en roche, en fer ou en glace pour calculer correctement la profondeur du cratère.',
146
+ },
147
+ {
148
+ name: 'Analyser le verdict de survie',
149
+ text: 'Faites glisser l\'astéroïde sur la carte et indiquez à quelle distance vous vous trouvez pour voir les effets des radiations, du séisme et de l\'onde de choc.',
150
+ },
151
+ ],
152
+ schemas: [],
153
+ };
@@ -0,0 +1,24 @@
1
+ import type { ScienceToolEntry, ToolDefinition } from '../../types';
2
+ import AsteroidImpactComponent from './component.astro';
3
+ import AsteroidImpactSEO from './seo.astro';
4
+ import AsteroidImpactBibliography from './bibliography.astro';
5
+
6
+ export const asteroidImpact: ScienceToolEntry = {
7
+ slug: 'simulador-impacto-asteroide',
8
+ icons: {
9
+ bg: 'mdi:meteor',
10
+ fg: 'mdi:earth',
11
+ },
12
+ i18n: {
13
+ es: () => import('./i18n/es').then((m) => m.content),
14
+ en: () => import('./i18n/en').then((m) => m.content),
15
+ fr: () => import('./i18n/fr').then((m) => m.content),
16
+ },
17
+ };
18
+
19
+ export const ASTEROID_IMPACT_TOOL: ToolDefinition = {
20
+ entry: asteroidImpact,
21
+ Component: AsteroidImpactComponent,
22
+ SEOComponent: AsteroidImpactSEO,
23
+ BibliographyComponent: AsteroidImpactBibliography,
24
+ };
@@ -0,0 +1,86 @@
1
+ export type Composition = "ice" | "rock" | "iron";
2
+
3
+ export interface ImpactInputs {
4
+ diameter: number;
5
+ distance: number;
6
+ composition: Composition;
7
+ velocity: number;
8
+ }
9
+
10
+ export interface ImpactResults {
11
+ kineticEnergy: number;
12
+ megatons: number;
13
+ craterDiameter: number;
14
+ craterDepth: number;
15
+ thermalRadius: number;
16
+ shockwave1psi: number;
17
+ shockwave5psi: number;
18
+ seismicMagnitude: number;
19
+ airburst: boolean;
20
+ survivalVerdict: "vaporized" | "severe-burns" | "ruptured-eardrums" | "safe";
21
+ }
22
+
23
+ const DENSITIES: Record<Composition, number> = {
24
+ ice: 917,
25
+ rock: 2600,
26
+ iron: 7800,
27
+ };
28
+
29
+ const MEGATONS_PER_JOULE = 2.39e-16;
30
+
31
+ export class ImpactPhysics {
32
+ private static getMass(diameter: number, composition: Composition): number {
33
+ const radius = diameter / 2;
34
+ const volume = (4 / 3) * Math.PI * Math.pow(radius, 3);
35
+ const density = DENSITIES[composition];
36
+ return volume * density;
37
+ }
38
+
39
+ private static getEnergy(mass: number, velocity: number): number {
40
+ const kineticEnergy = 0.5 * mass * Math.pow(velocity, 2);
41
+ return kineticEnergy * MEGATONS_PER_JOULE;
42
+ }
43
+
44
+ private static getRadius(megatons: number, airburst: boolean): number {
45
+ return airburst ? 0 : Math.pow(megatons, 0.3) * 1000;
46
+ }
47
+
48
+ private static getSurvivalVerdict(
49
+ distance: number,
50
+ craterDiameter: number,
51
+ thermalRadius: number,
52
+ shockwave5psi: number
53
+ ): ImpactResults["survivalVerdict"] {
54
+ if (distance < craterDiameter / 2) return "vaporized";
55
+ if (distance < thermalRadius) return "severe-burns";
56
+ if (distance < shockwave5psi) return "ruptured-eardrums";
57
+ return "safe";
58
+ }
59
+
60
+ static calculate(inputs: ImpactInputs): ImpactResults {
61
+ const { diameter, distance, composition, velocity } = inputs;
62
+ const mass = this.getMass(diameter, composition);
63
+ const megatons = this.getEnergy(mass, velocity);
64
+ const airburst = diameter < 50;
65
+ const craterDiameter = this.getRadius(megatons, airburst);
66
+ const craterDepth = craterDiameter * 0.3;
67
+ const thermalRadius = Math.pow(megatons, 0.41) * 1000;
68
+ const shockwave1psi = Math.pow(megatons, 0.33) * 2000;
69
+ const shockwave5psi = Math.pow(megatons, 0.33) * 1000;
70
+ const seismicMagnitude = Math.min(0.67 * Math.log10(megatons) + 3.87, 10);
71
+ const survivalVerdict = this.getSurvivalVerdict(distance, craterDiameter, thermalRadius, shockwave5psi);
72
+
73
+ return {
74
+ kineticEnergy: 0.5 * mass * Math.pow(velocity, 2),
75
+ megatons,
76
+ craterDiameter,
77
+ craterDepth,
78
+ thermalRadius,
79
+ shockwave1psi,
80
+ shockwave5psi,
81
+ seismicMagnitude,
82
+ airburst,
83
+ survivalVerdict,
84
+ };
85
+ }
86
+ }
@@ -0,0 +1,256 @@
1
+ import L from "leaflet";
2
+ import { ImpactPhysics, type Composition, type ImpactResults } from "./logic/impactPhysics";
3
+ import { getCompositionColor } from "./helpers";
4
+ import { getConfig, updateUI } from "./ui";
5
+ import { VERDICT_CONFIGS, DEFAULT_LABELS, type Labels } from "./constants";
6
+
7
+ let map: L.Map;
8
+
9
+ interface Impact {
10
+ id: string;
11
+ circles: L.LayerGroup;
12
+ marker: L.Marker;
13
+ data: ImpactResults;
14
+ config: typeof config;
15
+ }
16
+
17
+ const impacts: Impact[] = [];
18
+ const config = getConfig();
19
+
20
+ function setupControls() {
21
+ const sizeInput = document.getElementById("input-size") as HTMLInputElement;
22
+ const velInput = document.getElementById("input-velocity") as HTMLInputElement;
23
+ const matBtns = document.querySelectorAll(".asteroid-material-btn");
24
+ const presetBtns = document.querySelectorAll(".asteroid-preset-btn");
25
+ const clearBtn = document.getElementById("clear-btn");
26
+
27
+ sizeInput?.addEventListener("input", (e) => {
28
+ config.diameter = parseInt((e.target as HTMLInputElement).value);
29
+ updateUI();
30
+ });
31
+
32
+ velInput?.addEventListener("input", (e) => {
33
+ config.velocity = parseInt((e.target as HTMLInputElement).value);
34
+ updateUI();
35
+ });
36
+
37
+ matBtns.forEach((btn) => {
38
+ btn.addEventListener("click", () => {
39
+ config.composition = btn.getAttribute("data-type") as Composition;
40
+ updateUI();
41
+ });
42
+ });
43
+
44
+ presetBtns.forEach((btn) => {
45
+ btn.addEventListener("click", () => {
46
+ config.diameter = parseInt(btn.getAttribute("data-dia")!);
47
+ config.velocity = parseInt(btn.getAttribute("data-vel")!);
48
+ config.composition = btn.getAttribute("data-comp") as Composition;
49
+ updateUI();
50
+ });
51
+ });
52
+
53
+ clearBtn?.addEventListener("click", clearAll);
54
+ }
55
+
56
+ function setupGhostDrag() {
57
+ const source = document.getElementById("drag-source-desktop");
58
+ if (!source) return;
59
+
60
+ const start = (e: MouseEvent | TouchEvent) => {
61
+ e.preventDefault();
62
+ const targetOverlay = document.getElementById("asteroid-map-target-overlay");
63
+ if (targetOverlay) targetOverlay.classList.add("active");
64
+
65
+ const onEnd = (endEvent: MouseEvent | TouchEvent) => {
66
+ if (targetOverlay) targetOverlay.classList.remove("active");
67
+
68
+ const clientX = (endEvent as MouseEvent).clientX || (endEvent as TouchEvent).changedTouches[0].clientX;
69
+ const clientY = (endEvent as MouseEvent).clientY || (endEvent as TouchEvent).changedTouches[0].clientY;
70
+
71
+ const point = map.mouseEventToContainerPoint({ clientX, clientY } as MouseEvent);
72
+ const latlng = map.containerPointToLatLng(point);
73
+ spawnImpact(latlng);
74
+
75
+ window.removeEventListener("mousemove", onEnd);
76
+ window.removeEventListener("mouseup", onEnd);
77
+ window.removeEventListener("touchmove", onEnd);
78
+ window.removeEventListener("touchend", onEnd);
79
+ };
80
+
81
+ window.addEventListener("mousemove", onEnd);
82
+ window.addEventListener("mouseup", onEnd);
83
+ window.addEventListener("touchmove", onEnd, { passive: false });
84
+ window.addEventListener("touchend", onEnd);
85
+ };
86
+
87
+ source.addEventListener("mousedown", start);
88
+ source.addEventListener("touchstart", start);
89
+ }
90
+
91
+ function renderCircles(latlng: L.LatLng, physics: ImpactResults, group: L.LayerGroup) {
92
+ L.circle(latlng, {
93
+ radius: physics.craterDiameter / 2,
94
+ color: "#ef4444",
95
+ fillColor: "#ef4444",
96
+ fillOpacity: 0.6,
97
+ weight: 0,
98
+ }).addTo(group);
99
+
100
+ L.circle(latlng, {
101
+ radius: physics.thermalRadius,
102
+ color: "#f97316",
103
+ fillColor: "#f97316",
104
+ fillOpacity: 0.15,
105
+ weight: 1,
106
+ dashArray: "5 5",
107
+ }).addTo(group);
108
+
109
+ L.circle(latlng, {
110
+ radius: physics.shockwave1psi,
111
+ color: "#3b82f6",
112
+ fillColor: "#3b82f6",
113
+ fillOpacity: 0.05,
114
+ weight: 1,
115
+ }).addTo(group);
116
+ }
117
+
118
+ function setupMarkerEvents(marker: L.Marker, group: L.LayerGroup, physics: ImpactResults) {
119
+ marker.on("dragstart", () => {
120
+ map.dragging.disable();
121
+ });
122
+
123
+ marker.on("drag", (e: L.LeafletEvent) => {
124
+ const newPos = (e.target as L.Marker).getLatLng();
125
+ group.clearLayers();
126
+ renderCircles(newPos, physics, group);
127
+ updateVerdict();
128
+ });
129
+
130
+ marker.on("dragend", (e: L.LeafletEvent) => {
131
+ map.dragging.enable();
132
+ const newPos = (e.target as L.Marker).getLatLng();
133
+ group.clearLayers();
134
+ renderCircles(newPos, physics, group);
135
+ updateVerdict();
136
+ });
137
+ }
138
+
139
+ function spawnImpact(latlng: L.LatLng) {
140
+ const physics = ImpactPhysics.calculate({
141
+ diameter: config.diameter,
142
+ velocity: config.velocity * 1000,
143
+ composition: config.composition,
144
+ distance: 0,
145
+ });
146
+
147
+ const group = L.layerGroup();
148
+ renderCircles(latlng, physics, group);
149
+ group.addTo(map);
150
+
151
+ const color = getCompositionColor(config.composition);
152
+ const marker = L.marker(latlng, {
153
+ draggable: true,
154
+ icon: L.divIcon({
155
+ className: "asteroid-marker",
156
+ iconSize: [32, 32],
157
+ iconAnchor: [16, 16],
158
+ 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>`,
159
+ }),
160
+ }).addTo(map);
161
+
162
+ impacts.push({ id: Date.now().toString(), circles: group, marker, data: physics, config });
163
+ setupMarkerEvents(marker, group, physics);
164
+ updateVerdict();
165
+ }
166
+
167
+ function setupGPS() {
168
+ const btn = document.getElementById("asteroid-gps-btn");
169
+ btn?.addEventListener("click", () => {
170
+ const gpsText = document.getElementById("asteroid-gps-text");
171
+ if (gpsText) gpsText.textContent = "Buscando...";
172
+
173
+ navigator.geolocation.getCurrentPosition(
174
+ (pos) => {
175
+ const ll = new L.LatLng(pos.coords.latitude, pos.coords.longitude);
176
+ if (gpsText) gpsText.textContent = "GPS Activo";
177
+ map.flyTo(ll, 9);
178
+ },
179
+ () => {
180
+ if (gpsText) gpsText.textContent = "Error GPS";
181
+ }
182
+ );
183
+ });
184
+ }
185
+
186
+ function updateVerdictUI(verdictConfig: VerdictConfig) {
187
+ const label = document.getElementById("asteroid-verdict-label");
188
+ const value = document.getElementById("asteroid-verdict-value");
189
+ const icon = document.getElementById("asteroid-verdict-icon");
190
+ const container = document.getElementById("asteroid-verdict-container");
191
+
192
+ if (value) value.textContent = verdictConfig.text;
193
+ if (label) label.textContent = verdictConfig.label;
194
+ if (container) container.className = `asteroid-verdict-container ${verdictConfig.class}`;
195
+
196
+ const iconSource = document.getElementById(verdictConfig.iconId);
197
+ if (icon && iconSource) icon.innerHTML = iconSource.innerHTML;
198
+ }
199
+
200
+ function updateVerdict() {
201
+ const pill = document.getElementById("asteroid-verdict-pill");
202
+ if (impacts.length === 0) {
203
+ pill?.classList.remove("active");
204
+ } else {
205
+ pill?.classList.add("active");
206
+ updateVerdictUI(VERDICT_CONFIGS.safe);
207
+ }
208
+ }
209
+
210
+ function clearAll() {
211
+ impacts.forEach((i) => {
212
+ map.removeLayer(i.circles);
213
+ map.removeLayer(i.marker);
214
+ });
215
+ impacts.length = 0;
216
+ updateVerdict();
217
+ }
218
+
219
+ function init() {
220
+ map = L.map("asteroid-game-map", {
221
+ zoomControl: false,
222
+ attributionControl: false,
223
+ dragging: true,
224
+ }).setView([40.4168, -3.7038], 6);
225
+
226
+ L.tileLayer("https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.webp", {
227
+ subdomains: "abcd",
228
+ maxZoom: 20,
229
+ attribution: "&copy; OpenStreetMap &copy; CARTO",
230
+ }).addTo(map);
231
+
232
+ setupControls();
233
+ setupGhostDrag();
234
+ setupGPS();
235
+
236
+ map.on("click", () => {
237
+ map.dragging.enable();
238
+ });
239
+ }
240
+
241
+ export let labels: Labels = DEFAULT_LABELS;
242
+
243
+ export function setLabels(newLabels: Partial<Labels>) {
244
+ labels = { ...labels, ...newLabels };
245
+ }
246
+
247
+ export function initAsteroid(customLabels?: Partial<Labels>) {
248
+ if (customLabels) {
249
+ setLabels(customLabels);
250
+ }
251
+ if (document.readyState === "loading") {
252
+ document.addEventListener("DOMContentLoaded", init);
253
+ } else {
254
+ init();
255
+ }
256
+ }