hendrik289 commited on
Commit
7690ec7
·
verified ·
1 Parent(s): a0febd4

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +847 -19
index.html CHANGED
@@ -1,19 +1,847 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="de">
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <title>Realistische Sonnensystem-Simulation</title>
8
+ <style>
9
+ :root {
10
+ --bg: #0a0f1c;
11
+ --panel: #0f1629cc;
12
+ --panel-border: #2b3a66;
13
+ --accent: #7dd3fc;
14
+ --accent-2: #a78bfa;
15
+ --text: #e6f0ff;
16
+ --muted: #9fb2d6;
17
+ --good: #34d399;
18
+ --warn: #f59e0b;
19
+ --danger: #ef4444;
20
+ --shadow: 0 10px 30px rgba(0, 0, 0, .35);
21
+ --blur: saturate(140%) blur(8px);
22
+ --radius: 16px;
23
+ }
24
+
25
+ * {
26
+ box-sizing: border-box;
27
+ }
28
+
29
+ html,
30
+ body {
31
+ margin: 0;
32
+ padding: 0;
33
+ height: 100%;
34
+ width: 100%;
35
+ background: radial-gradient(1200px 800px at 70% 20%, #111a31 0%, #0b1223 40%, #070d1a 65%, #050913 100%), var(--bg);
36
+ color: var(--text);
37
+ font-family: ui-sans-serif, system-ui, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji;
38
+ overflow: hidden;
39
+ }
40
+
41
+ canvas {
42
+ position: fixed;
43
+ inset: 0;
44
+ width: 100vw;
45
+ height: 100vh;
46
+ display: block;
47
+ }
48
+
49
+ .hud {
50
+ position: fixed;
51
+ top: 16px;
52
+ left: 16px;
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: 12px;
56
+ z-index: 10;
57
+ pointer-events: none;
58
+ }
59
+
60
+ .header {
61
+ pointer-events: auto;
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 10px;
65
+ padding: 10px 14px;
66
+ backdrop-filter: var(--blur);
67
+ background: linear-gradient(180deg, rgba(255, 255, 255, .06), rgba(255, 255, 255, .02));
68
+ border: 1px solid var(--panel-border);
69
+ border-radius: var(--radius);
70
+ box-shadow: var(--shadow);
71
+ }
72
+
73
+ .logo {
74
+ width: 28px;
75
+ height: 28px;
76
+ border-radius: 50%;
77
+ background: radial-gradient(circle at 35% 35%, #ffd166 0%, #ff9f1c 35%, #ff6b35 60%, #f94144 100%);
78
+ box-shadow: 0 0 20px 6px rgba(255, 198, 94, .35), inset 0 0 20px rgba(255, 255, 255, .15);
79
+ border: 1px solid rgba(255, 255, 255, .25);
80
+ }
81
+
82
+ .title {
83
+ font-weight: 700;
84
+ letter-spacing: .3px;
85
+ }
86
+
87
+ .subtitle {
88
+ color: var(--muted);
89
+ font-size: .9rem;
90
+ }
91
+
92
+ .brand {
93
+ color: var(--accent);
94
+ font-weight: 700;
95
+ text-decoration: none;
96
+ }
97
+
98
+ .brand:hover {
99
+ text-decoration: underline;
100
+ }
101
+
102
+ .panel {
103
+ pointer-events: auto;
104
+ padding: 14px;
105
+ backdrop-filter: var(--blur);
106
+ background: linear-gradient(180deg, rgba(13, 20, 40, .75), rgba(10, 15, 28, .65));
107
+ border: 1px solid var(--panel-border);
108
+ border-radius: var(--radius);
109
+ box-shadow: var(--shadow);
110
+ min-width: 320px;
111
+ max-width: 420px;
112
+ }
113
+
114
+ .row {
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: space-between;
118
+ gap: 10px;
119
+ margin: 8px 0;
120
+ }
121
+
122
+ .label {
123
+ color: var(--muted);
124
+ font-size: .9rem;
125
+ }
126
+
127
+ .value {
128
+ font-variant-numeric: tabular-nums;
129
+ font-weight: 600;
130
+ }
131
+
132
+ .controls {
133
+ display: grid;
134
+ gap: 10px;
135
+ }
136
+
137
+ .control {
138
+ display: grid;
139
+ grid-template-columns: 1fr auto;
140
+ align-items: center;
141
+ gap: 10px;
142
+ }
143
+
144
+ input[type="range"] {
145
+ width: 220px;
146
+ -webkit-appearance: none;
147
+ appearance: none;
148
+ height: 6px;
149
+ border-radius: 999px;
150
+ background: linear-gradient(90deg, rgba(125, 211, 252, .35), rgba(167, 139, 250, .45));
151
+ outline: none;
152
+ border: 1px solid rgba(255, 255, 255, .08);
153
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .35);
154
+ }
155
+
156
+ input[type="range"]::-webkit-slider-thumb {
157
+ -webkit-appearance: none;
158
+ appearance: none;
159
+ width: 18px;
160
+ height: 18px;
161
+ border-radius: 50%;
162
+ background: radial-gradient(circle at 30% 30%, #fff 0%, #cfe9ff 35%, #8bd6ff 70%, #6ea8ff 100%);
163
+ border: 1px solid rgba(255, 255, 255, .7);
164
+ box-shadow: 0 3px 10px rgba(0, 0, 0, .35), 0 0 0 6px rgba(125, 211, 252, .15);
165
+ cursor: pointer;
166
+ }
167
+
168
+ input[type="range"]::-moz-range-thumb {
169
+ width: 18px;
170
+ height: 18px;
171
+ border-radius: 50%;
172
+ background: radial-gradient(circle at 30% 30%, #fff 0%, #cfe9ff 35%, #8bd6ff 70%, #6ea8ff 100%);
173
+ border: 1px solid rgba(255, 255, 255, .7);
174
+ box-shadow: 0 3px 10px rgba(0, 0, 0, .35), 0 0 0 6px rgba(125, 211, 252, .15);
175
+ cursor: pointer;
176
+ }
177
+
178
+ .toggles {
179
+ display: flex;
180
+ gap: 10px;
181
+ flex-wrap: wrap;
182
+ }
183
+
184
+ .chip {
185
+ display: flex;
186
+ align-items: center;
187
+ gap: 8px;
188
+ padding: 8px 12px;
189
+ border-radius: 999px;
190
+ cursor: pointer;
191
+ user-select: none;
192
+ border: 1px solid var(--panel-border);
193
+ background: rgba(255, 255, 255, .04);
194
+ transition: transform .12s ease, background .2s ease, border-color .2s ease;
195
+ }
196
+
197
+ .chip input {
198
+ display: none;
199
+ }
200
+
201
+ .chip span {
202
+ color: var(--muted);
203
+ }
204
+
205
+ .chip.active {
206
+ background: rgba(125, 211, 252, .12);
207
+ border-color: rgba(125, 211, 252, .45);
208
+ }
209
+
210
+ .chip.active span {
211
+ color: var(--text);
212
+ }
213
+
214
+ .chip:hover {
215
+ transform: translateY(-1px);
216
+ }
217
+
218
+ .legend {
219
+ display: flex;
220
+ flex-wrap: wrap;
221
+ gap: 8px;
222
+ margin-top: 6px;
223
+ }
224
+
225
+ .planet-key {
226
+ display: flex;
227
+ align-items: center;
228
+ gap: 6px;
229
+ padding: 6px 10px;
230
+ border-radius: 999px;
231
+ font-size: .85rem;
232
+ background: rgba(255, 255, 255, .04);
233
+ border: 1px solid var(--panel-border);
234
+ }
235
+
236
+ .dot {
237
+ width: 10px;
238
+ height: 10px;
239
+ border-radius: 50%;
240
+ box-shadow: 0 0 8px currentColor;
241
+ }
242
+
243
+ .footer {
244
+ position: fixed;
245
+ right: 16px;
246
+ bottom: 16px;
247
+ pointer-events: auto;
248
+ display: flex;
249
+ align-items: center;
250
+ gap: 10px;
251
+ padding: 10px 12px;
252
+ border-radius: var(--radius);
253
+ background: linear-gradient(180deg, rgba(255, 255, 255, .06), rgba(255, 255, 255, .02));
254
+ border: 1px solid var(--panel-border);
255
+ box-shadow: var(--shadow);
256
+ }
257
+
258
+ .btn {
259
+ padding: 8px 12px;
260
+ border-radius: 10px;
261
+ border: 1px solid var(--panel-border);
262
+ background: rgba(255, 255, 255, .04);
263
+ color: var(--text);
264
+ cursor: pointer;
265
+ transition: transform .1s ease, background .2s ease, border-color .2s ease;
266
+ }
267
+
268
+ .btn:hover {
269
+ transform: translateY(-1px);
270
+ background: rgba(125, 211, 252, .1);
271
+ }
272
+
273
+ .btn:active {
274
+ transform: translateY(0);
275
+ }
276
+
277
+ .btn.primary {
278
+ background: linear-gradient(180deg, rgba(125, 211, 252, .25), rgba(125, 211, 252, .15));
279
+ border-color: rgba(125, 211, 252, .45);
280
+ }
281
+
282
+ .note {
283
+ color: var(--muted);
284
+ font-size: .85rem;
285
+ }
286
+
287
+ @media (max-width: 780px) {
288
+ .panel {
289
+ min-width: unset;
290
+ width: calc(100vw - 32px);
291
+ }
292
+
293
+ input[type="range"] {
294
+ width: 160px;
295
+ }
296
+
297
+ .hud {
298
+ left: 50%;
299
+ transform: translateX(-50%);
300
+ width: calc(100vw - 32px);
301
+ }
302
+ }
303
+ </style>
304
+ </head>
305
+
306
+ <body>
307
+ <canvas id="space"></canvas>
308
+
309
+ <div class="hud">
310
+ <div class="header">
311
+ <div class="logo" aria-hidden="true"></div>
312
+ <div>
313
+ <div class="title">Sonnensystem-Simulation</div>
314
+ <div class="subtitle">Echtzeit-Physik mit anpassbarem Zoom, Umlaufgeschwindigkeit und Gravitation</div>
315
+ </div>
316
+ </div>
317
+
318
+ <div class="panel">
319
+ <div class="controls">
320
+ <div class="control">
321
+ <div class="label">Zoom</div>
322
+ <div style="display:flex; align-items:center; gap:8px;">
323
+ <input id="zoom" type="range" min="0.25" max="4" step="0.01" value="1.4" />
324
+ <div class="value" id="zoomVal">1.40×</div>
325
+ </div>
326
+ </div>
327
+
328
+ <div class="control">
329
+ <div class="label">Umlaufgeschwindigkeit</div>
330
+ <div style="display:flex; align-items:center; gap:8px;">
331
+ <input id="speed" type="range" min="0.1" max="50" step="0.1" value="8" />
332
+ <div class="value"><span id="speedVal">8.0</span>×</div>
333
+ </div>
334
+ </div>
335
+
336
+ <div class="control">
337
+ <div class="label">Gravitation</div>
338
+ <div style="display:flex; align-items:center; gap:8px;">
339
+ <input id="gravity" type="range" min="0.2" max="2.5" step="0.01" value="1.00" />
340
+ <div class="value"><span id="gravityVal">1.00</span>×</div>
341
+ </div>
342
+ </div>
343
+
344
+ <div class="control">
345
+ <div class="label">Planeten-Interaktionen</div>
346
+ <div class="toggles">
347
+ <label class="chip" id="togglePP">
348
+ <input type="checkbox" />
349
+ <span>Ein/Aus</span>
350
+ </label>
351
+ </div>
352
+ </div>
353
+
354
+ <div class="control">
355
+ <div class="label">Spuren anzeigen</div>
356
+ <div class="toggles">
357
+ <label class="chip" id="toggleTrails">
358
+ <input type="checkbox" checked />
359
+ <span>Ein/Aus</span>
360
+ </label>
361
+ </div>
362
+ </div>
363
+
364
+ <div class="control">
365
+ <div class="label">Planetennamen</div>
366
+ <div class="toggles">
367
+ <label class="chip" id="toggleLabels">
368
+ <input type="checkbox" checked />
369
+ <span>Ein/Aus</span>
370
+ </label>
371
+ </div>
372
+ </div>
373
+
374
+ <div class="row">
375
+ <div class="label">Sichtskalierung Planeten</div>
376
+ <div style="display:flex; align-items:center; gap:8px;">
377
+ <input id="size" type="range" min="0.5" max="2.0" step="0.01" value="1.00" />
378
+ <div class="value"><span id="sizeVal">1.00</span>×</div>
379
+ </div>
380
+ </div>
381
+ </div>
382
+
383
+ <div class="legend" id="legend"></div>
384
+ </div>
385
+ </div>
386
+
387
+ <div class="footer">
388
+ <button class="btn primary" id="playPause">Pause</button>
389
+ <button class="btn" id="reset">Reset</button>
390
+ <div class="note">Tipp: Rad = Zoom, Leertaste = Pause</div>
391
+ <a class="brand" href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" rel="noreferrer">Built with
392
+ anycoder</a>
393
+ </div>
394
+
395
+ <script>
396
+ // --- Canvas setup with HiDPI ---
397
+ const canvas = document.getElementById('space');
398
+ const ctx = canvas.getContext('2d', { alpha: false, desynchronized: true });
399
+
400
+ let W = 0, H = 0, DPR = Math.max(1, Math.min(2, window.devicePixelRatio || 1)); // cap DPR for perf
401
+
402
+ function resize() {
403
+ const { clientWidth, clientHeight } = canvas;
404
+ DPR = Math.max(1, Math.min(2, window.devicePixelRatio || 1));
405
+ W = clientWidth;
406
+ H = clientHeight;
407
+ canvas.width = Math.floor(W * DPR);
408
+ canvas.height = Math.floor(H * DPR);
409
+ ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
410
+ rebuildStarfield();
411
+ }
412
+ window.addEventListener('resize', resize);
413
+ resize();
414
+
415
+ // --- Controls ---
416
+ const zoomEl = document.getElementById('zoom');
417
+ const zoomValEl = document.getElementById('zoomVal');
418
+ const speedEl = document.getElementById('speed');
419
+ const speedValEl = document.getElementById('speedVal');
420
+ const gravityEl = document.getElementById('gravity');
421
+ const gravityValEl = document.getElementById('gravityVal');
422
+ const sizeEl = document.getElementById('size');
423
+ const sizeValEl = document.getElementById('sizeVal');
424
+
425
+ const togglePP = document.getElementById('togglePP');
426
+ const toggleTrails = document.getElementById('toggleTrails');
427
+ const toggleLabels = document.getElementById('toggleLabels');
428
+
429
+ const playPauseBtn = document.getElementById('playPause');
430
+ const resetBtn = document.getElementById('reset');
431
+
432
+ let isRunning = true;
433
+ playPauseBtn.addEventListener('click', () => {
434
+ isRunning = !isRunning;
435
+ playPauseBtn.textContent = isRunning ? 'Pause' : 'Start';
436
+ });
437
+ window.addEventListener('keydown', (e) => {
438
+ if (e.code === 'Space') {
439
+ isRunning = !isRunning;
440
+ playPauseBtn.textContent = isRunning ? 'Pause' : 'Start';
441
+ }
442
+ });
443
+
444
+ // Wheel zoom
445
+ let targetZoom = parseFloat(zoomEl.value);
446
+ let zoom = targetZoom;
447
+ canvas.addEventListener('wheel', (e) => {
448
+ e.preventDefault();
449
+ const delta = Math.sign(e.deltaY);
450
+ const step = 0.04;
451
+ targetZoom = clamp(targetZoom * (1 - delta * step), parseFloat(zoomEl.min), parseFloat(zoomEl.max));
452
+ zoomEl.value = targetZoom.toFixed(2);
453
+ updateZoomLabel();
454
+ }, { passive: false });
455
+
456
+ zoomEl.addEventListener('input', () => {
457
+ targetZoom = parseFloat(zoomEl.value);
458
+ updateZoomLabel();
459
+ });
460
+ function updateZoomLabel(){
461
+ zoomValEl.textContent = `${parseFloat(zoomEl.value).toFixed(2)}×`;
462
+ }
463
+ updateZoomLabel();
464
+
465
+ speedEl.addEventListener('input', () => {
466
+ speedValEl.textContent = parseFloat(speedEl.value).toFixed(1);
467
+ });
468
+ speedValEl.textContent = parseFloat(speedEl.value).toFixed(1);
469
+
470
+ gravityEl.addEventListener('input', () => {
471
+ gravityValEl.textContent = parseFloat(gravityEl.value).toFixed(2);
472
+ // Gravity changes orbital periods; reinitialize to keep planets on nice orbits with current settings
473
+ resetSystem();
474
+ });
475
+ gravityValEl.textContent = parseFloat(gravityEl.value).toFixed(2);
476
+
477
+ sizeEl.addEventListener('input', () => {
478
+ sizeValEl.textContent = parseFloat(sizeEl.value).toFixed(2);
479
+ });
480
+ sizeValEl.textContent = parseFloat(sizeEl.value).toFixed(2);
481
+
482
+ function setupChip(el){
483
+ const input = el.querySelector('input');
484
+ const sync = () => el.classList.toggle('active', input.checked);
485
+ input.addEventListener('change', sync);
486
+ el.addEventListener('click', (e) => {
487
+ if (e.target !== input) input.checked = !input.checked;
488
+ sync();
489
+ });
490
+ sync();
491
+ }
492
+ setupChip(togglePP);
493
+ setupChip(toggleTrails);
494
+ setupChip(toggleLabels);
495
+
496
+ resetBtn.addEventListener('click', () => resetSystem());
497
+
498
+ // --- Physics / Simulation ---
499
+ // Units: AU for distance, days for time, solar mass for mass.
500
+ // Gravitational constant in these units: G = k^2, where k = 0.01720209895 (Gaussian gravitational constant)
501
+ // => G = k^2 ≈ 0.0002959122082855911 AU^3 / (day^2 * solar_mass)
502
+ const G0 = 0.0002959122082855911;
503
+
504
+ const bodies = [];
505
+ const trailsEnabled = () => toggleTrails.querySelector('input').checked;
506
+ const labelsEnabled = () => toggleLabels.querySelector('input').checked;
507
+ const mutualGravityEnabled = () => togglePP.querySelector('input').checked;
508
+
509
+ const planetDefs = [
510
+ { name:'Merkur', a:0.387098, mass:1.651e-7, color:'#b8b8b8' },
511
+ { name:'Venus', a:0.723332, mass:2.447e-6, color:'#e6c07b' },
512
+ { name:'Erde', a:1.000000, mass:3.003e-6, color:'#4da3ff' },
513
+ { name:'Mars', a:1.523679, mass:3.213e-7, color:'#ff6b6b' },
514
+ { name:'Jupiter',a:5.204267, mass:9.543e-4, color:'#d4a373' },
515
+ { name:'Saturn', a:9.582017, mass:2.857e-4, color:'#e5c97d' },
516
+ { name:'Uranus', a:19.189164, mass:4.365e-5, color:'#7dd3fc' },
517
+ { name:'Neptun', a:30.069922, mass:5.149e-5, color:'#6ea8ff' },
518
+ ];
519
+ const SUN = {
520
+ name:'Sonne', mass:1, color:'#ffd166', drawR: 18, isSun: true
521
+ };
522
+
523
+ // Visual scaling (not physical): planets' radii for visibility
524
+ const basePlanetR = { Merkur:2.8, Venus:4.2, Erde:4.6, Mars:3.2, Jupiter:9.5, Saturn:8.4, Uranus:6.0, Neptun:6.0 };
525
+ const pxPerAUBase = 75; // base pixels per AU at zoom=1.0
526
+ function getPxPerAU(){ return pxPerAUBase * zoom; }
527
+
528
+ function resetSystem(){
529
+ bodies.length = 0;
530
+ // Sun at origin, stationary
531
+ bodies.push({ name:SUN.name, mass:SUN.mass, color:SUN.color, drawR:SUN.drawR, isSun:true, x:0, y:0, vx:0, vy:0, trail:[] });
532
+
533
+ for (const p of planetDefs) {
534
+ const r = p.a; // circular start
535
+ const posAngle = Math.random() * Math.PI * 2;
536
+ const x = r * Math.cos(posAngle);
537
+ const y = r * Math.sin(posAngle);
538
+ // Circular orbit speed around Sun
539
+ const G = G0 * parseFloat(gravityEl.value);
540
+ const v = Math.sqrt(G * SUN.mass / r);
541
+ // Perpendicular to radius for circular orbit
542
+ const vx = -v * Math.sin(posAngle);
543
+ const vy = v * Math.cos(posAngle);
544
+
545
+ bodies.push({
546
+ name: p.name, mass: p.mass, color: p.color,
547
+ drawR: (basePlanetR[p.name] || 4) * parseFloat(sizeEl.value),
548
+ x, y, vx, vy, isSun:false, trail:[]
549
+ });
550
+ }
551
+ }
552
+
553
+ resetSystem();
554
+
555
+ // For display legend
556
+ const legendEl = document.getElementById('legend');
557
+ function rebuildLegend(){
558
+ legendEl.innerHTML = '';
559
+ const frag = document.createDocumentFragment();
560
+ for (const b of bodies) {
561
+ if (b.isSun) continue;
562
+ const el = document.createElement('div');
563
+ el.className = 'planet-key';
564
+ const dot = document.createElement('span');
565
+ dot.className = 'dot';
566
+ dot.style.color = b.color;
567
+ dot.style.background = b.color;
568
+ const name = document.createElement('span');
569
+ name.textContent = b.name;
570
+ el.appendChild(dot); el.appendChild(name);
571
+ frag.appendChild(el);
572
+ }
573
+ legendEl.appendChild(frag);
574
+ }
575
+ rebuildLegend();
576
+
577
+ // Starfield background
578
+ let stars = [];
579
+ function rebuildStarfield(){
580
+ const count = Math.floor(Math.sqrt(W*H) * 0.25); // scale with area
581
+ stars = [];
582
+ for (let i=0;i<count;i++){
583
+ stars.push({
584
+ x: Math.random()*W,
585
+ y: Math.random()*H,
586
+ r: Math.random()*1.2 + 0.3,
587
+ a: Math.random()*0.6 + 0.2,
588
+ tw: Math.random()*0.6 + 0.4,
589
+ ph: Math.random()*Math.PI*2
590
+ });
591
+ }
592
+ }
593
+ rebuildStarfield();
594
+
595
+ // Utility
596
+ function clamp(v, a, b){ return Math.max(a, Math.min(b, v)); }
597
+
598
+ // Integration: Leapfrog (Velocity Verlet)
599
+ let lastTime = performance.now();
600
+ const maxFrameDays = 2.0; // clamp to avoid huge steps on tab switch
601
+
602
+ function step(dtDays){
603
+ const G = G0 * parseFloat(gravityEl.value);
604
+ const n = bodies.length;
605
+ // First half-kick
606
+ for (let i=0; i<n; i++){
607
+ const bi = bodies[i];
608
+ if (bi.isSun) continue; // keep sun fixed
609
+ const ax1 = computeAccelX(i);
610
+ const ay1 = computeAccelY(i);
611
+ bi.vx += ax1 * (dtDays*0.5);
612
+ bi.vy += ay1 * (dtDays*0.5);
613
+ }
614
+ // Drift
615
+ for (let i=0;i<n;i++){
616
+ const b = bodies[i];
617
+ if (b.isSun) continue;
618
+ b.x += b.vx * dtDays;
619
+ b.y += b.vy * dtDays;
620
+ }
621
+ // Second half-kick
622
+ for (let i=0; i<n; i++){
623
+ const bi = bodies[i];
624
+ if (bi.isSun) continue;
625
+ const ax2 = computeAccelX(i);
626
+ const ay2 = computeAccelY(i);
627
+ bi.vx += ax2 * (dtDays*0.5);
628
+ bi.vy += ay2 * (dtDays*0.5);
629
+ }
630
+
631
+ // Trails
632
+ if (trailsEnabled()) {
633
+ for (const b of bodies) {
634
+ if (b.isSun) continue;
635
+ b.trail.push({x:b.x, y:b.y});
636
+ if (b.trail.length > 800) b.trail.shift();
637
+ }
638
+ } else {
639
+ for (const b of bodies) b.trail.length = 0;
640
+ }
641
+ }
642
+
643
+ function computeAccelX(i){
644
+ let ax = 0;
645
+ const bi = bodies[i];
646
+ for (let j=0; j<bodies.length; j++){
647
+ if (i === j) continue;
648
+ const bj = bodies[j];
649
+ const dx = bj.x - bi.x;
650
+ const dy = bj.y - bi.y;
651
+ const r2 = dx*dx + dy*dy;
652
+ // Avoid division by zero (same position)
653
+ if (r2 < 1e-12) continue;
654
+ const invR3 = 1 / Math.pow(r2, 1.5);
655
+ const G = G0 * parseFloat(gravityEl.value);
656
+ // If mutual gravity disabled, only gravitate towards Sun
657
+ const factor = (!mutualGravityEnabled() && !bj.isSun) ? 0 : 1;
658
+ ax += G * bj.mass * dx * invR3 * factor;
659
+ }
660
+ return ax;
661
+ }
662
+
663
+ function computeAccelY(i){
664
+ let ay = 0;
665
+ const bi = bodies[i];
666
+ for (let j=0; j<bodies.length; j++){
667
+ if (i === j) continue;
668
+ const bj = bodies[j];
669
+ const dx = bj.x - bi.x;
670
+ const dy = bj.y - bi.y;
671
+ const r2 = dx*dx + dy*dy;
672
+ if (r2 < 1e-12) continue;
673
+ const invR3 = 1 / Math.pow(r2, 1.5);
674
+ const G = G0 * parseFloat(gravityEl.value);
675
+ const factor = (!mutualGravityEnabled() && !bj.isSun) ? 0 : 1;
676
+ ay += G * bj.mass * dy * invR3 * factor;
677
+ }
678
+ return ay;
679
+ }
680
+
681
+ // Rendering
682
+ function worldToScreen(x, y){
683
+ const s = getPxPerAU();
684
+ return [ W/2 + x*s, H/2 + y*s ];
685
+ }
686
+ function draw(){
687
+ // Background
688
+ ctx.fillStyle = '#070d1a';
689
+ ctx.fillRect(0,0,W,H);
690
+
691
+ // Subtle vignette
692
+ const grad = ctx.createRadialGradient(W*0.5, H*0.55, Math.min(W,H)*0.2, W*0.5, H*0.55, Math.max(W,H)*0.75);
693
+ grad.addColorStop(0, 'rgba(0,0,0,0)');
694
+ grad.addColorStop(1, 'rgba(0,0,0,0.35)');
695
+ ctx.fillStyle = grad;
696
+ ctx.fillRect(0,0,W,H);
697
+
698
+ // Stars (twinkle)
699
+ const t = performance.now() * 0.001;
700
+ for (const s of stars){
701
+ const tw = 0.65 + 0.35*Math.sin(t * s.tw + s.ph);
702
+ ctx.globalAlpha = s.a * tw;
703
+ ctx.fillStyle = '#cfe6ff';
704
+ ctx.beginPath();
705
+ ctx.arc(s.x, s.y, s.r, 0, Math.PI*2);
706
+ ctx.fill();
707
+ }
708
+ ctx.globalAlpha = 1;
709
+
710
+ // Trails
711
+ if (trailsEnabled()){
712
+ for (const b of bodies) {
713
+ if (b.isSun) continue;
714
+ ctx.beginPath();
715
+ for (let i=0;i<b.trail.length;i++){
716
+ const p = b.trail[i];
717
+ const [sx, sy] = worldToScreen(p.x, p.y);
718
+ if (i===0) ctx.moveTo(sx, sy);
719
+ else ctx.lineTo(sx, sy);
720
+ }
721
+ const last = b.trail[b.trail.length-1];
722
+ if (last){
723
+ const [sx, sy] = worldToScreen(last.x, last.y);
724
+ ctx.strokeStyle = b.color + 'cc';
725
+ ctx.lineWidth = 1.4;
726
+ ctx.stroke();
727
+ // Draw faint head dot
728
+ ctx.beginPath();
729
+ ctx.arc(sx, sy, 1.8, 0, Math.PI*2);
730
+ ctx.fillStyle = b.color + 'cc';
731
+ ctx.fill();
732
+ }
733
+ }
734
+ }
735
+
736
+ // Sun glow
737
+ {
738
+ const [sx, sy] = worldToScreen(0,0);
739
+ const r = SUN.drawR * 2.1 + 10;
740
+ const g = ctx.createRadialGradient(sx, sy, 2, sx, sy, r);
741
+ g.addColorStop(0, 'rgba(255,209,102,0.95)');
742
+ g.addColorStop(0.5, 'rgba(255,160,64,0.5)');
743
+ g.addColorStop(1, 'rgba(255,140,64,0.0)');
744
+ ctx.fillStyle = g;
745
+ ctx.beginPath();
746
+ ctx.arc(sx, sy, r, 0, Math.PI*2);
747
+ ctx.fill();
748
+ }
749
+
750
+ // Planets
751
+ ctx.textAlign = 'center';
752
+ ctx.textBaseline = 'top';
753
+ ctx.font = '12px ui-sans-serif, system-ui, Segoe UI, Roboto, Helvetica, Arial';
754
+
755
+ for (const b of bodies){
756
+ const [sx, sy] = worldToScreen(b.x, b.y);
757
+ if (b.isSun){
758
+ // Sun core
759
+ ctx.beginPath();
760
+ ctx.fillStyle = SUN.color;
761
+ ctx.shadowColor = '#ffae34';
762
+ ctx.shadowBlur = 20;
763
+ ctx.arc(sx, sy, SUN.drawR, 0, Math.PI*2);
764
+ ctx.fill();
765
+ ctx.shadowBlur = 0;
766
+ if (labelsEnabled()){
767
+ ctx.fillStyle = '#ffdf9b';
768
+ ctx.fillText('Sonne', sx, sy + SUN.drawR + 6);
769
+ }
770
+ continue;
771
+ }
772
+
773
+ // Planet body
774
+ ctx.beginPath();
775
+ ctx.fillStyle = b.color;
776
+ ctx.shadowColor = b.color;
777
+ ctx.shadowBlur = 12;
778
+ ctx.arc(sx, sy, b.drawR, 0, Math.PI*2);
779
+ ctx.fill();
780
+ ctx.shadowBlur = 0;
781
+
782
+ if (labelsEnabled()){
783
+ ctx.fillStyle = '#cfe6ff';
784
+ ctx.fillText(b.name, sx, sy + b.drawR + 6);
785
+ }
786
+ }
787
+ }
788
+
789
+ // Animation loop
790
+ function frame(now){
791
+ const elapsed = Math.min(0.1, (now - lastTime) / 1000); // seconds
792
+ lastTime = now;
793
+
794
+ // Smooth zoom towards target
795
+ zoom += (targetZoom - zoom) * 0.12;
796
+
797
+ if (isRunning){
798
+ // Convert elapsed to simulation days
799
+ let simDays = elapsed * parseFloat(speedEl.value);
800
+ simDays = Math.min(simDays, maxFrameDays);
801
+ // If mutual gravity toggled, ensure integrator rest always uses same dt
802
+ step(simDays);
803
+ }
804
+ draw();
805
+ requestAnimationFrame(frame);
806
+ }
807
+ requestAnimationFrame(frame);
808
+
809
+ // Initial values set
810
+ updateZoomLabel();
811
+ speedValEl.textContent = parseFloat(speedEl.value).toFixed(1);
812
+ gravityValEl.textContent = parseFloat(gravityEl.value).toFixed(2);
813
+ sizeValEl.textContent = parseFloat(sizeEl.value).toFixed(2);
814
+
815
+ // Update when size scaling changes
816
+ sizeEl.addEventListener('input', () => {
817
+ for (const b of bodies){
818
+ if (!b.isSun){
819
+ const base = basePlanetR[b.name] || 4;
820
+ b.drawR = base * parseFloat(sizeEl.value);
821
+ }
822
+ }
823
+ });
824
+
825
+ // Ensure planet labels/legend after reset
826
+ const observer = new MutationObserver(() => {});
827
+ // Optional: rebuild legend if someone changes language or so (not used here)
828
+
829
+ // In case DPR changes (move between screens)
830
+ window.matchMedia(`(resolution: ${DPR}dppx)`).addEventListener?.('change', resize);
831
+
832
+ // Helper: reset also called when gravity changes (set above)
833
+
834
+ // Accessibility: click anywhere on canvas toggles pause
835
+ canvas.addEventListener('click', () => {
836
+ isRunning = !isRunning;
837
+ playPauseBtn.textContent = isRunning ? 'Pause' : 'Start';
838
+ });
839
+
840
+ // Make sure resizing updates legend layout
841
+ window.addEventListener('resize', () => {
842
+ setTimeout(rebuildLegend, 50);
843
+ });
844
+ </script>
845
+ </body>
846
+
847
+ </html>