AK51 commited on
Commit
30fc3d4
·
verified ·
1 Parent(s): 41c77a9

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.js +56 -39
  2. index.html +33 -15
  3. style.css +9 -0
app.js CHANGED
@@ -1182,7 +1182,9 @@ const D2R = Math.PI / 180;
1182
  let satGroup = null, orbitLine = null, satCam = null;
1183
  let satEnabled = true, satViewOn = true, satFov = 45;
1184
  let lastFrameTime = performance.now();
1185
- const orbit = { a: 3000, e: 0.1, i: 45, raan: 0, argp: 0, nu: 0, speed: 200 };
 
 
1186
 
1187
  /* Which orbit family the satellite flies: a low lunar orbit around the
1188
  Moon, or a halo orbit about the Earth–Moon L2 point. */
@@ -1319,42 +1321,46 @@ function buildSatellite() {
1319
  orbitLine.visible = satEnabled;
1320
  }
1321
 
1322
- /* Advance the true anomaly using the physical rate (faster at periapsis),
1323
- scaled by the visualization speed multiplier. */
 
 
 
 
 
 
 
1324
  function updateSatellite(dt) {
1325
  if (!satEnabled || !satGroup) return;
1326
 
 
 
 
 
 
 
1327
  if (orbitMode === "l2") {
1328
- // The real L2 halo period is ~14.8 days — ~90× slower than a low
1329
- // lunar orbit — so at the shared speed multiplier it would look
1330
- // frozen. Use a visualization rate so the satellite visibly traces
1331
- // the halo across the speed slider's range.
1332
- if (orbit.speed > 0) {
1333
- const degPerSec = 0.03 * orbit.speed; // ~6°/s at the default 200×
1334
- l2.phase = (l2.phase + degPerSec * dt) % 360;
1335
- if (l2.phase < 0) l2.phase += 360;
1336
- const sPh = document.getElementById("s-l2phase");
1337
- const vPh = document.getElementById("v-l2phase");
1338
- if (sPh) sPh.value = l2.phase;
1339
- if (vPh) vPh.textContent = Math.round(l2.phase) + "°";
1340
- }
1341
  positionSatellite();
1342
  return;
1343
  }
1344
 
1345
- if (orbit.speed > 0) {
1346
- const e = orbit.e, a = orbit.a;
1347
- const n = Math.sqrt(MU_MOON / (a * a * a)); // mean motion, rad/s
1348
- const f = orbit.nu * D2R;
1349
- const dnu = n * orbit.speed *
1350
- Math.pow(1 + e * Math.cos(f), 2) / Math.pow(1 - e * e, 1.5); // rad/s
1351
- orbit.nu = (orbit.nu + (dnu * dt) / D2R) % 360;
1352
- if (orbit.nu < 0) orbit.nu += 360;
1353
- const sNu = document.getElementById("s-nu");
1354
- const vNu = document.getElementById("v-nu");
1355
- if (sNu) sNu.value = orbit.nu;
1356
- if (vNu) vNu.textContent = Math.round(orbit.nu) + "°";
1357
- }
1358
  positionSatellite();
1359
  }
1360
 
@@ -1526,18 +1532,23 @@ function toggleSatFullscreen(force) {
1526
  }
1527
  }
1528
 
