@jjlmoya/utils-science 1.33.0 → 1.35.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 +1 -1
- package/src/category/index.ts +3 -1
- package/src/entries.ts +5 -1
- package/src/index.ts +2 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/natural-selection-drift/component.astro +37 -6
- package/src/tool/natural-selection-drift/natural-selection-drift.css +134 -0
- package/src/tool/roche-limit-satellite-disruption/bibliography.astro +14 -0
- package/src/tool/roche-limit-satellite-disruption/bibliography.ts +16 -0
- package/src/tool/roche-limit-satellite-disruption/component.astro +97 -0
- package/src/tool/roche-limit-satellite-disruption/entry.ts +28 -0
- package/src/tool/roche-limit-satellite-disruption/i18n/de.ts +229 -0
- package/src/tool/roche-limit-satellite-disruption/i18n/en.ts +229 -0
- package/src/tool/roche-limit-satellite-disruption/i18n/es.ts +229 -0
- package/src/tool/roche-limit-satellite-disruption/i18n/fr.ts +229 -0
- package/src/tool/roche-limit-satellite-disruption/i18n/id.ts +229 -0
- package/src/tool/roche-limit-satellite-disruption/i18n/it.ts +229 -0
- package/src/tool/roche-limit-satellite-disruption/i18n/ja.ts +229 -0
- package/src/tool/roche-limit-satellite-disruption/i18n/ko.ts +229 -0
- package/src/tool/roche-limit-satellite-disruption/i18n/nl.ts +229 -0
- package/src/tool/roche-limit-satellite-disruption/i18n/pl.ts +229 -0
- package/src/tool/roche-limit-satellite-disruption/i18n/pt.ts +229 -0
- package/src/tool/roche-limit-satellite-disruption/i18n/ru.ts +229 -0
- package/src/tool/roche-limit-satellite-disruption/i18n/sv.ts +229 -0
- package/src/tool/roche-limit-satellite-disruption/i18n/tr.ts +229 -0
- package/src/tool/roche-limit-satellite-disruption/i18n/zh.ts +229 -0
- package/src/tool/roche-limit-satellite-disruption/index.ts +11 -0
- package/src/tool/roche-limit-satellite-disruption/logic.ts +102 -0
- package/src/tool/roche-limit-satellite-disruption/particle-system.ts +66 -0
- package/src/tool/roche-limit-satellite-disruption/roche-limit-satellite-disruption-calculator.css +568 -0
- package/src/tool/roche-limit-satellite-disruption/script.ts +274 -0
- package/src/tool/roche-limit-satellite-disruption/seo.astro +15 -0
- package/src/tool/roche-limit-satellite-disruption/storage.ts +28 -0
- package/src/tool/roche-limit-satellite-disruption/visual-data.ts +16 -0
- package/src/tool/three-body-problem/app.ts +274 -0
- package/src/tool/three-body-problem/bibliography.astro +14 -0
- package/src/tool/three-body-problem/bibliography.ts +16 -0
- package/src/tool/three-body-problem/component.astro +70 -0
- package/src/tool/three-body-problem/entry.ts +26 -0
- package/src/tool/three-body-problem/i18n/de.ts +162 -0
- package/src/tool/three-body-problem/i18n/en.ts +162 -0
- package/src/tool/three-body-problem/i18n/es.ts +162 -0
- package/src/tool/three-body-problem/i18n/fr.ts +162 -0
- package/src/tool/three-body-problem/i18n/id.ts +162 -0
- package/src/tool/three-body-problem/i18n/it.ts +162 -0
- package/src/tool/three-body-problem/i18n/ja.ts +162 -0
- package/src/tool/three-body-problem/i18n/ko.ts +162 -0
- package/src/tool/three-body-problem/i18n/nl.ts +162 -0
- package/src/tool/three-body-problem/i18n/pl.ts +162 -0
- package/src/tool/three-body-problem/i18n/pt.ts +162 -0
- package/src/tool/three-body-problem/i18n/ru.ts +162 -0
- package/src/tool/three-body-problem/i18n/sv.ts +162 -0
- package/src/tool/three-body-problem/i18n/tr.ts +162 -0
- package/src/tool/three-body-problem/i18n/zh.ts +162 -0
- package/src/tool/three-body-problem/index.ts +11 -0
- package/src/tool/three-body-problem/logic/ThreeBodyEngine.ts +179 -0
- package/src/tool/three-body-problem/seo.astro +15 -0
- package/src/tool/three-body-problem/three-body-problem-simulator.css +503 -0
- package/src/tools.ts +4 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
2
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
3
|
+
|
|
4
|
+
const slug = 'roche-limit-satellite-disruption';
|
|
5
|
+
const title = '洛希极限计算器与卫星解体模拟器';
|
|
6
|
+
const description = '计算行星与卫星的洛希极限,比较流体与刚性解体距离,可视化潮汐力如何将卫星转变为环系统。';
|
|
7
|
+
|
|
8
|
+
const howTo = [
|
|
9
|
+
{
|
|
10
|
+
name: '选择主天体',
|
|
11
|
+
text: '选择拉伸卫星的行星。计算器将加载其半径、密度和质量,用于洛希极限和轨道周期估算。',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: '选择卫星类型',
|
|
15
|
+
text: '选择冰卫星、岩石卫星、碎石堆或富铁天体。密度和内聚力的不同会改变解体边界。',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: '移动轨道滑块',
|
|
19
|
+
text: '向内或向外拖动轨道距离。视觉圆盘会显示卫星处于洛希极限之外、擦碰极限、正在碎裂还是已变为环。',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: '比较极限值',
|
|
23
|
+
text: '使用读数比较经典流体洛希极限与较低的刚性体估算值以及经内聚力调整的有效极限。',
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const faq = [
|
|
28
|
+
{
|
|
29
|
+
question: '什么是洛希极限?',
|
|
30
|
+
answer: '洛希极限是距离大质量主天体的一段距离,在此距离处,跨越较小轨道天体的潮汐力变得足够强,足以克服该小天体的自引力。在该边界以内,脆弱的或类流体的卫星可能被撕裂。',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
question: '为什么有流体洛希极限和刚性洛希极限之分?',
|
|
34
|
+
answer: '流体卫星容易变形,潮汐力可以加剧其拉长,使其在更远的距离外被瓦解。刚性卫星凭借材料强度抵抗变形,因此简单的刚性估算将解体位置估算得更靠近主天体。',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
question: '洛希极限内的每个卫星都会立即变成环吗?',
|
|
38
|
+
answer: '不会。真实的解体取决于自转、成分、裂缝、孔隙率、加热、撞击和材料强度。本工具展示的是经典引力边界,并采用过渡带来传达风险,而不是一个瞬间切换的开关。',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
question: '为什么土星环位于洛希极限附近?',
|
|
42
|
+
answer: '土星环占据了一个区域,在此区域中冰物质可以以颗粒形式存在,而不是聚集成一颗大卫星。洛希极限有助于解释为什么环颗粒能够在靠近行星的地方保持分散。',
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
export const content: ToolLocaleContent = {
|
|
47
|
+
slug,
|
|
48
|
+
title,
|
|
49
|
+
description,
|
|
50
|
+
ui: {
|
|
51
|
+
primaryBody: '主天体',
|
|
52
|
+
satelliteType: '卫星类型',
|
|
53
|
+
orbitDistance: '轨道距离',
|
|
54
|
+
rocheBoundary: '洛希边界',
|
|
55
|
+
fluidLimit: '流体极限',
|
|
56
|
+
rigidLimit: '刚性极限',
|
|
57
|
+
activeLimit: '有效极限',
|
|
58
|
+
safetyRatio: '安全比率',
|
|
59
|
+
orbitalPeriod: '轨道周期',
|
|
60
|
+
tidalStress: '潮汐应力',
|
|
61
|
+
ringFormation: '环形成',
|
|
62
|
+
stable: '稳定轨道',
|
|
63
|
+
grazing: '潮汐擦碰',
|
|
64
|
+
fragmenting: '碎裂中',
|
|
65
|
+
ring: '环系统',
|
|
66
|
+
km: 'km',
|
|
67
|
+
hours: '小时',
|
|
68
|
+
density: '密度',
|
|
69
|
+
cohesion: '内聚力',
|
|
70
|
+
planetRadius: '行星半径',
|
|
71
|
+
reset: '重置',
|
|
72
|
+
closePass: '近距离飞越',
|
|
73
|
+
moonTrack: '卫星轨迹',
|
|
74
|
+
debrisTrack: '碎片轨迹',
|
|
75
|
+
},
|
|
76
|
+
seo: [
|
|
77
|
+
{
|
|
78
|
+
type: 'title',
|
|
79
|
+
text: '洛希极限公式、含义及如何使用本计算器',
|
|
80
|
+
level: 2,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
type: 'paragraph',
|
|
84
|
+
html: '<strong>洛希极限</strong>是指一颗主要靠自身引力维持的卫星在绕大天体运行时,不会被潮汐力撕裂的最小轨道距离。人们通常搜索它,是为了想知道某颗卫星、彗星、小行星或人造场景能否在近距离飞越行星时幸存,或者物质是否会扩散成环。本计算器通过结合行星半径、行星密度、卫星密度以及卫星的近似内部强度来回答这个问题。',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
type: 'paragraph',
|
|
88
|
+
html: '关键思想很简单:引力在卫星上并非均匀分布。近端受到的拉力大于远端,从而产生拉伸力。如果这种潮汐拉伸力强于卫星的自引力和物质内聚力,天体就会破裂、脱落质量并最终碎裂。因此,洛希极限不仅仅是一个距离;它是外部潮汐应力与内部结合力之间的比较。',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
type: 'title',
|
|
92
|
+
text: '计算器使用的洛希极限公式',
|
|
93
|
+
level: 3,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
type: 'paragraph',
|
|
97
|
+
html: '对于流体或非常弱的卫星,经典近似公式为 <strong>d = 2.44 R (rho_M / rho_m)^(1/3)</strong>。对于刚性卫星,常用近似公式为 <strong>d = 1.26 R (rho_M / rho_m)^(1/3)</strong>。在这些公式中,<strong>d</strong> 是从行星中心测量的洛希极限,<strong>R</strong> 是主天体的半径,<strong>rho_M</strong> 是主天体的密度,<strong>rho_m</strong> 是卫星的密度。',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
type: 'list',
|
|
101
|
+
items: [
|
|
102
|
+
'<strong>主天体半径:</strong>即使密度相似,较大的行星也会产生更大的洛希极限距离。',
|
|
103
|
+
'<strong>主天体密度:</strong>密度较大的主天体在给定半径倍数处会增强潮汐强度。',
|
|
104
|
+
'<strong>卫星密度:</strong>密度较大的卫星具有更强的自引力,因此可以在更靠近行星的位置幸存。',
|
|
105
|
+
'<strong>卫星强度:</strong>流体状、冰质、破裂或碎石堆天体比致密的刚性天体在更远处解体。',
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
type: 'table',
|
|
110
|
+
headers: ['模型', '公式形式', '适用场景', '结果含义'],
|
|
111
|
+
rows: [
|
|
112
|
+
['流体洛希极限', '2.44 R (rho_M / rho_m)^(1/3)', '冰卫星、熔融天体、碎石堆、弱彗星', '易于变形的天体的保守解体距离。'],
|
|
113
|
+
['刚性洛希极限', '1.26 R (rho_M / rho_m)^(1/3)', '具有材料强度的致密岩石或金属天体', '材料强度延缓解体的较近下限估算值。'],
|
|
114
|
+
['内聚力调整显示', '介于流体和刚性之间', '本模拟器中的快速场景比较', '针对所选卫星类型的实用风险线,并非普遍的自然法则开关。'],
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
type: 'title',
|
|
119
|
+
text: '示例:为什么土星附近的冰卫星容易解体',
|
|
120
|
+
level: 3,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
type: 'paragraph',
|
|
124
|
+
html: '土星的密度远小于地球,但它体积巨大。低密度的冰卫星自引力较弱,无法与致密的岩石卫星相比,因此密度比仍然使流体洛希极限远离土星的云顶。这就是为什么洛希极限物理学对于理解土星为何能维持一个宽广明亮的、主要由冰颗粒组成而非一颗重新聚集的大卫星的环系统至关重要。',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
type: 'paragraph',
|
|
128
|
+
html: '如果在计算器中选择土星和一颗冰卫星,然后向内拖动轨道,观察安全比率。当比率高于 <strong>1.00x</strong> 时,所选轨道位于有效洛希边界之外。接近 <strong>1.00x</strong> 时,卫星处于潮汐擦碰区域,质量脱落或破裂成为可能。低于 <strong>1.00x</strong> 时,可视化向碎片弧和环形成转变,因为所选模型预测会发生解体。',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
type: 'title',
|
|
132
|
+
text: '如何解读安全比率',
|
|
133
|
+
level: 3,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
type: 'paragraph',
|
|
137
|
+
html: '<strong>安全比率</strong>是当前轨道距离除以所选洛希边界。比率为 <strong>1.25x</strong> 意味着轨道比有效解体估算值远 25%。比率为 <strong>1.00x</strong> 意味着轨道恰好位于所选洛希边界上。比率为 <strong>0.80x</strong> 意味着卫星已深入所选解体区域内部。',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: 'table',
|
|
141
|
+
headers: ['安全比率', '显示状态', '实际含义'],
|
|
142
|
+
rows: [
|
|
143
|
+
['高于 1.12x', '稳定轨道', '卫星在此简化模型中位于所选洛希边界之外。'],
|
|
144
|
+
['1.00x 至 1.12x', '潮汐擦碰', '天体足够近,变形、破裂或表面脱落可能产生影响。'],
|
|
145
|
+
['0.78x 至 1.00x', '碎裂中', '在所选模型中,自引力已不足以维持;碎片流是可能的。'],
|
|
146
|
+
['低于 0.78x', '环系统', '原始天体被表示为沿相邻轨道散布的物质。'],
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
type: 'paragraph',
|
|
151
|
+
html: '显示轨道周期的原因在于,近距离飞越不仅关乎距离。解体区域内的物质沿快速、略有差异的轨道运动。一旦碎片分离,轨道剪切力将它们散布在行星周围,而碰撞则使碎片扁平化并按大小分选,形成盘状环。',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
type: 'title',
|
|
155
|
+
text: '为什么洛希极限能产生环',
|
|
156
|
+
level: 3,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
type: 'paragraph',
|
|
160
|
+
html: '当卫星在洛希极限外碎裂时,碎片最终可能碰撞并重新聚集成一颗较小的卫星。在洛希极限内,附近的碎片难以合并成一个稳定的自引力天体,因为潮汐力不断将物质拉开。其结果可能是一个长寿命的环,尤其是当碎片是冰质的、存在碰撞、并受到小卫星或共振持续扰动时。',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
type: 'paragraph',
|
|
164
|
+
html: '环的形成是渐进的。一颗被瓦解的卫星首先被拉长,然后脱落颗粒和较大的碎片。这些碎片占据略有不同的轨道半径,因此它们会彼此超前或滞后。随着时间的推移,碰撞抑制了垂直运动,物质沉降成一个薄盘。这就是为什么模拟器展示的是从单颗卫星到碎片弧再到更完整的环的过渡,而不是将解体视为一次瞬间爆炸。',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
type: 'title',
|
|
168
|
+
text: '本洛希极限计算器的重要限制',
|
|
169
|
+
level: 3,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
type: 'paragraph',
|
|
173
|
+
html: '本计算器旨在提供快速的科学直觉,而非高保真的任务设计。真实的卫星受到自转、轨道偏心率、内部分层、抗拉强度、孔隙率、温度、潮汐加热、既有裂缝、撞击以及其他卫星的共振影响。一个在偏心轨道上旋转的碎石堆与一个在圆形轨道上的寒冷整体岩石可能以不同的方式失效,即使它们的平均密度看起来相似。',
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
type: 'list',
|
|
177
|
+
items: [
|
|
178
|
+
'<strong>使用流体极限</strong>适用于弱的、冰质的、熔融的、高度碎裂的或由松散集合体组成的天体。',
|
|
179
|
+
'<strong>使用刚性极限</strong>作为具有实际内部强度的致密天体的下限估算值。',
|
|
180
|
+
'<strong>读取有效极限</strong>作为模拟器针对所选卫星类型选择的工作边界。',
|
|
181
|
+
'<strong>不要将结果</strong>解读为对某个真实命名卫星的精确预测,这需要详细的地球物理模型。',
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
type: 'title',
|
|
186
|
+
text: '本工具帮助解答的常见问题',
|
|
187
|
+
level: 3,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
type: 'paragraph',
|
|
191
|
+
html: '使用本工具来估算以下问题:一颗卫星在解体前能距离地球多近?为什么土星环位于洛希极限区域内?岩石卫星是否比冰卫星能在更近的距离幸存?密度如何改变洛希极限?流体洛希极限和刚性洛希极限有什么区别?控制组件正是围绕这些比较而构建,因此改变一个变量即可立即显示解体距离、安全比率和环形成可视化如何响应。',
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
faq,
|
|
195
|
+
bibliography,
|
|
196
|
+
howTo,
|
|
197
|
+
schemas: [
|
|
198
|
+
{
|
|
199
|
+
'@context': 'https://schema.org',
|
|
200
|
+
'@type': 'SoftwareApplication',
|
|
201
|
+
name: title,
|
|
202
|
+
description,
|
|
203
|
+
applicationCategory: 'ScientificApplication',
|
|
204
|
+
operatingSystem: 'Any',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
'@context': 'https://schema.org',
|
|
208
|
+
'@type': 'FAQPage',
|
|
209
|
+
mainEntity: faq.map((item) => ({
|
|
210
|
+
'@type': 'Question',
|
|
211
|
+
name: item.question,
|
|
212
|
+
acceptedAnswer: {
|
|
213
|
+
'@type': 'Answer',
|
|
214
|
+
text: item.answer,
|
|
215
|
+
},
|
|
216
|
+
})),
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
'@context': 'https://schema.org',
|
|
220
|
+
'@type': 'HowTo',
|
|
221
|
+
name: title,
|
|
222
|
+
step: howTo.map((step) => ({
|
|
223
|
+
'@type': 'HowToStep',
|
|
224
|
+
name: step.name,
|
|
225
|
+
text: step.text,
|
|
226
|
+
})),
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { rocheLimitSatelliteDisruption } from './entry';
|
|
2
|
+
import type { ToolDefinition } from '../../types';
|
|
3
|
+
|
|
4
|
+
export * from './entry';
|
|
5
|
+
|
|
6
|
+
export const ROCHE_LIMIT_SATELLITE_DISRUPTION_TOOL: ToolDefinition = {
|
|
7
|
+
entry: rocheLimitSatelliteDisruption,
|
|
8
|
+
Component: () => import('./component.astro'),
|
|
9
|
+
SEOComponent: () => import('./seo.astro'),
|
|
10
|
+
BibliographyComponent: () => import('./bibliography.astro'),
|
|
11
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export type PrimaryId = 'earth' | 'mars' | 'jupiter' | 'saturn' | 'neptune';
|
|
2
|
+
export type SatelliteId = 'icy-moon' | 'rocky-moon' | 'rubble-pile' | 'iron-core';
|
|
3
|
+
|
|
4
|
+
export interface PrimaryBody {
|
|
5
|
+
id: PrimaryId;
|
|
6
|
+
name: string;
|
|
7
|
+
radiusKm: number;
|
|
8
|
+
densityGcm3: number;
|
|
9
|
+
massKg: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SatelliteBody {
|
|
13
|
+
id: SatelliteId;
|
|
14
|
+
name: string;
|
|
15
|
+
densityGcm3: number;
|
|
16
|
+
radiusKm: number;
|
|
17
|
+
cohesion: 'fluid' | 'fractured' | 'rigid';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RocheInput {
|
|
21
|
+
primaryId: PrimaryId;
|
|
22
|
+
satelliteId: SatelliteId;
|
|
23
|
+
orbitDistanceKm: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RocheResult {
|
|
27
|
+
primary: PrimaryBody;
|
|
28
|
+
satellite: SatelliteBody;
|
|
29
|
+
fluidLimitKm: number;
|
|
30
|
+
rigidLimitKm: number;
|
|
31
|
+
selectedLimitKm: number;
|
|
32
|
+
safetyRatio: number;
|
|
33
|
+
tidalStressIndex: number;
|
|
34
|
+
ringProgress: number;
|
|
35
|
+
orbitalPeriodHours: number;
|
|
36
|
+
verdict: 'stable' | 'grazing' | 'fragmenting' | 'ring';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const GRAVITATIONAL_CONSTANT = 6.6743e-11;
|
|
40
|
+
|
|
41
|
+
export const PRIMARY_BODIES: PrimaryBody[] = [
|
|
42
|
+
{ id: 'earth', name: 'Earth', radiusKm: 6371, densityGcm3: 5.51, massKg: 5.972e24 },
|
|
43
|
+
{ id: 'mars', name: 'Mars', radiusKm: 3390, densityGcm3: 3.93, massKg: 6.417e23 },
|
|
44
|
+
{ id: 'jupiter', name: 'Jupiter', radiusKm: 69911, densityGcm3: 1.33, massKg: 1.898e27 },
|
|
45
|
+
{ id: 'saturn', name: 'Saturn', radiusKm: 58232, densityGcm3: 0.69, massKg: 5.683e26 },
|
|
46
|
+
{ id: 'neptune', name: 'Neptune', radiusKm: 24622, densityGcm3: 1.64, massKg: 1.024e26 },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
export const SATELLITE_BODIES: SatelliteBody[] = [
|
|
50
|
+
{ id: 'icy-moon', name: 'Icy moon', densityGcm3: 0.93, radiusKm: 760, cohesion: 'fluid' },
|
|
51
|
+
{ id: 'rocky-moon', name: 'Rocky moon', densityGcm3: 3.34, radiusKm: 1737, cohesion: 'fractured' },
|
|
52
|
+
{ id: 'rubble-pile', name: 'Rubble pile', densityGcm3: 1.75, radiusKm: 45, cohesion: 'fluid' },
|
|
53
|
+
{ id: 'iron-core', name: 'Iron-rich moon', densityGcm3: 5.3, radiusKm: 980, cohesion: 'rigid' },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
function findPrimary(id: PrimaryId): PrimaryBody {
|
|
57
|
+
return PRIMARY_BODIES.find((body) => body.id === id) ?? PRIMARY_BODIES[0];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function findSatellite(id: SatelliteId): SatelliteBody {
|
|
61
|
+
return SATELLITE_BODIES.find((body) => body.id === id) ?? SATELLITE_BODIES[0];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function selectedLimitFor(satellite: SatelliteBody, fluidLimitKm: number, rigidLimitKm: number): number {
|
|
65
|
+
if (satellite.cohesion === 'rigid') return rigidLimitKm;
|
|
66
|
+
if (satellite.cohesion === 'fractured') return (fluidLimitKm + rigidLimitKm) / 2;
|
|
67
|
+
return fluidLimitKm;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function verdictFor(safetyRatio: number): RocheResult['verdict'] {
|
|
71
|
+
if (safetyRatio < 0.78) return 'ring';
|
|
72
|
+
if (safetyRatio < 1) return 'fragmenting';
|
|
73
|
+
if (safetyRatio < 1.12) return 'grazing';
|
|
74
|
+
return 'stable';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function calculateRocheLimit(input: RocheInput): RocheResult {
|
|
78
|
+
const primary = findPrimary(input.primaryId);
|
|
79
|
+
const satellite = findSatellite(input.satelliteId);
|
|
80
|
+
const densityRatio = primary.densityGcm3 / satellite.densityGcm3;
|
|
81
|
+
const fluidLimitKm = 2.44 * primary.radiusKm * Math.cbrt(densityRatio);
|
|
82
|
+
const rigidLimitKm = 1.26 * primary.radiusKm * Math.cbrt(densityRatio);
|
|
83
|
+
const selectedLimitKm = selectedLimitFor(satellite, fluidLimitKm, rigidLimitKm);
|
|
84
|
+
const orbitDistanceKm = Math.max(primary.radiusKm * 1.02, input.orbitDistanceKm);
|
|
85
|
+
const safetyRatio = orbitDistanceKm / selectedLimitKm;
|
|
86
|
+
const tidalStressIndex = Math.min(2.2, Math.pow(selectedLimitKm / orbitDistanceKm, 3));
|
|
87
|
+
const ringProgress = Math.max(0, Math.min(1, (1.08 - safetyRatio) / 0.38));
|
|
88
|
+
const orbitalPeriodHours = 2 * Math.PI * Math.sqrt(Math.pow(orbitDistanceKm * 1000, 3) / (GRAVITATIONAL_CONSTANT * primary.massKg)) / 3600;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
primary,
|
|
92
|
+
satellite,
|
|
93
|
+
fluidLimitKm,
|
|
94
|
+
rigidLimitKm,
|
|
95
|
+
selectedLimitKm,
|
|
96
|
+
safetyRatio,
|
|
97
|
+
tidalStressIndex,
|
|
98
|
+
ringProgress,
|
|
99
|
+
orbitalPeriodHours,
|
|
100
|
+
verdict: verdictFor(safetyRatio),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export type VisualState = 'orbiting' | 'deforming' | 'disrupting' | 'ringFormed';
|
|
2
|
+
|
|
3
|
+
export interface ParticleState {
|
|
4
|
+
moon: { x: number; y: number; orbitRadius: number; progress: number };
|
|
5
|
+
visualState: VisualState;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const particles = Array.from({ length: 90 }, (_, index) => ({
|
|
9
|
+
angle: index * 0.42,
|
|
10
|
+
radiusOffset: (index % 9) - 4,
|
|
11
|
+
speed: 0.006 + (index % 7) * 0.0012,
|
|
12
|
+
life: 0,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
export function resizeParticleCanvas(canvas: HTMLCanvasElement): void {
|
|
16
|
+
const rect = canvas.getBoundingClientRect();
|
|
17
|
+
const scale = window.devicePixelRatio || 1;
|
|
18
|
+
canvas.width = Math.max(1, Math.floor(rect.width * scale));
|
|
19
|
+
canvas.height = Math.max(1, Math.floor(rect.height * scale));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function animateParticles(canvas: HTMLCanvasElement, getState: () => ParticleState): void {
|
|
23
|
+
const context = canvas.getContext('2d');
|
|
24
|
+
if (!context) return;
|
|
25
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
26
|
+
const state = getState();
|
|
27
|
+
if (state.moon.progress > 0.01) drawParticleFrame(canvas, context, state);
|
|
28
|
+
window.requestAnimationFrame(() => animateParticles(canvas, getState));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function drawParticleFrame(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, state: ParticleState): void {
|
|
32
|
+
const scaleX = canvas.width / 560;
|
|
33
|
+
const scaleY = canvas.height / 560;
|
|
34
|
+
particles.forEach((particle, index) => {
|
|
35
|
+
const formed = state.visualState === 'ringFormed';
|
|
36
|
+
particle.life = formed ? 1 : (particle.life + state.moon.progress * 0.018 + 0.004) % 1;
|
|
37
|
+
particle.angle += particle.speed * (1 + state.moon.progress * 2.4);
|
|
38
|
+
const release = formed ? 1 : Math.min(1, particle.life * 1.35);
|
|
39
|
+
const angle = particle.angle + index;
|
|
40
|
+
const ringX = 280 + Math.cos(angle) * (state.moon.orbitRadius + particle.radiusOffset * 3.2);
|
|
41
|
+
const ringY = 280 + Math.sin(angle) * (state.moon.orbitRadius * 0.31 + particle.radiusOffset);
|
|
42
|
+
drawParticle(context, {
|
|
43
|
+
x: state.moon.x + (ringX - state.moon.x) * release,
|
|
44
|
+
y: state.moon.y + (ringY - state.moon.y) * release,
|
|
45
|
+
angle,
|
|
46
|
+
frontSide: Math.sin(angle) > 0,
|
|
47
|
+
scaleX,
|
|
48
|
+
scaleY,
|
|
49
|
+
progress: state.moon.progress,
|
|
50
|
+
visualState: state.visualState,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function drawParticle(context: CanvasRenderingContext2D, point: { x: number; y: number; angle: number; frontSide: boolean; scaleX: number; scaleY: number; progress: number; visualState: VisualState }): void {
|
|
56
|
+
const isDarkTheme = document.documentElement.classList.contains('theme-dark');
|
|
57
|
+
let alpha = point.progress * 0.58;
|
|
58
|
+
if (point.visualState === 'ringFormed') alpha = point.frontSide ? 0.72 : 0.26;
|
|
59
|
+
context.strokeStyle = `rgba(${isDarkTheme ? '248, 214, 109' : '93, 70, 30'}, ${0.16 + alpha})`;
|
|
60
|
+
context.lineCap = 'round';
|
|
61
|
+
context.lineWidth = Math.max(1, (point.frontSide ? 1.65 : 0.9) * point.scaleX);
|
|
62
|
+
context.beginPath();
|
|
63
|
+
context.moveTo(point.x * point.scaleX, point.y * point.scaleY);
|
|
64
|
+
context.lineTo((point.x + Math.cos(point.angle) * 6) * point.scaleX, (point.y + Math.sin(point.angle) * 3) * point.scaleY);
|
|
65
|
+
context.stroke();
|
|
66
|
+
}
|