@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.
Files changed (60) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +5 -1
  4. package/src/index.ts +2 -0
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/natural-selection-drift/component.astro +37 -6
  8. package/src/tool/natural-selection-drift/natural-selection-drift.css +134 -0
  9. package/src/tool/roche-limit-satellite-disruption/bibliography.astro +14 -0
  10. package/src/tool/roche-limit-satellite-disruption/bibliography.ts +16 -0
  11. package/src/tool/roche-limit-satellite-disruption/component.astro +97 -0
  12. package/src/tool/roche-limit-satellite-disruption/entry.ts +28 -0
  13. package/src/tool/roche-limit-satellite-disruption/i18n/de.ts +229 -0
  14. package/src/tool/roche-limit-satellite-disruption/i18n/en.ts +229 -0
  15. package/src/tool/roche-limit-satellite-disruption/i18n/es.ts +229 -0
  16. package/src/tool/roche-limit-satellite-disruption/i18n/fr.ts +229 -0
  17. package/src/tool/roche-limit-satellite-disruption/i18n/id.ts +229 -0
  18. package/src/tool/roche-limit-satellite-disruption/i18n/it.ts +229 -0
  19. package/src/tool/roche-limit-satellite-disruption/i18n/ja.ts +229 -0
  20. package/src/tool/roche-limit-satellite-disruption/i18n/ko.ts +229 -0
  21. package/src/tool/roche-limit-satellite-disruption/i18n/nl.ts +229 -0
  22. package/src/tool/roche-limit-satellite-disruption/i18n/pl.ts +229 -0
  23. package/src/tool/roche-limit-satellite-disruption/i18n/pt.ts +229 -0
  24. package/src/tool/roche-limit-satellite-disruption/i18n/ru.ts +229 -0
  25. package/src/tool/roche-limit-satellite-disruption/i18n/sv.ts +229 -0
  26. package/src/tool/roche-limit-satellite-disruption/i18n/tr.ts +229 -0
  27. package/src/tool/roche-limit-satellite-disruption/i18n/zh.ts +229 -0
  28. package/src/tool/roche-limit-satellite-disruption/index.ts +11 -0
  29. package/src/tool/roche-limit-satellite-disruption/logic.ts +102 -0
  30. package/src/tool/roche-limit-satellite-disruption/particle-system.ts +66 -0
  31. package/src/tool/roche-limit-satellite-disruption/roche-limit-satellite-disruption-calculator.css +568 -0
  32. package/src/tool/roche-limit-satellite-disruption/script.ts +274 -0
  33. package/src/tool/roche-limit-satellite-disruption/seo.astro +15 -0
  34. package/src/tool/roche-limit-satellite-disruption/storage.ts +28 -0
  35. package/src/tool/roche-limit-satellite-disruption/visual-data.ts +16 -0
  36. package/src/tool/three-body-problem/app.ts +274 -0
  37. package/src/tool/three-body-problem/bibliography.astro +14 -0
  38. package/src/tool/three-body-problem/bibliography.ts +16 -0
  39. package/src/tool/three-body-problem/component.astro +70 -0
  40. package/src/tool/three-body-problem/entry.ts +26 -0
  41. package/src/tool/three-body-problem/i18n/de.ts +162 -0
  42. package/src/tool/three-body-problem/i18n/en.ts +162 -0
  43. package/src/tool/three-body-problem/i18n/es.ts +162 -0
  44. package/src/tool/three-body-problem/i18n/fr.ts +162 -0
  45. package/src/tool/three-body-problem/i18n/id.ts +162 -0
  46. package/src/tool/three-body-problem/i18n/it.ts +162 -0
  47. package/src/tool/three-body-problem/i18n/ja.ts +162 -0
  48. package/src/tool/three-body-problem/i18n/ko.ts +162 -0
  49. package/src/tool/three-body-problem/i18n/nl.ts +162 -0
  50. package/src/tool/three-body-problem/i18n/pl.ts +162 -0
  51. package/src/tool/three-body-problem/i18n/pt.ts +162 -0
  52. package/src/tool/three-body-problem/i18n/ru.ts +162 -0
  53. package/src/tool/three-body-problem/i18n/sv.ts +162 -0
  54. package/src/tool/three-body-problem/i18n/tr.ts +162 -0
  55. package/src/tool/three-body-problem/i18n/zh.ts +162 -0
  56. package/src/tool/three-body-problem/index.ts +11 -0
  57. package/src/tool/three-body-problem/logic/ThreeBodyEngine.ts +179 -0
  58. package/src/tool/three-body-problem/seo.astro +15 -0
  59. package/src/tool/three-body-problem/three-body-problem-simulator.css +503 -0
  60. package/src/tools.ts +4 -0