1529
- /* A slider paired with an editable number input (two-way synced). */
1530
- function bindNum(sliderId, numId, apply) {
 
 
1531
  const s = document.getElementById(sliderId);
1532
  const n = document.getElementById(numId);
1533
  if (!s || !n) return;
 
1534
  const clamp = (val) => {
1535
  const min = parseFloat(s.min), max = parseFloat(s.max);
1536
  return Math.min(Math.max(val, min), max);
1537
  };
 
1538
  s.addEventListener("input", () => {
1539
  const v = parseFloat(s.value);
1540
  n.value = v;
 
1541
  apply(v);
1542
  });
1543
  n.addEventListener("input", () => {
@@ -1545,6 +1556,7 @@ function bindNum(sliderId, numId, apply) {
1545
  if (isNaN(v)) return; // allow mid-typing
1546
  const cv = clamp(v);
1547
  s.value = cv;
 
1548
  apply(cv);
1549
  });
1550
  n.addEventListener("change", () => {
@@ -1552,10 +1564,12 @@ function bindNum(sliderId, numId, apply) {
1552
  if (isNaN(v)) v = parseFloat(s.value);
1553
  n.value = v;
1554
  s.value = v;
 
1555
  apply(v);
1556
  });
1557
  // initialize from the slider's current value
1558
  n.value = parseFloat(s.value);
 
1559
  apply(parseFloat(s.value));
1560
  }
1561
 
@@ -1574,16 +1588,19 @@ function wireSatellite() {
1574
  handler(); // initialize display
1575
  };
1576
 
1577
- bind("s-a", "v-a", (v) => `${v} km · ${Math.round(v - 1737)} km alt`, (v) => { orbit.a = v; updateOrbitLine(); positionSatellite(); updatePeriodReadout(); });
1578
- bind("s-e", "v-e", (v) => v.toFixed(2), (v) => { orbit.e = v; updateOrbitLine(); positionSatellite(); });
1579
- bind("s-i", "v-i", (v) => `${v}°`, (v) => { orbit.i = v; updateOrbitLine(); positionSatellite(); });
1580
- bind("s-raan", "v-raan", (v) => `${v}°`, (v) => { orbit.raan = v; updateOrbitLine(); positionSatellite(); });
1581
- bind("s-argp", "v-argp", (v) => `${v}°`, (v) => { orbit.argp = v; updateOrbitLine(); positionSatellite(); });
1582
- bind("s-nu", "v-nu", (v) => `${Math.round(v)}°`, (v) => { orbit.nu = v; positionSatellite(); });
 
 
 
1583
 
1584
  // L2 halo controls
1585
- bind("s-l2amp", "v-l2amp", (v) => `${v} km`, (v) => { l2.amp = v; updateOrbitLine(); positionSatellite(); });
1586
- bind("s-l2phase", "v-l2phase", (v) => `${Math.round(v)}°`, (v) => { l2.phase = v; positionSatellite(); });
1587
 
1588
  // Speed and FOV use a slider paired with an editable number input.
1589
  bindNum("s-speed", "n-speed", (v) => { orbit.speed = v; });
 
1182
  let satGroup = null, orbitLine = null, satCam = null;
1183
  let satEnabled = true, satViewOn = true, satFov = 45;
1184
  let lastFrameTime = performance.now();
1185
+ // `speed` is a percentage: 100% = the satellite's true physical rate
1186
+ // relative to the simulated time clock (see updateSatellite).
1187
+ const orbit = { a: 3000, e: 0.1, i: 45, raan: 0, argp: 0, nu: 0, speed: 100 };
1188
 
1189
  /* Which orbit family the satellite flies: a low lunar orbit around the
1190
  Moon, or a halo orbit about the Earth–Moon L2 point. */
 
1321
  orbitLine.visible = satEnabled;
1322
  }
1323
 
1324
+ /* Advance the satellite along its orbit.
1325
+
1326
+ Motion is driven by the SAME simulated clock as the Sun/Earth, so the
1327
+ satellite only moves while "Animate time" is on Play. The amount of
1328
+ simulated time that passes this frame is:
1329
+ simSeconds = timeRateDays · dt · 86400
1330
+ "Orbit speed" is a percentage on top of that: at 100% the satellite
1331
+ advances at its true physical rate relative to the simulated clock
1332
+ (i.e. in sync with time), and higher values just fast-forward it. */
1333
  function updateSatellite(dt) {
1334
  if (!satEnabled || !satGroup) return;
1335
 
1336
+ // Only move when time is playing (and the speed isn't zeroed).
1337
+ if (!timeAnimating || orbit.speed <= 0) { positionSatellite(); return; }
1338
+
1339
+ const speedFrac = orbit.speed / 100; // 100% ⇒ 1× real rate
1340
+ const simSeconds = timeRateDays * dt * 86400 * speedFrac;
1341
+
1342
  if (orbitMode === "l2") {
1343
+ const degPerSec = 360 / (L2_PERIOD_DAYS * 86400); // real halo rate
1344
+ l2.phase = (l2.phase + degPerSec * simSeconds) % 360;
1345
+ if (l2.phase < 0) l2.phase += 360;
1346
+ const sPh = document.getElementById("s-l2phase");
1347
+ const nPh = document.getElementById("n-l2phase");
1348
+ if (sPh) sPh.value = l2.phase;
1349
+ if (nPh && nPh !== document.activeElement) nPh.value = Math.round(l2.phase);
 
 
 
 
 
 
1350
  positionSatellite();
1351
  return;
1352
  }
1353
 
1354
+ const e = orbit.e, a = orbit.a;
1355
+ const n = Math.sqrt(MU_MOON / (a * a * a)); // mean motion, rad/s
1356
+ const f = orbit.nu * D2R;
1357
+ const dnu = n * Math.pow(1 + e * Math.cos(f), 2) / Math.pow(1 - e * e, 1.5); // rad/s
1358
+ orbit.nu = (orbit.nu + (dnu * simSeconds) / D2R) % 360;
1359
+ if (orbit.nu < 0) orbit.nu += 360;
1360
+ const sNu = document.getElementById("s-nu");
1361
+ const nNu = document.getElementById("n-nu");
1362
+ if (sNu) sNu.value = orbit.nu;
1363
+ if (nNu && nNu !== document.activeElement) nNu.value = Math.round(orbit.nu);
 
 
 
1364
  positionSatellite();
1365
  }
1366
 
 
1532
  }
