@jjlmoya/utils-science 1.32.0 → 1.34.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/planet-atmosphere-survival/bibliography.astro +14 -0
- package/src/tool/planet-atmosphere-survival/bibliography.ts +16 -0
- package/src/tool/planet-atmosphere-survival/component.astro +404 -0
- package/src/tool/planet-atmosphere-survival/entry.ts +26 -0
- package/src/tool/planet-atmosphere-survival/i18n/de.ts +255 -0
- package/src/tool/planet-atmosphere-survival/i18n/en.ts +255 -0
- package/src/tool/planet-atmosphere-survival/i18n/es.ts +255 -0
- package/src/tool/planet-atmosphere-survival/i18n/fr.ts +255 -0
- package/src/tool/planet-atmosphere-survival/i18n/id.ts +255 -0
- package/src/tool/planet-atmosphere-survival/i18n/it.ts +255 -0
- package/src/tool/planet-atmosphere-survival/i18n/ja.ts +255 -0
- package/src/tool/planet-atmosphere-survival/i18n/ko.ts +255 -0
- package/src/tool/planet-atmosphere-survival/i18n/nl.ts +255 -0
- package/src/tool/planet-atmosphere-survival/i18n/pl.ts +255 -0
- package/src/tool/planet-atmosphere-survival/i18n/pt.ts +255 -0
- package/src/tool/planet-atmosphere-survival/i18n/ru.ts +255 -0
- package/src/tool/planet-atmosphere-survival/i18n/sv.ts +255 -0
- package/src/tool/planet-atmosphere-survival/i18n/tr.ts +255 -0
- package/src/tool/planet-atmosphere-survival/i18n/zh.ts +255 -0
- package/src/tool/planet-atmosphere-survival/index.ts +11 -0
- package/src/tool/planet-atmosphere-survival/logic.ts +201 -0
- package/src/tool/planet-atmosphere-survival/planet-atmosphere-survival-calculator.css +494 -0
- package/src/tool/planet-atmosphere-survival/seo.astro +15 -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,274 @@
|
|
|
1
|
+
import { ThreeBodyEngine, THREE_BODY_PRESETS, type BodyState } from './logic/ThreeBodyEngine';
|
|
2
|
+
|
|
3
|
+
export function initThreeBodyProblem() {
|
|
4
|
+
const root = document.getElementById('three-body-root');
|
|
5
|
+
if (root) {
|
|
6
|
+
const canvas = document.getElementById('three-body-canvas') as HTMLCanvasElement;
|
|
7
|
+
const controls = document.getElementById('three-body-controls');
|
|
8
|
+
const toggleButton = document.getElementById('three-body-toggle');
|
|
9
|
+
const resetButton = document.getElementById('three-body-reset');
|
|
10
|
+
const speedInput = document.getElementById('three-body-speed') as HTMLInputElement;
|
|
11
|
+
const trailInput = document.getElementById('three-body-trail') as HTMLInputElement;
|
|
12
|
+
const energyValue = document.getElementById('three-body-energy');
|
|
13
|
+
const separationValue = document.getElementById('three-body-separation');
|
|
14
|
+
const centerValue = document.getElementById('three-body-center');
|
|
15
|
+
const presetButtons = Array.from(document.querySelectorAll('.three-body-preset'));
|
|
16
|
+
const tabButtons = Array.from(document.querySelectorAll('.three-body-tab'));
|
|
17
|
+
const engine = new ThreeBodyEngine();
|
|
18
|
+
let activePreset = THREE_BODY_PRESETS[0];
|
|
19
|
+
let bodies = engine.cloneBodies(activePreset.bodies);
|
|
20
|
+
let trails = bodies.map(() => [] as Array<{ x: number; y: number }>);
|
|
21
|
+
let isRunning = true;
|
|
22
|
+
let animationFrame = 0;
|
|
23
|
+
let zoom = activePreset.zoom;
|
|
24
|
+
let activeBodyIndex = 0;
|
|
25
|
+
const labels = {
|
|
26
|
+
pause: root.dataset.pauseLabel ?? 'Pause',
|
|
27
|
+
play: root.dataset.playLabel ?? 'Play',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function setPreset(presetId: string) {
|
|
31
|
+
const selectedPreset = THREE_BODY_PRESETS.find((preset) => preset.id === presetId) ?? THREE_BODY_PRESETS[0];
|
|
32
|
+
activePreset = selectedPreset;
|
|
33
|
+
bodies = engine.cloneBodies(selectedPreset.bodies);
|
|
34
|
+
trails = bodies.map(() => []);
|
|
35
|
+
zoom = selectedPreset.zoom;
|
|
36
|
+
trailInput.value = selectedPreset.trailLength.toString();
|
|
37
|
+
renderControls();
|
|
38
|
+
updatePresetButtons(presetId);
|
|
39
|
+
}
|
|
40
|
+
function updatePresetButtons(presetId: string) {
|
|
41
|
+
presetButtons.forEach((button) => {
|
|
42
|
+
button.classList.toggle('is-active', button.getAttribute('data-preset') === presetId);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function renderControls() {
|
|
46
|
+
if (!controls) return;
|
|
47
|
+
controls.innerHTML = '';
|
|
48
|
+
bodies.forEach((body, index) => {
|
|
49
|
+
controls.appendChild(createBodyPanel(body, index));
|
|
50
|
+
});
|
|
51
|
+
bindBodyInputs(controls.querySelectorAll('input'));
|
|
52
|
+
bindScrubbableInputs(controls.querySelectorAll('.three-body-scrub-input'));
|
|
53
|
+
}
|
|
54
|
+
function createBodyPanel(body: BodyState, index: number) {
|
|
55
|
+
const panel = document.createElement('article');
|
|
56
|
+
panel.className = 'three-body-panel';
|
|
57
|
+
panel.classList.toggle('is-active', index === activeBodyIndex);
|
|
58
|
+
panel.style.setProperty('--body-color', body.color);
|
|
59
|
+
panel.dataset.bodyLabel = body.label;
|
|
60
|
+
panel.dataset.bodyIndex = index.toString();
|
|
61
|
+
panel.innerHTML = `
|
|
62
|
+
<div class="three-body-panel-title">
|
|
63
|
+
<strong style="color: ${body.color}">${body.label}</strong>
|
|
64
|
+
</div>
|
|
65
|
+
${renderScrubInput({ index, field: 'mass', label: 'm', min: 0.2, max: 4, value: body.mass, step: 0.05, color: body.color })}
|
|
66
|
+
${renderScrubInput({ index, field: 'vx', label: 'v_x', min: -1.6, max: 1.6, value: body.vx, step: 0.01, color: body.color })}
|
|
67
|
+
${renderScrubInput({ index, field: 'vy', label: 'v_y', min: -1.6, max: 1.6, value: body.vy, step: 0.01, color: body.color })}
|
|
68
|
+
`;
|
|
69
|
+
return panel;
|
|
70
|
+
}
|
|
71
|
+
function bindBodyInputs(inputs: NodeListOf<Element>) {
|
|
72
|
+
inputs.forEach((input) => {
|
|
73
|
+
input.addEventListener('input', () => {
|
|
74
|
+
const slider = input as HTMLInputElement;
|
|
75
|
+
const bodyIndex = Number(slider.dataset.bodyIndex);
|
|
76
|
+
const field = slider.dataset.field as keyof BodyState;
|
|
77
|
+
const value = clampToInput(slider, Number(slider.value));
|
|
78
|
+
if (Number.isFinite(bodyIndex) && field) {
|
|
79
|
+
bodies[bodyIndex] = { ...bodies[bodyIndex], [field]: value };
|
|
80
|
+
trails[bodyIndex] = [];
|
|
81
|
+
slider.value = formatInputValue(value, slider);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function setActiveBody(index: number) {
|
|
87
|
+
activeBodyIndex = index;
|
|
88
|
+
tabButtons.forEach((button) => {
|
|
89
|
+
button.classList.toggle('is-active', Number((button as HTMLElement).dataset.bodyTab) === index);
|
|
90
|
+
});
|
|
91
|
+
controls?.querySelectorAll('.three-body-panel').forEach((panel) => {
|
|
92
|
+
panel.classList.toggle('is-active', Number((panel as HTMLElement).dataset.bodyIndex) === index);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
interface ScrubInputConfig {
|
|
96
|
+
index: number;
|
|
97
|
+
field: string;
|
|
98
|
+
label: string;
|
|
99
|
+
min: number;
|
|
100
|
+
max: number;
|
|
101
|
+
value: number;
|
|
102
|
+
step: number;
|
|
103
|
+
color: string;
|
|
104
|
+
}
|
|
105
|
+
function renderScrubInput(config: ScrubInputConfig) {
|
|
106
|
+
const value = formatInputValue(config.value, { dataset: { step: config.step.toString() } } as HTMLInputElement);
|
|
107
|
+
return `
|
|
108
|
+
<label class="three-body-scrub-row">
|
|
109
|
+
<span>${config.label}</span>
|
|
110
|
+
<input class="three-body-scrub-input" style="color: ${config.color}" data-body-index="${config.index}" data-field="${config.field}" data-min="${config.min}" data-max="${config.max}" data-step="${config.step}" inputmode="decimal" value="${value}" />
|
|
111
|
+
</label>
|
|
112
|
+
`;
|
|
113
|
+
}
|
|
114
|
+
function formatInputValue(value: number, input: HTMLInputElement) {
|
|
115
|
+
const step = Number(input.dataset.step) || 0.01;
|
|
116
|
+
const decimals = step >= 1 ? 0 : Math.max(0, step.toString().split('.')[1]?.length ?? 2);
|
|
117
|
+
return value.toFixed(decimals);
|
|
118
|
+
}
|
|
119
|
+
function clampToInput(input: HTMLInputElement, value: number) {
|
|
120
|
+
const min = Number(input.dataset.min);
|
|
121
|
+
const max = Number(input.dataset.max);
|
|
122
|
+
if (!Number.isFinite(value)) return Number(input.value) || 0;
|
|
123
|
+
return Math.min(max, Math.max(min, value));
|
|
124
|
+
}
|
|
125
|
+
function bindScrubbableInputs(inputs: NodeListOf<Element>) {
|
|
126
|
+
inputs.forEach((inputElement) => {
|
|
127
|
+
const input = inputElement as HTMLInputElement;
|
|
128
|
+
let startX = 0;
|
|
129
|
+
let startValue = 0;
|
|
130
|
+
let dragged = false;
|
|
131
|
+
|
|
132
|
+
input.addEventListener('pointerdown', (event) => {
|
|
133
|
+
startX = event.clientX;
|
|
134
|
+
startValue = Number(input.value) || 0;
|
|
135
|
+
dragged = false;
|
|
136
|
+
input.setPointerCapture(event.pointerId);
|
|
137
|
+
});
|
|
138
|
+
input.addEventListener('pointermove', (event) => {
|
|
139
|
+
if (!input.hasPointerCapture(event.pointerId)) return;
|
|
140
|
+
const delta = event.clientX - startX;
|
|
141
|
+
if (Math.abs(delta) < 2) return;
|
|
142
|
+
dragged = true;
|
|
143
|
+
const step = Number(input.dataset.step) || 0.01;
|
|
144
|
+
const nextValue = clampToInput(input, startValue + delta * step);
|
|
145
|
+
input.value = formatInputValue(nextValue, input);
|
|
146
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
147
|
+
});
|
|
148
|
+
input.addEventListener('pointerup', (event) => {
|
|
149
|
+
if (input.hasPointerCapture(event.pointerId)) input.releasePointerCapture(event.pointerId);
|
|
150
|
+
if (dragged) input.blur();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function draw() {
|
|
155
|
+
const context = canvas.getContext('2d');
|
|
156
|
+
if (!context) return;
|
|
157
|
+
|
|
158
|
+
const dpr = window.devicePixelRatio || 1;
|
|
159
|
+
const width = canvas.clientWidth;
|
|
160
|
+
const height = canvas.clientHeight;
|
|
161
|
+
canvas.width = width * dpr;
|
|
162
|
+
canvas.height = height * dpr;
|
|
163
|
+
context.scale(dpr, dpr);
|
|
164
|
+
context.clearRect(0, 0, width, height);
|
|
165
|
+
|
|
166
|
+
const originX = width / 2;
|
|
167
|
+
const originY = height / 2;
|
|
168
|
+
const origin = { x: originX, y: originY };
|
|
169
|
+
drawGrid(context, width, height, origin);
|
|
170
|
+
bodies.forEach((body, index) => drawBody(context, body, trails[index], origin));
|
|
171
|
+
}
|
|
172
|
+
function drawBody(context: CanvasRenderingContext2D, body: BodyState, trail: Array<{ x: number; y: number }>, origin: { x: number; y: number }) {
|
|
173
|
+
drawTrail(context, body, trail, origin);
|
|
174
|
+
drawBodyGlow(context, body, origin);
|
|
175
|
+
}
|
|
176
|
+
function drawTrail(context: CanvasRenderingContext2D, body: BodyState, trail: Array<{ x: number; y: number }>, origin: { x: number; y: number }) {
|
|
177
|
+
context.beginPath();
|
|
178
|
+
trail.forEach((point, pointIndex) => {
|
|
179
|
+
const screenX = origin.x + point.x * zoom;
|
|
180
|
+
const screenY = origin.y + point.y * zoom;
|
|
181
|
+
if (pointIndex === 0) context.moveTo(screenX, screenY);
|
|
182
|
+
else context.lineTo(screenX, screenY);
|
|
183
|
+
});
|
|
184
|
+
context.strokeStyle = body.color;
|
|
185
|
+
context.globalAlpha = 0.82;
|
|
186
|
+
context.lineWidth = 1.85;
|
|
187
|
+
context.shadowColor = body.color;
|
|
188
|
+
context.shadowBlur = 2.5;
|
|
189
|
+
context.stroke();
|
|
190
|
+
context.shadowBlur = 0;
|
|
191
|
+
context.globalAlpha = 1;
|
|
192
|
+
}
|
|
193
|
+
function drawBodyGlow(context: CanvasRenderingContext2D, body: BodyState, origin: { x: number; y: number }) {
|
|
194
|
+
const x = origin.x + body.x * zoom;
|
|
195
|
+
const y = origin.y + body.y * zoom;
|
|
196
|
+
const radius = Math.max(4, Math.sqrt(body.mass) * 5);
|
|
197
|
+
const glow = context.createRadialGradient(x, y, 2, x, y, radius * 3);
|
|
198
|
+
glow.addColorStop(0, body.color);
|
|
199
|
+
glow.addColorStop(1, 'rgba(255,255,255,0)');
|
|
200
|
+
context.fillStyle = glow;
|
|
201
|
+
context.beginPath();
|
|
202
|
+
context.arc(x, y, radius * 3, 0, Math.PI * 2);
|
|
203
|
+
context.fill();
|
|
204
|
+
context.fillStyle = body.color;
|
|
205
|
+
context.beginPath();
|
|
206
|
+
context.arc(x, y, radius, 0, Math.PI * 2);
|
|
207
|
+
context.fill();
|
|
208
|
+
}
|
|
209
|
+
function drawGrid(context: CanvasRenderingContext2D, width: number, height: number, origin: { x: number; y: number }) {
|
|
210
|
+
const isDark = document.documentElement.classList.contains('theme-dark') || document.body.classList.contains('theme-dark');
|
|
211
|
+
context.strokeStyle = isDark ? 'rgba(255,255,255,0.09)' : 'rgba(67, 56, 43, 0.12)';
|
|
212
|
+
context.lineWidth = 1;
|
|
213
|
+
for (let x = origin.x % 48; x < width; x += 48) {
|
|
214
|
+
context.beginPath();
|
|
215
|
+
context.moveTo(x, 0);
|
|
216
|
+
context.lineTo(x, height);
|
|
217
|
+
context.stroke();
|
|
218
|
+
}
|
|
219
|
+
for (let y = origin.y % 48; y < height; y += 48) {
|
|
220
|
+
context.beginPath();
|
|
221
|
+
context.moveTo(0, y);
|
|
222
|
+
context.lineTo(width, y);
|
|
223
|
+
context.stroke();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function updateReadout() {
|
|
227
|
+
const metrics = engine.calculateMetrics(bodies);
|
|
228
|
+
if (energyValue) energyValue.textContent = metrics.totalEnergy.toFixed(3);
|
|
229
|
+
if (separationValue) separationValue.textContent = `${metrics.minSeparation.toFixed(2)} - ${metrics.maxSeparation.toFixed(2)}`;
|
|
230
|
+
if (centerValue) centerValue.textContent = `${metrics.centerOfMassX.toFixed(2)}, ${metrics.centerOfMassY.toFixed(2)}`;
|
|
231
|
+
}
|
|
232
|
+
function tick() {
|
|
233
|
+
if (isRunning) {
|
|
234
|
+
const speed = Number(speedInput.value) || 1;
|
|
235
|
+
for (let i = 0; i < 3; i += 1) {
|
|
236
|
+
const snapshot = engine.step(bodies, activePreset.timeStep * speed);
|
|
237
|
+
bodies = snapshot.bodies;
|
|
238
|
+
}
|
|
239
|
+
const trailLimit = Number(trailInput.value) || activePreset.trailLength;
|
|
240
|
+
trails.forEach((trail, index) => {
|
|
241
|
+
trail.push({ x: bodies[index].x, y: bodies[index].y });
|
|
242
|
+
if (trail.length > trailLimit) trail.shift();
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
updateReadout();
|
|
246
|
+
draw();
|
|
247
|
+
animationFrame = window.requestAnimationFrame(tick);
|
|
248
|
+
}
|
|
249
|
+
presetButtons.forEach((button) => {
|
|
250
|
+
button.addEventListener('click', () => {
|
|
251
|
+
setPreset(button.getAttribute('data-preset') ?? 'figureEight');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
tabButtons.forEach((button) => {
|
|
255
|
+
button.addEventListener('click', () => {
|
|
256
|
+
setActiveBody(Number((button as HTMLElement).dataset.bodyTab) || 0);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
toggleButton?.addEventListener('click', () => {
|
|
260
|
+
isRunning = !isRunning;
|
|
261
|
+
toggleButton.textContent = isRunning ? labels.pause : labels.play;
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
resetButton?.addEventListener('click', () => {
|
|
265
|
+
setPreset(activePreset.id);
|
|
266
|
+
});
|
|
267
|
+
bindScrubbableInputs(document.querySelectorAll('.three-body-global-controls .three-body-scrub-input'));
|
|
268
|
+
setPreset('figureEight');
|
|
269
|
+
tick();
|
|
270
|
+
document.addEventListener('astro:before-swap', () => {
|
|
271
|
+
window.cancelAnimationFrame(animationFrame);
|
|
272
|
+
}, { once: true });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } 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
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && <SharedBibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'A remarkable periodic solution of the three-body problem in the case of equal masses',
|
|
6
|
+
url: 'https://arxiv.org/abs/math/0011268',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'Three-body problem',
|
|
10
|
+
url: 'https://en.wikipedia.org/wiki/Three-body_problem',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'Solar System Dynamics',
|
|
14
|
+
url: 'https://www.solarsystemdynamics.info/',
|
|
15
|
+
},
|
|
16
|
+
];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
import './three-body-problem-simulator.css';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
ui: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { ui } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<div
|
|
12
|
+
class="three-body-root"
|
|
13
|
+
id="three-body-root"
|
|
14
|
+
data-pause-label={ui.pauseButton}
|
|
15
|
+
data-play-label={ui.playButton}
|
|
16
|
+
><section class="three-body-stage" aria-label={ui.canvasLabel}>
|
|
17
|
+
<div class="three-body-sky">
|
|
18
|
+
<canvas id="three-body-canvas"></canvas>
|
|
19
|
+
<div class="three-body-reticle" aria-hidden="true"></div>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="three-body-readout">
|
|
22
|
+
<div>
|
|
23
|
+
<span>E</span>
|
|
24
|
+
<strong id="three-body-energy">0.00</strong>
|
|
25
|
+
</div>
|
|
26
|
+
<div>
|
|
27
|
+
<span>R</span>
|
|
28
|
+
<strong id="three-body-separation">0.00</strong>
|
|
29
|
+
</div>
|
|
30
|
+
<div>
|
|
31
|
+
<span>COM</span>
|
|
32
|
+
<strong id="three-body-center">0.00, 0.00</strong>
|
|
33
|
+
</div>
|
|
34
|
+
</div></section>
|
|
35
|
+
<section class="three-body-console">
|
|
36
|
+
<div class="three-body-global-controls">
|
|
37
|
+
<label class="three-body-scrub-row">
|
|
38
|
+
<span>dt</span>
|
|
39
|
+
<input id="three-body-speed" class="three-body-scrub-input" inputmode="decimal" data-min="0.25" data-max="2.5" data-step="0.05" value="1.00" />
|
|
40
|
+
</label>
|
|
41
|
+
<label class="three-body-scrub-row">
|
|
42
|
+
<span>trail</span>
|
|
43
|
+
<input id="three-body-trail" class="three-body-scrub-input" inputmode="numeric" data-min="120" data-max="900" data-step="20" value="720" />
|
|
44
|
+
</label>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="three-body-body-tabs" aria-label="Body selector">
|
|
47
|
+
<button class="three-body-tab is-active" type="button" data-body-tab="0">A</button>
|
|
48
|
+
<button class="three-body-tab" type="button" data-body-tab="1">B</button>
|
|
49
|
+
<button class="three-body-tab" type="button" data-body-tab="2">C</button>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="three-body-body-controls" id="three-body-controls"></div>
|
|
52
|
+
</section>
|
|
53
|
+
<div class="three-body-instrument-bar">
|
|
54
|
+
<div class="three-body-presets" aria-label={ui.presetsLabel}>
|
|
55
|
+
<button class="three-body-preset is-active" type="button" data-preset="figureEight"><span class="full">{ui.figureEightPreset}</span><span class="short">Fig 8</span></button>
|
|
56
|
+
<button class="three-body-preset" type="button" data-preset="lagrange"><span class="full">{ui.lagrangePreset}</span><span class="short">Lagrange</span></button>
|
|
57
|
+
<button class="three-body-preset" type="button" data-preset="slingshot"><span class="full">{ui.slingshotPreset}</span><span class="short">Shot</span></button>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="three-body-actions">
|
|
60
|
+
<button class="three-body-action" type="button" id="three-body-toggle">{ui.pauseButton}</button>
|
|
61
|
+
<button class="three-body-action" type="button" id="three-body-reset">{ui.resetButton}</button>
|
|
62
|
+
</div></div></div>
|
|
63
|
+
|
|
64
|
+
<script>
|
|
65
|
+
import { initThreeBodyProblem } from './app';
|
|
66
|
+
|
|
67
|
+
initThreeBodyProblem();
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ScienceToolEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const threeBodyProblem: ScienceToolEntry = {
|
|
4
|
+
id: 'three-body-problem-simulator',
|
|
5
|
+
icons: {
|
|
6
|
+
bg: 'mdi:orbit-variant',
|
|
7
|
+
fg: 'mdi:vector-polyline',
|
|
8
|
+
},
|
|
9
|
+
i18n: {
|
|
10
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
11
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
12
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
13
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
14
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
15
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
16
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
17
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
18
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
19
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
20
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
21
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
22
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
23
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
24
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
2
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
3
|
+
|
|
4
|
+
const slug = 'dreikoerperproblem-simulator';
|
|
5
|
+
const title = 'Dreikörperproblem Simulator';
|
|
6
|
+
const description = 'Simuliere drei Gravitationskörper in einer zweidimensionalen Ebene mit einstellbaren Massen, Geschwindigkeitsvektoren, Spuren und stabilen oder chaotischen Voreinstellungen.';
|
|
7
|
+
|
|
8
|
+
const howTo = [
|
|
9
|
+
{
|
|
10
|
+
name: 'Wähle eine orbitale Voreinstellung',
|
|
11
|
+
text: 'Beginne mit der Achterbahn für einen stabilen Referenzfall, dem Lagrange-Dreieck für ein rotierendes Gleichgewicht oder der Schleudervoreinstellung für eine sichtbar chaotische Begegnung.',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'Passe Massen und Geschwindigkeitsvektoren an',
|
|
15
|
+
text: 'Nutze die Schieberegler für jeden Körper, um Masse und anfängliche Geschwindigkeitskomponenten zu ändern. Kleine Änderungen können ein Muster bewahren, verformen oder vollständig zerstören.',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'Lies die Diagnosewerte',
|
|
19
|
+
text: 'Beobachte die Gesamtenergie, die nächste und entfernteste Entfernung sowie den Massenschwerpunkt, um zu verstehen, ob die numerische Umlaufbahn gebunden bleibt oder auseinanderdriftet.',
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const faq = [
|
|
24
|
+
{
|
|
25
|
+
question: 'Was ist das Dreikörperproblem?',
|
|
26
|
+
answer: 'Das Dreikörperproblem fragt, wie sich drei Massen bewegen, wenn jeder Körper die beiden anderen gravitativ anzieht. Anders als beim Zweikörperproblem gibt es keine allgemeine geschlossene Gleichung, die jede mögliche Konfiguration löst, daher werden die meisten praktischen Fälle mit numerischer Integration untersucht.',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
question: 'Warum sind Dreikörperumlaufbahnen instabil?',
|
|
30
|
+
answer: 'Viele Dreikörpersysteme sind empfindlich gegenüber Anfangsbedingungen. Eine winzige Änderung der Geschwindigkeit, Position oder Masse verändert den Zeitpunkt naher Begegnungen, und diese Begegnungen können dramatisch Energie austauschen. Das Ergebnis ist ein System, das eine Weile gebunden bleiben und dann plötzlich einen Körper ausstoßen kann.',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
question: 'Was zeigt die Achterbahn-Voreinstellung?',
|
|
34
|
+
answer: 'Die Achterbahn-Voreinstellung ist eine berühmte periodische Lösung für drei gleiche Massen. Jeder Körper folgt derselben Bahn mit einem Phasenversatz und zeigt, dass das Dreikörperproblem elegante stabile Inseln innerhalb einer viel größeren chaotischen Landschaft enthalten kann.',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
question: 'Ist dies ein physikalisch exakter Astronomie-Simulator?',
|
|
38
|
+
answer: 'Dieses Werkzeug verwendet ein abgeschwächtes newtonsches Modell und einen symplektischen Geschwindigkeits-Verlet-Schritt, damit die Bewegung für das Lernen treu und stabil wirkt. Es ist für die interaktive Erkundung konzipiert, nicht für die hochpräzise Ephemeridenvorhersage.',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
question: 'Wie sollte ich die Gesamtenergie interpretieren?',
|
|
42
|
+
answer: 'Negative Gesamtenergie deutet normalerweise auf ein gebundenes System hin, während Energie nahe Null eine Flucht erleichtern kann. In einer numerischen Simulation warnt eine große Energiedrift auch davor, dass der Zeitschritt oder die Begegnungsgeometrie den Integrator überlastet.',
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
export const content: ToolLocaleContent = {
|
|
47
|
+
slug,
|
|
48
|
+
title,
|
|
49
|
+
description,
|
|
50
|
+
ui: {
|
|
51
|
+
title,
|
|
52
|
+
kicker: 'Gravitations-Chaos-Labor',
|
|
53
|
+
canvasLabel: 'Interaktive zweidimensionale Dreikörper-Orbit-Leinwand',
|
|
54
|
+
presetsLabel: 'Orbitale Voreinstellungen',
|
|
55
|
+
figureEightPreset: 'Achterbahn',
|
|
56
|
+
lagrangePreset: 'Lagrange-Dreieck',
|
|
57
|
+
slingshotPreset: 'Schleuder',
|
|
58
|
+
pauseButton: 'Pause',
|
|
59
|
+
playButton: 'Abspielen',
|
|
60
|
+
resetButton: 'Zurücksetzen',
|
|
61
|
+
speedLabel: 'Simulationsgeschwindigkeit',
|
|
62
|
+
trailLabel: 'Spurlänge',
|
|
63
|
+
massLabel: 'Masse',
|
|
64
|
+
velocityXLabel: 'Geschwindigkeit X',
|
|
65
|
+
velocityYLabel: 'Geschwindigkeit Y',
|
|
66
|
+
energyLabel: 'Gesamtenergie',
|
|
67
|
+
separationLabel: 'Entfernungsbereich',
|
|
68
|
+
centerMassLabel: 'Massenschwerpunkt',
|
|
69
|
+
},
|
|
70
|
+
seo: [
|
|
71
|
+
{
|
|
72
|
+
type: 'title',
|
|
73
|
+
text: 'Interaktiver Dreikörperproblem-Simulator für orbitales Chaos',
|
|
74
|
+
level: 2,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
type: 'paragraph',
|
|
78
|
+
html: 'Das Dreikörperproblem ist eine der klarsten Demonstrationen dafür, dass einfache Gesetze komplizierte Bewegungen erzeugen können. Die newtonsche Gravitation liefert eine kompakte Kraftregel, aber sobald ein dritter massiver Körper zum System hinzukommt, formt jeder Orbit kontinuierlich die beiden anderen um. Dieser Simulator lässt dich direkt mit dieser Instabilität experimentieren: Wähle eine bekannte Konfiguration, passe Massen und Geschwindigkeitsvektoren an und beobachte, ob die Körper eine sich wiederholende Umlaufbahn, ein rotierendes Dreieck oder ein chaotisches Streuereignis bilden.',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: 'title',
|
|
82
|
+
text: 'Was die Voreinstellungen zeigen',
|
|
83
|
+
level: 3,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: 'table',
|
|
87
|
+
headers: ['Voreinstellung', 'Physikalische Idee', 'Worauf achten'],
|
|
88
|
+
rows: [
|
|
89
|
+
['<strong>Achterbahn</strong>', 'Eine periodische Lösung mit gleichen Massen, bei der alle drei Körper dieselbe Schleife teilen.', 'Der Orbit bleibt nur organisiert, wenn Symmetrie und Geschwindigkeitsgleichgewicht sorgfältig erhalten bleiben.'],
|
|
90
|
+
['<strong>Lagrange-Dreieck</strong>', 'Drei Körper besetzen ein gleichseitiges Dreieck, das sich um den Massenschwerpunkt dreht.', 'Massenbilanz und Tangentialgeschwindigkeit verhindern, dass das Dreieck nach innen kollabiert.'],
|
|
91
|
+
['<strong>Schleuder</strong>', 'Eine nahe Begegnung überträgt Energie zwischen den Körpern.', 'Ein Körper kann an Geschwindigkeit gewinnen, während ein anderer stärker gebunden wird, was zeigt, warum chaotische Ausstöße auftreten.'],
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
type: 'title',
|
|
96
|
+
text: 'Warum kleine Änderungen wichtig sind',
|
|
97
|
+
level: 3,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
type: 'paragraph',
|
|
101
|
+
html: 'Bei einem Zweikörper-Orbit bieten der Massenschwerpunkt und die Orbitalellipse ein stabiles geometrisches Bild. In einem Dreikörpersystem wirken nahe Vorbeiflüge wie gravitative Verhandlungen: Ein Körper kann orbitale Energie ausleihen, scharf die Richtung ändern oder eine geordnete Schleife in ein Streuereignis verwandeln. Diese Empfindlichkeit ist der Grund, warum reale astrophysikalische Systeme wie Dreifachsterne, Planeten-Mond-Begegnungen und frühe Sonnensystem-Planetesimale oft numerische Integration erfordern und nicht eine einzige einfache Formel.',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
type: 'title',
|
|
105
|
+
text: 'Wie man die Diagnose verwendet',
|
|
106
|
+
level: 3,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
type: 'list',
|
|
110
|
+
items: [
|
|
111
|
+
'<strong>Gesamtenergie</strong> hilft dir zu beurteilen, ob das System gebunden ist und ob die numerische Integration stabil bleibt.',
|
|
112
|
+
'<strong>Entfernungsbereich</strong> zeigt die nächsten und entferntesten Paarabstände, wodurch Beinahekollisionen und Ausstöße leicht zu erkennen sind.',
|
|
113
|
+
'<strong>Massenschwerpunkt</strong> sollte relativ stabil bleiben, wenn der anfängliche Impuls ausgeglichen ist; eine Drift deutet auf eine absichtlich asymmetrische Konfiguration oder einen geänderten Geschwindigkeitsvektor hin.',
|
|
114
|
+
'<strong>Spurlänge</strong> offenbart die langfristige Struktur: Kurze Spuren betonen die aktuelle Interaktion, während lange Spuren sich wiederholende Schleifen und langsame orbitale Präzession sichtbar machen.',
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
type: 'title',
|
|
119
|
+
text: 'Im Werkzeug verwendetes numerisches Modell',
|
|
120
|
+
level: 3,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
type: 'paragraph',
|
|
124
|
+
html: 'Der Simulator verwendet die newtonsche invers-quadratische Anziehung mit einem kleinen Glättungsterm, der visuelle Explosionen bei extrem nahen Vorbeiflügen verhindert. Die Bewegung wird mit einem Geschwindigkeits-Verlet-Schritt vorangetrieben, einer gängigen Wahl für Orbit-Demonstrationen, da er das Energieverhalten besser handhabt als ein einfaches Euler-Update. Das Ergebnis ist ein reaktionsschnelles Bildungsmodell, das das qualitative Verhalten des Dreikörperproblems sichtbar macht, ohne professionelle Himmelsmechanik-Software ersetzen zu wollen.',
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
faq,
|
|
128
|
+
bibliography,
|
|
129
|
+
howTo,
|
|
130
|
+
schemas: [
|
|
131
|
+
{
|
|
132
|
+
'@context': 'https://schema.org',
|
|
133
|
+
'@type': 'SoftwareApplication',
|
|
134
|
+
name: title,
|
|
135
|
+
description,
|
|
136
|
+
applicationCategory: 'ScientificApplication',
|
|
137
|
+
operatingSystem: 'Any',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
'@context': 'https://schema.org',
|
|
141
|
+
'@type': 'FAQPage',
|
|
142
|
+
mainEntity: faq.map((item) => ({
|
|
143
|
+
'@type': 'Question',
|
|
144
|
+
name: item.question,
|
|
145
|
+
acceptedAnswer: {
|
|
146
|
+
'@type': 'Answer',
|
|
147
|
+
text: item.answer,
|
|
148
|
+
},
|
|
149
|
+
})),
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
'@context': 'https://schema.org',
|
|
153
|
+
'@type': 'HowTo',
|
|
154
|
+
name: title,
|
|
155
|
+
step: howTo.map((step) => ({
|
|
156
|
+
'@type': 'HowToStep',
|
|
157
|
+
name: step.name,
|
|
158
|
+
text: step.text,
|
|
159
|
+
})),
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
};
|