Upload 3 files
Browse files- app.js +56 -39
- index.html +33 -15
- 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 |
-
|
|
|
|
|
|
|
| 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
|
| 1323 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1324 |
function updateSatellite(dt) {
|
| 1325 |
if (!satEnabled || !satGroup) return;
|
| 1326 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1327 |
if (orbitMode === "l2") {
|
| 1328 |
-
|
| 1329 |
-
|
| 1330 |
-
|
| 1331 |
-
|
| 1332 |
-
|
| 1333 |
-
|
| 1334 |
-
|
| 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 |
-
|
| 1346 |
-
|
| 1347 |
-
|
| 1348 |
-
|
| 1349 |
-
|
| 1350 |
-
|
| 1351 |
-
|
| 1352 |
-
|
| 1353 |
-
|
| 1354 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 1578 |
-
|
| 1579 |
-
|
| 1580 |
-
|
| 1581 |
-
|
| 1582 |
-
|
|
|
|
|
|
|
|
|
|
| 1583 |
|
| 1584 |
// L2 halo controls
|
| 1585 |
-
|
| 1586 |
-
|
| 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=
|
| 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
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
| 175 |
</div>
|
| 176 |
<div class="slider-row">
|
| 177 |
-
<label for="s-e">Eccentricity
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 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 Ω
|
|
|
|
|
|
|
| 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 ω
|
|
|
|
|
|
|
| 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 ν
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 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="
|
| 225 |
</label>
|
| 226 |
-
<input type="range" id="s-speed" min="0" max="400" step="1" value="
|
| 227 |
</div>
|
| 228 |
|
| 229 |
<div class="slider-row">
|
|
@@ -305,6 +323,6 @@
|
|
| 305 |
}
|
| 306 |
}
|
| 307 |
</script>
|
| 308 |
-
<script type="module" src="app.js?v=
|
| 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;
|