1533
  }
1534
 
1535
+ /* A slider paired with an editable number input (two-way synced).
1536
+ An optional (labelId, fmt) pair keeps a descriptive span in sync — e.g.
1537
+ the semi-major-axis altitude readout. */
1538
+ function bindNum(sliderId, numId, apply, labelId, fmt) {
1539
  const s = document.getElementById(sliderId);
1540
  const n = document.getElementById(numId);
1541
  if (!s || !n) return;
1542
+ const label = labelId ? document.getElementById(labelId) : null;
1543
  const clamp = (val) => {
1544
  const min = parseFloat(s.min), max = parseFloat(s.max);
1545
  return Math.min(Math.max(val, min), max);
1546
  };
1547
+ const setLabel = (v) => { if (label && fmt) label.textContent = fmt(v); };
1548
  s.addEventListener("input", () => {
1549
  const v = parseFloat(s.value);
1550
  n.value = v;
1551
+ setLabel(v);
1552
  apply(v);
1553
  });
1554
  n.addEventListener("input", () => {
 
1556
  if (isNaN(v)) return; // allow mid-typing
1557
  const cv = clamp(v);
1558
  s.value = cv;
1559
+ setLabel(cv);
1560
  apply(cv);
1561
  });
1562
  n.addEventListener("change", () => {
 
1564
  if (isNaN(v)) v = parseFloat(s.value);
1565
  n.value = v;
1566
  s.value = v;
1567
+ setLabel(v);
1568
  apply(v);
1569
  });
1570
  // initialize from the slider's current value
1571
  n.value = parseFloat(s.value);
1572
+ setLabel(parseFloat(s.value));
1573
  apply(parseFloat(s.value));
1574
  }
1575
 
 
1588
  handler(); // initialize display
1589
  };
1590
 
1591
+ // The six classical orbital elements — each a slider paired with an
1592
+ // editable number input the user can type into directly.
1593
+ bindNum("s-a", "n-a", (v) => { orbit.a = v; updateOrbitLine(); positionSatellite(); updatePeriodReadout(); },
1594
+ "v-a", (v) => `${Math.round(v - 1737)} km alt`);
1595
+ bindNum("s-e", "n-e", (v) => { orbit.e = v; updateOrbitLine(); positionSatellite(); });
1596
+ bindNum("s-i", "n-i", (v) => { orbit.i = v; updateOrbitLine(); positionSatellite(); });
1597
+ bindNum("s-raan", "n-raan", (v) => { orbit.raan = v; updateOrbitLine(); positionSatellite(); });
1598
+ bindNum("s-argp", "n-argp", (v) => { orbit.argp = v; updateOrbitLine(); positionSatellite(); });
1599
+ bindNum("s-nu", "n-nu", (v) => { orbit.nu = v; positionSatellite(); });
1600
 
1601
  // L2 halo controls
1602
+ bindNum("s-l2amp", "n-l2amp", (v) => { l2.amp = v; updateOrbitLine(); positionSatellite(); });
1603
+ bindNum("s-l2phase", "n-l2phase", (v) => { l2.phase = v; positionSatellite(); });
1604
 
1605
  // Speed and FOV use a slider paired with an editable number input.
1606
  bindNum("s-speed", "n-speed", (v) => { orbit.speed = v; });