@@ -0,0 +1,11 @@
1
+ import { threeBodyProblem } from './entry';
2
+ import type { ToolDefinition } from '../../types';
3
+
4
+ export * from './entry';
5
+
6
+ export const THREE_BODY_PROBLEM_TOOL: ToolDefinition = {
7
+ entry: threeBodyProblem,
8
+ Component: () => import('./component.astro'),
9
+ SEOComponent: () => import('./seo.astro'),
10
+ BibliographyComponent: () => import('./bibliography.astro'),
11
+ };
@@ -0,0 +1,179 @@
1
+ export interface BodyState {
2
+ id: string;
3
+ label: string;
4
+ mass: number;
5
+ x: number;
6
+ y: number;
7
+ vx: number;
8
+ vy: number;
9
+ color: string;
10
+ }
11
+
12
+ export interface ThreeBodyMetrics {
13
+ kineticEnergy: number;
14
+ potentialEnergy: number;
15
+ totalEnergy: number;
16
+ centerOfMassX: number;
17
+ centerOfMassY: number;
18
+ maxSeparation: number;
19
+ minSeparation: number;
20
+ }
21
+
22
+ export interface SimulationSnapshot {
23
+ bodies: BodyState[];
24
+ metrics: ThreeBodyMetrics;
25
+ }
26
+
27
+ export interface ThreeBodyPreset {
28
+ id: string;
29
+ bodies: BodyState[];
30
+ timeStep: number;
31
+ trailLength: number;
32
+ zoom: number;
33
+ }
34
+
35
+ interface SeparationMetrics {
36
+ potentialEnergy: number;
37
+ maxSeparation: number;
38
+ minSeparation: number;
39
+ }
40
+
41
+ const GRAVITATIONAL_CONSTANT = 1;
42
+ const SOFTENING = 0.035;
43
+
44
+ export const THREE_BODY_PRESETS: ThreeBodyPreset[] = [
45
+ {
46
+ id: 'figureEight',
47
+ timeStep: 0.012,
48
+ trailLength: 720,
49
+ zoom: 145,
50
+ bodies: [
51
+ { id: 'body-a', label: 'A', mass: 1, x: -0.97000436, y: 0.24308753, vx: 0.46620369, vy: 0.43236573, color: '#b85b62' },
52
+ { id: 'body-b', label: 'B', mass: 1, x: 0.97000436, y: -0.24308753, vx: 0.46620369, vy: 0.43236573, color: '#2f7f8d' },
53
+ { id: 'body-c', label: 'C', mass: 1, x: 0, y: 0, vx: -0.93240737, vy: -0.86473146, color: '#a9853d' },
54
+ ],
55
+ },
56
+ {
57
+ id: 'lagrange',
58
+ timeStep: 0.014,
59
+ trailLength: 620,
60
+ zoom: 155,
61
+ bodies: [
62
+ { id: 'body-a', label: 'A', mass: 1.2, x: 0, y: 1, vx: -0.54, vy: 0, color: '#b85b62' },
63
+ { id: 'body-b', label: 'B', mass: 1.2, x: -0.866, y: -0.5, vx: 0.27, vy: -0.468, color: '#2f7f8d' },
64
+ { id: 'body-c', label: 'C', mass: 1.2, x: 0.866, y: -0.5, vx: 0.27, vy: 0.468, color: '#a9853d' },
65
+ ],
66
+ },
67
+ {
68
+ id: 'slingshot',
69
+ timeStep: 0.01,
70
+ trailLength: 520,
71
+ zoom: 120,
72
+ bodies: [
73
+ { id: 'body-a', label: 'A', mass: 2.5, x: -0.45, y: 0, vx: 0, vy: -0.24, color: '#b85b62' },
74
+ { id: 'body-b', label: 'B', mass: 1, x: 0.72, y: 0.1, vx: -0.2, vy: 0.66, color: '#2f7f8d' },
75
+ { id: 'body-c', label: 'C', mass: 0.35, x: 0.16, y: -1.22, vx: 1.18, vy: 0.25, color: '#a9853d' },
76
+ ],
77
+ },
78
+ ];
79
+
80
+ export class ThreeBodyEngine {
81
+ public cloneBodies(bodies: BodyState[]): BodyState[] {
82
+ return bodies.map((body) => ({ ...body }));
83
+ }
84
+
85
+ public step(bodies: BodyState[], dt: number): SimulationSnapshot {
86
+ const accelerations = this.calculateAccelerations(bodies);
87
+ const halfStepBodies = bodies.map((body, index) => ({
88
+ ...body,
89
+ vx: body.vx + accelerations[index].ax * dt * 0.5,
90
+ vy: body.vy + accelerations[index].ay * dt * 0.5,
91
+ }));
92
+
93
+ const movedBodies = halfStepBodies.map((body) => ({
94
+ ...body,
95
+ x: body.x + body.vx * dt,
96
+ y: body.y + body.vy * dt,
97
+ }));
98
+
99
+ const nextAccelerations = this.calculateAccelerations(movedBodies);
100
+ const nextBodies = movedBodies.map((body, index) => ({
101
+ ...body,
102
+ vx: body.vx + nextAccelerations[index].ax * dt * 0.5,
103
+ vy: body.vy + nextAccelerations[index].ay * dt * 0.5,
104
+ }));
105
+
106
+ return {
107
+ bodies: nextBodies,
108
+ metrics: this.calculateMetrics(nextBodies),
109
+ };
110
+ }
111
+
112
+ public calculateMetrics(bodies: BodyState[]): ThreeBodyMetrics {
113
+ let kineticEnergy = 0;
114
+ let totalMass = 0;
115
+ let weightedX = 0;
116
+ let weightedY = 0;
117
+
118
+ bodies.forEach((body) => {
119
+ kineticEnergy += 0.5 * body.mass * (body.vx * body.vx + body.vy * body.vy);
120
+ totalMass += body.mass;
121
+ weightedX += body.mass * body.x;
122
+ weightedY += body.mass * body.y;
123
+ });
124
+
125
+ const separation = this.calculateSeparationMetrics(bodies);
126
+
127
+ return {
128
+ kineticEnergy,
129
+ potentialEnergy: separation.potentialEnergy,
130
+ totalEnergy: kineticEnergy + separation.potentialEnergy,
131
+ centerOfMassX: weightedX / totalMass,
132
+ centerOfMassY: weightedY / totalMass,
133
+ maxSeparation: separation.maxSeparation,
134
+ minSeparation: separation.minSeparation,
135
+ };
136
+ }
137
+
138
+ private calculateSeparationMetrics(bodies: BodyState[]): SeparationMetrics {
139
+ let potentialEnergy = 0;
140
+ let maxSeparation = 0;
141
+ let minSeparation = Number.POSITIVE_INFINITY;
142
+
143
+ for (let i = 0; i < bodies.length; i += 1) {
144
+ for (let j = i + 1; j < bodies.length; j += 1) {
145
+ const dx = bodies[j].x - bodies[i].x;
146
+ const dy = bodies[j].y - bodies[i].y;
147
+ const distance = Math.sqrt(dx * dx + dy * dy + SOFTENING * SOFTENING);
148
+ potentialEnergy -= (GRAVITATIONAL_CONSTANT * bodies[i].mass * bodies[j].mass) / distance;
149
+ maxSeparation = Math.max(maxSeparation, distance);
150
+ minSeparation = Math.min(minSeparation, distance);
151
+ }
152
+ }
153
+
154
+ return {
155
+ potentialEnergy,
156
+ maxSeparation,
157
+ minSeparation,
158
+ };
159
+ }
160
+
161
+ private calculateAccelerations(bodies: BodyState[]) {
162
+ return bodies.map((body, index) => {
163
+ let ax = 0;
164
+ let ay = 0;
165
+
166
+ bodies.forEach((other, otherIndex) => {
167
+ if (index === otherIndex) return;
168
+ const dx = other.x - body.x;
169
+ const dy = other.y - body.y;
170
+ const softenedDistanceSquared = dx * dx + dy * dy + SOFTENING * SOFTENING;
171
+ const inverseDistanceCubed = 1 / Math.pow(softenedDistanceSquared, 1.5);
172
+ ax += GRAVITATIONAL_CONSTANT * other.mass * dx * inverseDistanceCubed;
173
+ ay += GRAVITATIONAL_CONSTANT * other.mass * dy * inverseDistanceCubed;
174
+ });
175
+
176
+ return { ax, ay };
177
+ });
178
+ }
179
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { threeBodyProblem } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const content = await threeBodyProblem.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
15
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,503 @@
1
+ :root {
2
+ --three-body-ink: #24221d;
3
+ --three-body-muted: rgba(36, 34, 29, 0.56);
4
+ --three-body-card: #eee7d8;
5
+ --three-body-card-edge: rgba(73, 62, 42, 0.2);
6
+ --three-body-panel: rgba(255, 252, 239, 0.42);
7
+ --three-body-space: #e8dfc9;
8
+ --three-body-overlay: rgba(250, 246, 232, 0.58);
9
+ --three-body-overlay-strong: rgba(255, 252, 239, 0.8);
10
+ --three-body-control: rgba(49, 45, 36, 0.72);
11
+ --three-body-control-active: #24221d;
12
+ --three-body-shadow: rgba(55, 48, 38, 0.18);
13
+ }
14
+
15
+ .theme-dark {
16
+ --three-body-ink: #e8edf2;
17
+ --three-body-muted: rgba(232, 237, 242, 0.52);
18
+ --three-body-card: #071019;
19
+ --three-body-card-edge: rgba(255, 255, 255, 0.12);
20
+ --three-body-panel: rgba(255, 255, 255, 0.035);
21
+ --three-body-space: #050914;
22
+ --three-body-overlay: rgba(9, 15, 23, 0.55);
23
+ --three-body-overlay-strong: rgba(9, 15, 23, 0.78);
24
+ --three-body-control: rgba(232, 237, 242, 0.66);
25
+ --three-body-control-active: #f8fafc;
26
+ --three-body-shadow: rgba(0, 0, 0, 0.36);
27
+ }
28
+
29
+ .three-body-root {
30
+ display: grid;
31
+ gap: 0.75rem;
32
+ max-width: 1180px;
33
+ margin: 0 auto;
34
+ padding: 0.75rem;
35
+ border: 1px solid var(--three-body-card-edge);
36
+ border-radius: 8px;
37
+ background:
38
+ linear-gradient(90deg, rgba(36, 34, 29, 0.035) 1px, transparent 1px),
39
+ linear-gradient(rgba(36, 34, 29, 0.035) 1px, transparent 1px),
40
+ var(--three-body-card);
41
+ background-size: 24px 24px;
42
+ box-shadow: 0 22px 60px var(--three-body-shadow);
43
+ color: var(--three-body-ink);
44
+ }
45
+
46
+ .three-body-instrument-bar {
47
+ display: flex;
48
+ flex-wrap: wrap;
49
+ gap: 0.45rem;
50
+ justify-content: space-between;
51
+ padding-top: 0.15rem;
52
+ }
53
+
54
+ .theme-dark .three-body-root {
55
+ background:
56
+ radial-gradient(circle at 50% 38%, rgba(61, 79, 96, 0.18), transparent 42%),
57
+ linear-gradient(90deg, rgba(255, 255, 255, 0.035) 1px, transparent 1px),
58
+ linear-gradient(rgba(255, 255, 255, 0.035) 1px, transparent 1px),
59
+ var(--three-body-card);
60
+ background-size: auto, 26px 26px, 26px 26px, auto;
61
+ }
62
+
63
+ .three-body-stage {
64
+ overflow: hidden;
65
+ border: 1px solid var(--three-body-card-edge);
66
+ border-radius: 8px;
67
+ background: var(--three-body-space);
68
+ }
69
+
70
+ .three-body-sky {
71
+ position: relative;
72
+ min-height: 440px;
73
+ overflow: hidden;
74
+ background:
75
+ radial-gradient(circle at center, transparent 0 18%, rgba(36, 34, 29, 0.04) 18.2% 18.5%, transparent 18.8%),
76
+ linear-gradient(90deg, rgba(36, 34, 29, 0.065) 1px, transparent 1px),
77
+ linear-gradient(rgba(36, 34, 29, 0.065) 1px, transparent 1px),
78
+ var(--three-body-space);
79
+ background-size: auto, 32px 32px, 32px 32px, auto;
80
+ }
81
+
82
+ .theme-dark .three-body-sky {
83
+ background:
84
+ radial-gradient(circle at 18% 28%, rgba(89, 116, 135, 0.18), transparent 27%),
85
+ radial-gradient(circle at 82% 18%, rgba(160, 130, 87, 0.08), transparent 24%),
86
+ linear-gradient(90deg, rgba(255, 255, 255, 0.045) 1px, transparent 1px),
87
+ linear-gradient(rgba(255, 255, 255, 0.045) 1px, transparent 1px),
88
+ var(--three-body-space);
89
+ background-size: auto, auto, 34px 34px, 34px 34px, auto;
90
+ }
91
+
92
+ .three-body-sky canvas {
93
+ display: block;
94
+ width: 100%;
95
+ height: 100%;
96
+ min-height: 440px;
97
+ }
98
+
99
+ .three-body-presets,
100
+ .three-body-actions {
101
+ display: flex;
102
+ flex-wrap: wrap;
103
+ gap: 0.35rem;
104
+ min-width: 0;
105
+ }
106
+
107
+ .three-body-preset,
108
+ .three-body-action {
109
+ min-height: 32px;
110
+ padding: 0 0.72rem;
111
+ border: 1px solid var(--three-body-card-edge);
112
+ border-radius: 8px;
113
+ background: var(--three-body-overlay);
114
+ backdrop-filter: blur(12px);
115
+ color: var(--three-body-control);
116
+ cursor: pointer;
117
+ font-size: 0.72rem;
118
+ font-weight: 700;
119
+ letter-spacing: 0;
120
+ }
121
+
122
+ .three-body-preset.is-active,
123
+ .three-body-action:hover,
124
+ .three-body-preset:hover {
125
+ background: var(--three-body-overlay-strong);
126
+ color: var(--three-body-control-active);
127
+ }
128
+
129
+ .three-body-preset .short {
130
+ display: none;
131
+ }
132
+
133
+ .three-body-readout {
134
+ display: grid;
135
+ grid-template-columns: repeat(3, minmax(0, 1fr));
136
+ gap: 0;
137
+ opacity: 0.58;
138
+ transition: opacity 0.18s ease;
139
+ }
140
+
141
+ .three-body-readout:hover {
142
+ opacity: 1;
143
+ }
144
+
145
+ .three-body-readout div {
146
+ display: grid;
147
+ grid-template-columns: auto 1fr;
148
+ gap: 0.45rem;
149
+ align-items: baseline;
150
+ min-width: 0;
151
+ padding: 0.38rem 0.5rem;
152
+ border-top: 1px solid var(--three-body-card-edge);
153
+ border-right: 1px solid var(--three-body-card-edge);
154
+ background: var(--three-body-overlay);
155
+ backdrop-filter: blur(8px);
156
+ }
157
+
158
+ .three-body-readout div:last-child {
159
+ border-right: 0;
160
+ }
161
+
162
+ .three-body-readout span {
163
+ color: var(--three-body-muted);
164
+ font-size: 0.66rem;
165
+ }
166
+
167
+ .three-body-readout strong {
168
+ color: var(--three-body-control-active);
169
+ font-size: 0.78rem;
170
+ font-weight: 600;
171
+ }
172
+
173
+ .three-body-reticle {
174
+ position: absolute;
175
+ inset: 50% auto auto 50%;
176
+ width: 72px;
177
+ height: 72px;
178
+ border: 1px solid color-mix(in srgb, var(--three-body-control-active) 16%, transparent);
179
+ border-radius: 50%;
180
+ transform: translate(-50%, -50%);
181
+ pointer-events: none;
182
+ }
183
+
184
+ .three-body-reticle::before,
185
+ .three-body-reticle::after {
186
+ position: absolute;
187
+ inset: 50% auto auto 50%;
188
+ width: 142px;
189
+ height: 1px;
190
+ background: color-mix(in srgb, var(--three-body-control-active) 12%, transparent);
191
+ content: "";
192
+ transform: translate(-50%, -50%);
193
+ }
194
+
195
+ .three-body-reticle::after {
196
+ width: 1px;
197
+ height: 142px;
198
+ }
199
+
200
+ .three-body-console {
201
+ display: grid;
202
+ gap: 0.75rem;
203
+ align-content: center;
204
+ padding: 0.1rem;
205
+ }
206
+
207
+ .three-body-global-controls {
208
+ display: grid;
209
+ grid-template-columns: repeat(2, minmax(0, 1fr));
210
+ gap: 0.55rem;
211
+ padding: 0.25rem 0.05rem 0.7rem;
212
+ border-bottom: 1px solid var(--three-body-card-edge);
213
+ }
214
+
215
+ .three-body-body-controls {
216
+ display: grid;
217
+ grid-template-columns: repeat(3, minmax(0, 1fr));
218
+ gap: clamp(0.9rem, 2.5vw, 1.45rem);
219
+ }
220
+
221
+ .three-body-body-tabs {
222
+ display: none;
223
+ }
224
+
225
+ .three-body-panel {
226
+ position: relative;
227
+ display: grid;
228
+ gap: 0.58rem;
229
+ min-width: 0;
230
+ padding: 3.7rem 0 0;
231
+ border: 0;
232
+ border-radius: 0;
233
+ background: transparent;
234
+ overflow: visible;
235
+ }
236
+
237
+ .three-body-panel::before {
238
+ position: absolute;
239
+ inset: -0.35rem auto auto 0;
240
+ color: var(--body-color);
241
+ content: attr(data-body-label);
242
+ font-size: clamp(3rem, 6vw, 4.15rem);
243
+ font-weight: 700;
244
+ line-height: 1;
245
+ opacity: 0.11;
246
+ pointer-events: none;
247
+ }
248
+
249
+ .three-body-panel-title {
250
+ position: relative;
251
+ display: flex;
252
+ justify-content: flex-start;
253
+ min-height: 0;
254
+ }
255
+
256
+ .three-body-panel-title strong {
257
+ position: absolute;
258
+ top: -3.55rem;
259
+ left: 0.08rem;
260
+ font-size: clamp(3rem, 6vw, 4.15rem);
261
+ font-weight: 800;
262
+ line-height: 1;
263
+ opacity: 0.14;
264
+ pointer-events: none;
265
+ }
266
+
267
+ .three-body-scrub-row {
268
+ position: relative;
269
+ display: grid;
270
+ grid-template-columns: 3.2ch minmax(8ch, 1fr);
271
+ gap: 0.72rem;
272
+ align-items: center;
273
+ min-height: 54px;
274
+ padding: 0.5rem 0.25rem;
275
+ border: 1px solid transparent;
276
+ border-radius: 8px;
277
+ transition:
278
+ background 0.15s ease,
279
+ border-color 0.15s ease,
280
+ box-shadow 0.15s ease;
281
+ }
282
+
283
+ .three-body-scrub-row:hover {
284
+ border-color: transparent;
285
+ background: color-mix(in srgb, var(--three-body-overlay-strong) 28%, transparent);
286
+ box-shadow: 0 0 0 1px color-mix(in srgb, currentcolor 7%, transparent);
287
+ }
288
+
289
+ .three-body-scrub-row span {
290
+ color: var(--three-body-muted);
291
+ font-size: 0.95rem;
292
+ font-style: italic;
293
+ }
294
+
295
+ .three-body-scrub-input {
296
+ min-width: 0;
297
+ width: 100%;
298
+ height: 40px;
299
+ padding: 0 0.45rem 0 0.15rem;
300
+ border: 0;
301
+ border-bottom: 1px solid color-mix(in srgb, currentcolor 38%, transparent);
302
+ border-radius: 0;
303
+ background: transparent;
304
+ color: var(--three-body-control-active);
305
+ cursor: ew-resize;
306
+ font-size: clamp(1.12rem, 2vw, 1.42rem);
307
+ font-weight: 700;
308
+ outline: 0;
309
+ text-align: right;
310
+ }
311
+
312
+ .three-body-scrub-input:focus {
313
+ border-bottom-color: currentcolor;
314
+ background: color-mix(in srgb, var(--three-body-overlay-strong) 46%, transparent);
315
+ cursor: text;
316
+ }
317
+
318
+ @media (min-width: 760px) {
319
+ .three-body-root {
320
+ grid-template-areas:
321
+ "stage console"
322
+ "toolbar console";
323
+ grid-template-columns: minmax(0, 1.34fr) minmax(440px, 0.66fr);
324
+ gap: 1rem;
325
+ padding: 0.9rem;
326
+ align-items: stretch;
327
+ }
328
+
329
+ .three-body-stage {
330
+ grid-area: stage;
331
+ }
332
+
333
+ .three-body-console {
334
+ grid-area: console;
335
+ position: sticky;
336
+ top: 1rem;
337
+ min-height: 650px;
338
+ }
339
+
340
+ .three-body-instrument-bar {
341
+ grid-area: toolbar;
342
+ }
343
+
344
+ .three-body-sky,
345
+ .three-body-sky canvas {
346
+ min-height: 650px;
347
+ }
348
+
349
+ }
350
+
351
+ @media (max-width: 759px) {
352
+ .three-body-root {
353
+ gap: 0.55rem;
354
+ padding: 0.55rem;
355
+ }
356
+
357
+ .three-body-sky,
358
+ .three-body-sky canvas {
359
+ min-height: 310px;
360
+ }
361
+
362
+ .three-body-reticle {
363
+ width: 54px;
364
+ height: 54px;
365
+ }
366
+
367
+ .three-body-reticle::before {
368
+ width: 96px;
369
+ }
370
+
371
+ .three-body-reticle::after {
372
+ height: 96px;
373
+ }
374
+
375
+ .three-body-readout {
376
+ opacity: 0.74;
377
+ }
378
+
379
+ .three-body-readout div {
380
+ padding: 0.32rem 0.38rem;
381
+ }
382
+
383
+ .three-body-readout span,
384
+ .three-body-readout strong {
385
+ font-size: 0.62rem;
386
+ line-height: 1.1;
387
+ }
388
+
389
+ .three-body-console {
390
+ gap: 0.45rem;
391
+ align-content: start;
392
+ padding: 0;
393
+ }
394
+
395
+ .three-body-global-controls {
396
+ grid-template-columns: repeat(2, minmax(0, 1fr));
397
+ gap: 0.35rem;
398
+ padding: 0;
399
+ border-bottom: 0;
400
+ }
401
+
402
+ .three-body-global-controls .three-body-scrub-row {
403
+ min-height: 38px;
404
+ padding: 0.18rem 0.35rem;
405
+ }
406
+
407
+ .three-body-global-controls .three-body-scrub-input {
408
+ height: 30px;
409
+ font-size: 0.92rem;
410
+ }
411
+
412
+ .three-body-body-tabs {
413
+ display: grid;
414
+ grid-template-columns: repeat(3, 1fr);
415
+ gap: 0.35rem;
416
+ }
417
+
418
+ .three-body-tab {
419
+ min-height: 38px;
420
+ border: 1px solid var(--three-body-card-edge);
421
+ border-radius: 8px;
422
+ background: transparent;
423
+ color: var(--three-body-muted);
424
+ cursor: pointer;
425
+ font-size: 1.25rem;
426
+ font-weight: 800;
427
+ }
428
+
429
+ .three-body-tab.is-active {
430
+ background: var(--three-body-overlay-strong);
431
+ color: var(--three-body-control-active);
432
+ }
433
+
434
+ .three-body-body-controls {
435
+ grid-template-columns: 1fr;
436
+ min-height: 64px;
437
+ }
438
+
439
+ .three-body-panel {
440
+ display: none;
441
+ grid-template-columns: repeat(3, minmax(0, 1fr));
442
+ gap: 0.25rem;
443
+ padding: 0;
444
+ }
445
+
446
+ .three-body-panel.is-active {
447
+ display: grid;
448
+ }
449
+
450
+ .three-body-panel::before,
451
+ .three-body-panel-title {
452
+ display: none;
453
+ }
454
+
455
+ .three-body-scrub-row {
456
+ grid-template-columns: 1fr;
457
+ gap: 0.12rem;
458
+ min-height: 58px;
459
+ padding: 0.28rem 0.38rem;
460
+ }
461
+
462
+ .three-body-scrub-row span {
463
+ font-size: 0.72rem;
464
+ }
465
+
466
+ .three-body-scrub-input {
467
+ height: 32px;
468
+ padding: 0;
469
+ font-size: 1rem;
470
+ text-align: left;
471
+ }
472
+
473
+ .three-body-instrument-bar {
474
+ display: grid;
475
+ grid-template-columns: minmax(0, 1fr) auto;
476
+ gap: 0.35rem;
477
+ padding-top: 0;
478
+ }
479
+
480
+ .three-body-presets,
481
+ .three-body-actions {
482
+ display: grid;
483
+ grid-auto-flow: column;
484
+ grid-auto-columns: minmax(0, 1fr);
485
+ gap: 0.25rem;
486
+ }
487
+
488
+ .three-body-preset,
489
+ .three-body-action {
490
+ min-height: 34px;
491
+ padding: 0 0.42rem;
492
+ font-size: 0.66rem;
493
+ white-space: nowrap;
494
+ }
495
+
496
+ .three-body-preset .full {
497
+ display: none;
498
+ }
499
+
500
+ .three-body-preset .short {
501
+ display: inline;
502
+ }
503
+ }
package/src/tools.ts CHANGED
@@ -16,6 +16,8 @@ import { PHASE_DIAGRAM_CRITICAL_POINTS_TOOL } from './tool/phase-diagram-critica
16
16
  import { TWIN_PARADOX_VISUALIZER_TOOL } from './tool/twin-paradox-visualizer/index';
17
17
  import { MANDELBROT_FRACTAL_TOOL } from './tool/mandelbrot-fractal/index';
18
18
  import { PLANET_ATMOSPHERE_SURVIVAL_TOOL } from './tool/planet-atmosphere-survival/index';
19
+ import { THREE_BODY_PROBLEM_TOOL } from './tool/three-body-problem/index';
20
+ import { ROCHE_LIMIT_SATELLITE_DISRUPTION_TOOL } from './tool/roche-limit-satellite-disruption/index';
19
21
 
20
22
  export const ALL_TOOLS: ToolDefinition[] = [
21
23
  COLONY_COUNTER_TOOL,
@@ -34,4 +36,6 @@ export const ALL_TOOLS: ToolDefinition[] = [
34
36
  TWIN_PARADOX_VISUALIZER_TOOL,
35
37
  MANDELBROT_FRACTAL_TOOL,
36
38
  PLANET_ATMOSPHERE_SURVIVAL_TOOL,
39
+ THREE_BODY_PROBLEM_TOOL,
40
+ ROCHE_LIMIT_SATELLITE_DISRUPTION_TOOL,
37
41
  ];