422 lines
16 KiB
HTML
422 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Senthara Orbit, Rotation, Phases & Astronomical Data</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/phaser@3.60.0/dist/phaser.js"></script>
|
|
<style>
|
|
body { margin: 0; overflow: hidden; }
|
|
canvas { display: block; }
|
|
/* Control panel style */
|
|
#controlPanel {
|
|
position: absolute;
|
|
bottom: 10px;
|
|
right: 10px;
|
|
background-color: rgba(0, 0, 0, 0.7);
|
|
padding: 10px;
|
|
border-radius: 5px;
|
|
font-family: Arial, sans-serif;
|
|
color: white;
|
|
}
|
|
#controlPanel button, #controlPanel select {
|
|
margin: 5px;
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
border: 1px solid #ccc;
|
|
background-color: #eee;
|
|
color: black;
|
|
cursor: pointer;
|
|
}
|
|
#controlPanel button:hover, #controlPanel select:hover {
|
|
background-color: #ddd;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="controlPanel">
|
|
<button id="playPauseButton">Play</button>
|
|
<button id="rewindButton">Rewind</button>
|
|
<select id="speedSelect">
|
|
<option value="1">x1</option>
|
|
<option value="2">x2</option>
|
|
<option value="5" selected>x5</option>
|
|
<option value="10">x10</option>
|
|
<option value="100">x100</option>
|
|
<option value="1000">x1000</option>
|
|
<option value="10000">x10000</option>
|
|
<option value="100000">x100000</option>
|
|
<option value="1000000">x1000000</option>
|
|
</select>
|
|
</div>
|
|
<script>
|
|
/////////////////////////////
|
|
// 1. Geometry and Moon Phases
|
|
/////////////////////////////
|
|
|
|
/**
|
|
* Returns {x, y} for an elliptical orbit.
|
|
* a = semi-major axis, ecc = eccentricity, b = a*sqrt(1-ecc^2).
|
|
* Theta is computed linearly from simulation time.
|
|
*/
|
|
function getEllipticalPosition(simTime, period, a, ecc) {
|
|
const thetaDeg = (360 * (simTime % period)) / period - 90;
|
|
const theta = Phaser.Math.DegToRad(thetaDeg);
|
|
const b = a * Math.sqrt(1 - ecc * ecc);
|
|
return { x: a * Math.cos(theta), y: b * Math.sin(theta) };
|
|
}
|
|
|
|
/**
|
|
* Compute the fraction of the moon's disc illuminated as seen from the planet.
|
|
* We define vectors:
|
|
* M→S = (starX - moonX, starY - moonY)
|
|
* M→P = (planetX - moonX, planetY - moonY)
|
|
* Then, litFraction = (1 + cos(alpha))/2, where alpha is the angle between M→S and M→P.
|
|
* When alpha=0, fraction=1 (full), when alpha=π, fraction=0 (new).
|
|
*/
|
|
function getMoonLitFraction(moonX, moonY, starX, starY, planetX, planetY) {
|
|
const MSx = starX - moonX;
|
|
const MSy = starY - moonY;
|
|
const MPx = planetX - moonX;
|
|
const MPy = planetY - moonY;
|
|
const dot = MSx * MPx + MSy * MPy;
|
|
const magMS = Math.sqrt(MSx * MSx + MSy * MSy);
|
|
const magMP = Math.sqrt(MPx * MPx + MPy * MPy);
|
|
if (magMS < 1e-9 || magMP < 1e-9) return 0.0;
|
|
let cosAlpha = dot / (magMS * magMP);
|
|
cosAlpha = Math.max(-1, Math.min(1, cosAlpha));
|
|
return 1 - 0.5 * (1 + cosAlpha);
|
|
}
|
|
|
|
/**
|
|
* Given a lit fraction (0 to 1) and whether the moon is waxing, compute a phase emoji.
|
|
* We compute an effective phase angle:
|
|
* phaseAngle = arccos(2*frac - 1) in degrees.
|
|
* If waning, effectivePhase = 360 - phaseAngle.
|
|
* Then divide the 360° into eight segments.
|
|
*/
|
|
function getMoonPhaseEmoji(frac, waxing) {
|
|
// Compute phase angle in degrees.
|
|
let phaseAngle = Math.acos(2 * frac - 1) * (180 / Math.PI);
|
|
if (!waxing) {
|
|
phaseAngle = 360 - phaseAngle;
|
|
}
|
|
// Map phaseAngle to one of eight phases.
|
|
if (phaseAngle < 45) return "🌑"; // 0-45: New Moon
|
|
else if (phaseAngle < 90) return waxing ? "🌒" : "🌘";
|
|
else if (phaseAngle < 135) return waxing ? "🌓" : "🌗";
|
|
else if (phaseAngle < 180) return waxing ? "🌔" : "🌖";
|
|
else if (phaseAngle < 225) return "🌕"; // 180-225: Full Moon
|
|
else if (phaseAngle < 270) return waxing ? "🌔" : "🌖";
|
|
else if (phaseAngle < 315) return waxing ? "🌓" : "🌗";
|
|
else return waxing ? "🌒" : "🌘";
|
|
}
|
|
|
|
/////////////////////////////
|
|
// 2. Senthara Date and Cycle Info
|
|
/////////////////////////////
|
|
|
|
/**
|
|
* Returns the Senthara date as "Year X - <Season> Y".
|
|
* The epoch is offset so that simulationTime=0 corresponds to "Autumn 36".
|
|
*/
|
|
function getSentharaDate(simDays) {
|
|
const offset = 134;
|
|
const X = Math.floor(simDays + offset);
|
|
const year = Math.floor(X / 396) + 1;
|
|
const dayOfYear = (X % 396) + 1;
|
|
const seasonIndex = Math.floor((dayOfYear - 1) / 99);
|
|
const dayOfSeason = dayOfYear - seasonIndex * 99;
|
|
const seasons = ["Summer", "Autumn", "Winter", "Spring"];
|
|
return `Year ${year} - ${seasons[seasonIndex]} Day ${dayOfSeason}`;
|
|
}
|
|
|
|
/////////////////////////////
|
|
// 3. Phaser Setup
|
|
/////////////////////////////
|
|
|
|
const config = {
|
|
type: Phaser.AUTO,
|
|
width: window.innerWidth,
|
|
height: window.innerHeight,
|
|
backgroundColor: "#000000",
|
|
scene: { preload, create, update },
|
|
scale: { mode: Phaser.Scale.RESIZE }
|
|
};
|
|
|
|
const game = new Phaser.Game(config);
|
|
|
|
let globalGraphics;
|
|
let astroText; // Header text (top-left)
|
|
let simulationTime = 0; // in days
|
|
let simulationSpeed = 5; // speed multiplier
|
|
let isPlaying = false;
|
|
let isRewinding = false;
|
|
|
|
// Orbital periods (in days)
|
|
const planetPeriod = 396; // Planet's orbit around the star
|
|
const planetRotationPeriod = 1; // Planet rotates once per day
|
|
const kerielPeriod = 27;
|
|
const arkaenPeriod = 82;
|
|
const minianPeriod = 98;
|
|
|
|
// Eccentricity values (0 = circle)
|
|
const planetEcc = 0.2;
|
|
const kerielEcc = 0.05;
|
|
const arkaenEcc = 0.05;
|
|
const minianEcc = 0.05;
|
|
|
|
// Full cycle lengths
|
|
const shortCycleLen = 108486; // ~274 years
|
|
const longCycleLen = 2386692; // ~6027 years
|
|
|
|
// Orbital radii (semi-major axes, computed relative to window size)
|
|
let planetOrbitRadius, kerielOrbitRadius, arkaenOrbitRadius, minianOrbitRadius;
|
|
|
|
// Containers for the planet and moons.
|
|
let planetContainer;
|
|
let kerielContainer, arkaenContainer, minianContainer;
|
|
|
|
// Tilt indicator for planet's spin axis.
|
|
let tiltIndicator;
|
|
const tiltLength = 30;
|
|
const planetTiltDeg = 23.5;
|
|
const tiltIndicatorAngle = Phaser.Math.DegToRad(-90 + planetTiltDeg);
|
|
|
|
// Global star (sun) position.
|
|
let starX, starY;
|
|
|
|
// We'll store previous lit fraction values for waxing/waning detection.
|
|
let oldFracKeriel = 0, oldFracArkaen = 0, oldFracMinian = 0;
|
|
|
|
function preload() {}
|
|
|
|
function create() {
|
|
globalGraphics = this.add.graphics();
|
|
astroText = this.add.text(10, 10, "", {
|
|
font: "20px Arial",
|
|
fill: "#ffffff"
|
|
});
|
|
|
|
recalcOrbitRadii();
|
|
starX = this.cameras.main.width / 2;
|
|
starY = this.cameras.main.height / 2;
|
|
|
|
// Create container for the planet.
|
|
planetContainer = this.add.container(0, 0);
|
|
let planetBody = this.add.graphics();
|
|
planetBody.fillStyle(0x00FF00, 1);
|
|
planetBody.fillCircle(0, 0, 12);
|
|
planetBody.lineStyle(2, 0x000000, 1);
|
|
planetBody.beginPath();
|
|
planetBody.moveTo(0, 0);
|
|
planetBody.lineTo(12, 0);
|
|
planetBody.strokePath();
|
|
planetContainer.add(planetBody);
|
|
|
|
// Planet night overlay.
|
|
let nightOverlay = this.add.graphics();
|
|
nightOverlay.fillStyle(0x000000, 0.5);
|
|
nightOverlay.slice(0, 0, 12, 0, Math.PI, false);
|
|
nightOverlay.fillPath();
|
|
planetContainer.nightOverlay = nightOverlay;
|
|
planetContainer.add(nightOverlay);
|
|
|
|
// Create moon containers.
|
|
kerielContainer = createMoonContainer(this, 6, 0xFF0000);
|
|
arkaenContainer = createMoonContainer(this, 5, 0x0000FF);
|
|
minianContainer = createMoonContainer(this, 5, 0xFFFFFF);
|
|
|
|
// Add night overlays for moons.
|
|
kerielContainer.add(createMoonNightOverlay(this, 6));
|
|
arkaenContainer.add(createMoonNightOverlay(this, 5));
|
|
minianContainer.add(createMoonNightOverlay(this, 5));
|
|
|
|
// Create tilt indicator.
|
|
tiltIndicator = this.add.graphics();
|
|
|
|
// UI event handlers.
|
|
const playPauseButton = document.getElementById("playPauseButton");
|
|
const rewindButton = document.getElementById("rewindButton");
|
|
const speedSelect = document.getElementById("speedSelect");
|
|
|
|
playPauseButton.addEventListener("click", () => {
|
|
isPlaying = !isPlaying;
|
|
playPauseButton.textContent = isPlaying ? "Pause" : "Play";
|
|
if (isPlaying) isRewinding = false;
|
|
});
|
|
rewindButton.addEventListener("click", () => {
|
|
isRewinding = !isRewinding;
|
|
rewindButton.textContent = isRewinding ? "Forward" : "Rewind";
|
|
if (isRewinding) isPlaying = false;
|
|
playPauseButton.textContent = "Play";
|
|
});
|
|
speedSelect.addEventListener("change", () => {
|
|
simulationSpeed = parseFloat(speedSelect.value);
|
|
});
|
|
|
|
this.scale.on('resize', (gameSize) => {
|
|
recalcOrbitRadii();
|
|
astroText.setPosition(10, 10);
|
|
starX = gameSize.width / 2;
|
|
starY = gameSize.height / 2;
|
|
});
|
|
}
|
|
|
|
// Helper: Create a moon container with a circular body and a marker.
|
|
function createMoonContainer(scene, radius, color) {
|
|
let container = scene.add.container(0, 0);
|
|
let body = scene.add.graphics();
|
|
body.fillStyle(color, 1);
|
|
body.fillCircle(0, 0, radius);
|
|
body.lineStyle(2, 0x000000, 1);
|
|
body.beginPath();
|
|
body.moveTo(0, 0);
|
|
body.lineTo(radius, 0);
|
|
body.strokePath();
|
|
container.add(body);
|
|
return container;
|
|
}
|
|
|
|
// Helper: Create a half-circle night overlay for a moon.
|
|
function createMoonNightOverlay(scene, radius) {
|
|
let overlay = scene.add.graphics();
|
|
overlay.fillStyle(0x000000, 0.5);
|
|
overlay.slice(0, 0, radius, 0, Math.PI, false);
|
|
overlay.fillPath();
|
|
return overlay;
|
|
}
|
|
|
|
// Recalculate orbital radii based on window size.
|
|
function recalcOrbitRadii() {
|
|
const minDim = Math.min(window.innerWidth, window.innerHeight);
|
|
planetOrbitRadius = minDim * 0.3;
|
|
kerielOrbitRadius = planetOrbitRadius * 0.25;
|
|
arkaenOrbitRadius = planetOrbitRadius * 0.35;
|
|
minianOrbitRadius = planetOrbitRadius * 0.45;
|
|
}
|
|
|
|
function update(time, delta) {
|
|
if (isPlaying) {
|
|
simulationTime += (delta * simulationSpeed) / 1000;
|
|
} else if (isRewinding) {
|
|
simulationTime -= (delta * simulationSpeed) / 1000;
|
|
}
|
|
|
|
globalGraphics.clear();
|
|
|
|
// Draw the central star.
|
|
globalGraphics.fillStyle(0xFFFF00, 1);
|
|
globalGraphics.fillCircle(starX, starY, 20);
|
|
|
|
// Draw planet's elliptical orbit.
|
|
const bPlanet = planetOrbitRadius * Math.sqrt(1 - planetEcc * planetEcc);
|
|
globalGraphics.lineStyle(1, 0x555555, 1);
|
|
globalGraphics.strokeEllipse(starX, starY, 2 * planetOrbitRadius, 2 * bPlanet);
|
|
|
|
// Compute planet's position.
|
|
const planetPos = getEllipticalPosition(simulationTime, planetPeriod, planetOrbitRadius, planetEcc);
|
|
const planetX = starX + planetPos.x;
|
|
const planetY = starY + planetPos.y;
|
|
|
|
planetContainer.x = planetX;
|
|
planetContainer.y = planetY;
|
|
const planetRotationAngle = Phaser.Math.DegToRad(360 * (simulationTime % planetRotationPeriod) / planetRotationPeriod);
|
|
planetContainer.rotation = planetRotationAngle;
|
|
const subsolarAnglePlanet = Phaser.Math.Angle.Between(planetX, planetY, starX, starY);
|
|
planetContainer.nightOverlay.rotation = subsolarAnglePlanet + Math.PI/2 - planetContainer.rotation;
|
|
|
|
// Draw tilt indicator.
|
|
tiltIndicator.clear();
|
|
tiltIndicator.lineStyle(2, 0x00FFFF, 1);
|
|
const dx = (tiltLength/2) * Math.cos(tiltIndicatorAngle);
|
|
const dy = (tiltLength/2) * Math.sin(tiltIndicatorAngle);
|
|
tiltIndicator.strokeLineShape(new Phaser.Geom.Line(
|
|
planetX - dx, planetY - dy,
|
|
planetX + dx, planetY + dy
|
|
));
|
|
|
|
// Draw moon orbits around the planet.
|
|
drawMoonOrbit(planetX, planetY, kerielOrbitRadius, kerielEcc, 0x888888);
|
|
drawMoonOrbit(planetX, planetY, arkaenOrbitRadius, arkaenEcc, 0x888888);
|
|
drawMoonOrbit(planetX, planetY, minianOrbitRadius, minianEcc, 0x888888);
|
|
|
|
// Update each moon.
|
|
updateMoon(kerielContainer, kerielOrbitRadius, kerielEcc, kerielPeriod);
|
|
updateMoon(arkaenContainer, arkaenOrbitRadius, arkaenEcc, arkaenPeriod);
|
|
updateMoon(minianContainer, minianOrbitRadius, minianEcc, minianPeriod);
|
|
|
|
// Build astronomical header text.
|
|
const astroInfo = buildAstronomicalText(planetX, planetY);
|
|
astroText.setText(astroInfo);
|
|
}
|
|
|
|
// Draw elliptical orbit centered at (px,py) with semi-major axis a and eccentricity ecc.
|
|
function drawMoonOrbit(px, py, a, ecc, color) {
|
|
const b = a * Math.sqrt(1 - ecc * ecc);
|
|
globalGraphics.lineStyle(1, color, 1);
|
|
globalGraphics.strokeEllipse(px, py, 2 * a, 2 * b);
|
|
}
|
|
|
|
// Update a moon's position, tidal locking, and its night overlay.
|
|
function updateMoon(moonContainer, a, ecc, period) {
|
|
const pos = getEllipticalPosition(simulationTime, period, a, ecc);
|
|
const moonX = planetContainer.x + pos.x;
|
|
const moonY = planetContainer.y + pos.y;
|
|
moonContainer.x = moonX;
|
|
moonContainer.y = moonY;
|
|
moonContainer.rotation = Phaser.Math.Angle.Between(moonX, moonY, planetContainer.x, planetContainer.y);
|
|
const overlay = moonContainer.list[moonContainer.list.length - 1];
|
|
const subsolarAngleMoon = Phaser.Math.Angle.Between(moonX, moonY, starX, starY);
|
|
overlay.rotation = subsolarAngleMoon + Math.PI/2 - moonContainer.rotation;
|
|
}
|
|
|
|
// Build the astronomical header text.
|
|
function buildAstronomicalText(planetX, planetY) {
|
|
let info = "Senthara\n";
|
|
info += "Date: " + getSentharaDate(simulationTime) + "\n";
|
|
|
|
// For each moon, compute its lit fraction and determine waxing/waning.
|
|
// Keriel:
|
|
const kerielX = kerielContainer.x;
|
|
const kerielY = kerielContainer.y;
|
|
let fracKeriel = getMoonLitFraction(kerielX, kerielY, starX, starY, planetX, planetY);
|
|
let waxingKeriel = (fracKeriel >= oldFracKeriel);
|
|
let emojiKeriel = getMoonPhaseEmoji(fracKeriel, waxingKeriel);
|
|
let kerielDay = Math.floor(simulationTime % kerielPeriod) + 1;
|
|
oldFracKeriel = fracKeriel;
|
|
|
|
info += `Keriel Cycle Day: ${kerielDay} (Illum=${(fracKeriel * 100).toFixed(0)}% ${emojiKeriel})\n`;
|
|
|
|
// Arkaen:
|
|
const arkaenX = arkaenContainer.x;
|
|
const arkaenY = arkaenContainer.y;
|
|
let fracArkaen = getMoonLitFraction(arkaenX, arkaenY, starX, starY, planetX, planetY);
|
|
let waxingArkaen = (fracArkaen >= oldFracArkaen);
|
|
let emojiArkaen = getMoonPhaseEmoji(fracArkaen, waxingArkaen);
|
|
let arkaenDay = Math.floor(simulationTime % arkaenPeriod) + 1;
|
|
oldFracArkaen = fracArkaen;
|
|
|
|
info += `Arkaen Cycle Day: ${arkaenDay} (Illum=${(fracArkaen * 100).toFixed(0)}% ${emojiArkaen})\n`;
|
|
|
|
// Minian:
|
|
const minianX = minianContainer.x;
|
|
const minianY = minianContainer.y;
|
|
let fracMinian = getMoonLitFraction(minianX, minianY, starX, starY, planetX, planetY);
|
|
let waxingMinian = (fracMinian >= oldFracMinian);
|
|
let emojiMinian = getMoonPhaseEmoji(fracMinian, waxingMinian);
|
|
let minianDay = Math.floor(simulationTime % minianPeriod) + 1;
|
|
oldFracMinian = fracMinian;
|
|
|
|
info += `Minian Cycle Day: ${minianDay} (Illum=${(fracMinian * 100).toFixed(0)}% ${emojiMinian})\n`;
|
|
|
|
// Cycle progress.
|
|
let shortProg = Math.floor(simulationTime % shortCycleLen);
|
|
let longProg = Math.floor(simulationTime % longCycleLen);
|
|
info += `Short Convergence Cycle: ${shortProg} / ${shortCycleLen} days (~274 years)\n`;
|
|
info += `Grand Convergence Cycle: ${longProg} / ${longCycleLen} days (~6027 years)`;
|
|
return info;
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|