index.html CHANGED
@@ -4,7 +4,7 @@
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>Luna Maps — 3D Moon Explorer</title>
7
- <link rel="stylesheet" href="style.css?v=7" />
8
  </head>
9
  <body>
10
  <div id="app">
@@ -167,30 +167,44 @@
167
  <div class="period-line">🛰 One orbit ≈ <span id="period-readout">—</span> <span class="period-note">(real time)</span></div>
168
  </div>
169
 
170
- <!-- Low lunar orbit: classical Keplerian elements -->
 
171
  <div id="lunar-orbit-controls">
172
  <div class="slider-row">
173
- <label for="s-a">Semi-major axis <span id="v-a">3000 km · 1263 km alt</span></label>
174
- <input type="range" id="s-a" min="1800" max="8000" step="10" value="3000" />
 
 
 
175
  </div>
176
  <div class="slider-row">
177
- <label for="s-e">Eccentricity <span id="v-e">0.10</span></label>
 
 
178
  <input type="range" id="s-e" min="0" max="0.7" step="0.01" value="0.1" />
179
  </div>
180
  <div class="slider-row">
181
- <label for="s-i">Inclination i <span id="v-i">45°</span></label>
 
 
182
  <input type="range" id="s-i" min="0" max="180" step="1" value="45" />
183
  </div>
184
  <div class="slider-row">
185
- <label for="s-raan">Ascending node Ω <span id="v-raan">0°</span></label>
 
 
186
  <input type="range" id="s-raan" min="0" max="360" step="1" value="0" />
187
  </div>
188
  <div class="slider-row">
189
- <label for="s-argp">Arg. of periapsis ω <span id="v-argp">0°</span></label>
 
 
190
  <input type="range" id="s-argp" min="0" max="360" step="1" value="0" />
191
  </div>
192
  <div class="slider-row">
193
- <label for="s-nu">True anomaly ν <span id="v-nu">0°</span></label>
 
 
194
  <input type="range" id="s-nu" min="0" max="360" step="1" value="0" />
195
  </div>
196
  </div>
@@ -203,7 +217,9 @@
203
  hemisphere that never faces Earth.
204
  </div>
205
  <div class="slider-row">
206
- <label for="s-l2amp">Halo amplitude <span id="v-l2amp">25000 km</span></label>
 
 
207
  <input type="range" id="s-l2amp" min="2000" max="70000" step="500" value="25000" />
208
  </div>
209
  <div class="map-toggle">
@@ -214,16 +230,18 @@
214
  </div>
215
  </div>
216
  <div class="slider-row">
217
- <label for="s-l2phase">Halo phase <span id="v-l2phase">0°</span></label>
 
 
218
  <input type="range" id="s-l2phase" min="0" max="360" step="1" value="0" />
219
  </div>
220
  </div>
221
 
222
  <div class="slider-row">
223
- <label for="s-speed">Orbit speed
224
- <span class="num-wrap"><input type="number" id="n-speed" class="num-input" min="0" max="400" step="1" value="200" aria-label="Orbit speed multiplier" />×</span>
225
  </label>
226
- <input type="range" id="s-speed" min="0" max="400" step="1" value="200" />
227
  </div>
228
 
229
  <div class="slider-row">
@@ -305,6 +323,6 @@
305
  }
306
  }
307
  </script>
308
- <script type="module" src="app.js?v=7"></script>
309
  </body>
310
  </html>
 
4
  <meta charset="UTF-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
  <title>Luna Maps — 3D Moon Explorer</title>
7
+ <link rel="stylesheet" href="style.css?v=9" />
8
  </head>
9
  <body>
10
  <div id="app">
 
167
  <div class="period-line">🛰 One orbit ≈ <span id="period-readout">—</span> <span class="period-note">(real time)</span></div>
168
  </div>
169
 
170
+ <!-- Low lunar orbit: classical Keplerian elements.
171
+ Each is a slider paired with an editable number input. -->
172
  <div id="lunar-orbit-controls">
173
  <div class="slider-row">
174
+ <label for="s-a">Semi-major axis
175
+ <span class="num-wrap"><input type="number" id="n-a" class="num-input" min="1800" max="12000" step="10" value="3000" aria-label="Semi-major axis (km)" />km</span>
176
+ </label>
177
+ <input type="range" id="s-a" min="1800" max="12000" step="10" value="3000" />
178
+ <div class="sub-note"><span id="v-a">1263 km alt</span></div>
179
  </div>
180
  <div class="slider-row">
181
+ <label for="s-e">Eccentricity
182
+ <span class="num-wrap"><input type="number" id="n-e" class="num-input" min="0" max="0.7" step="0.01" value="0.1" aria-label="Eccentricity" /></span>
183
+ </label>
184
  <input type="range" id="s-e" min="0" max="0.7" step="0.01" value="0.1" />
185
  </div>
186
  <div class="slider-row">
187
+ <label for="s-i">Inclination i
188
+ <span class="num-wrap"><input type="number" id="n-i" class="num-input" min="0" max="180" step="1" value="45" aria-label="Inclination (degrees)" />°</span>
189
+ </label>
190
  <input type="range" id="s-i" min="0" max="180" step="1" value="45" />
191
  </div>
192
  <div class="slider-row">
193
+ <label for="s-raan">Ascending node Ω
194
+ <span class="num-wrap"><input type="number" id="n-raan" class="num-input" min="0" max="360" step="1" value="0" aria-label="Right ascension of ascending node (degrees)" />°</span>
195
+ </label>
196
  <input type="range" id="s-raan" min="0" max="360" step="1" value="0" />
197
  </div>
198
  <div class="slider-row">
199
+ <label for="s-argp">Arg. of periapsis ω
200
+ <span class="num-wrap"><input type="number" id="n-argp" class="num-input" min="0" max="360" step="1" value="0" aria-label="Argument of periapsis (degrees)" />°</span>
201
+ </label>
202
  <input type="range" id="s-argp" min="0" max="360" step="1" value="0" />
203
  </div>
204
  <div class="slider-row">
205
+ <label for="s-nu">True anomaly ν
206
+ <span class="num-wrap"><input type="number" id="n-nu" class="num-input" min="0" max="360" step="1" value="0" aria-label="True anomaly (degrees)" />°</span>
207
+ </label>
208
  <input type="range" id="s-nu" min="0" max="360" step="1" value="0" />
209
  </div>
210
  </div>
 
217
  hemisphere that never faces Earth.
218
  </div>
219
  <div class="slider-row">
220
+ <label for="s-l2amp">Halo amplitude
221
+ <span class="num-wrap"><input type="number" id="n-l2amp" class="num-input" min="2000" max="70000" step="500" value="25000" aria-label="Halo amplitude (km)" />km</span>
222
+ </label>
223
  <input type="range" id="s-l2amp" min="2000" max="70000" step="500" value="25000" />
224
  </div>
225
  <div class="map-toggle">
 
230
  </div>
231
  </div>
232
  <div class="slider-row">
233
+ <label for="s-l2phase">Halo phase
234
+ <span class="num-wrap"><input type="number" id="n-l2phase" class="num-input" min="0" max="360" step="1" value="0" aria-label="Halo phase (degrees)" />°</span>
235
+ </label>
236
  <input type="range" id="s-l2phase" min="0" max="360" step="1" value="0" />
237
  </div>
238
  </div>
239
 
240
  <div class="slider-row">
241
+ <label for="s-speed">Orbit speed <span class="period-note">(100% = synced to time)</span>
242
+ <span class="num-wrap"><input type="number" id="n-speed" class="num-input" min="0" max="400" step="1" value="100" aria-label="Orbit speed (percent of real rate relative to simulated time)" />%</span>
243
  </label>
244
+ <input type="range" id="s-speed" min="0" max="400" step="1" value="100" />
245
  </div>
246
 
247
  <div class="slider-row">
 
323
  }
324
  }
325
  </script>
326
+ <script type="module" src="app.js?v=9"></script>
327
  </body>
328
  </html>
style.css CHANGED
@@ -187,6 +187,15 @@ html, body {
187
  height: 14px;
188
  }
189
 
 
 
 
 
 
 
 
 
 
190
  .slider-row input[type="range"] {
191
  -webkit-appearance: none;
192
  appearance: none;
 
187
  height: 14px;
188
  }
189
 
190
+ /* secondary note under a slider (e.g. the altitude readout) */
191
+ .sub-note {
192
+ margin-top: 4px;
193
+ font-size: 11px;
194
+ color: var(--accent-2);
195
+ text-align: right;
196
+ font-variant-numeric: tabular-nums;
197
+ }
198
+
199
  .slider-row input[type="range"] {
200
  -webkit-appearance: none;
201
  appearance: none;