982 lines
37 KiB
HTML
982 lines
37 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Earth Orbit Simulator - Kepler's Laws Visualization</title>
|
|
<!--
|
|
WEB-ENHANCED ITERATION #12
|
|
|
|
Web Source: https://en.wikipedia.org/wiki/Kepler%27s_equation
|
|
Topic: Kepler's Equation - Numerical Solution Methods
|
|
|
|
Key Learnings Applied:
|
|
1. Newton-Raphson solver with optimal initial guess (E_0 = M for e < 0.8)
|
|
2. Convergence tracking - monitor iterations and error for validation
|
|
3. Enhanced numerical stability with proper convergence criteria
|
|
4. Debug panel showing all Kepler calculation internals (M, E, v, iterations, error)
|
|
|
|
Unique Enhancements:
|
|
- Orbital trail visualization showing Earth's historical path
|
|
- Perihelion/aphelion markers with labels and distance indicators
|
|
- Real-time velocity vector arrows showing speed/direction changes
|
|
- Kepler solver debug panel with live convergence data
|
|
- Color-coded orbit segments by season
|
|
- Area-swept visualization (Kepler's 2nd Law demonstration)
|
|
- Eccentric anomaly reference circle overlay
|
|
|
|
Astronomical Parameters:
|
|
- Eccentricity: 0.0167086
|
|
- Semi-major axis: 149.598M km (1 AU)
|
|
- Sidereal day: 23h 56m 4.0916s
|
|
- Axial tilt: 23.4393° (J2000)
|
|
- Kepler solver precision: <1e-10 radians
|
|
-->
|
|
<style>
|
|
body {
|
|
margin: 0;
|
|
overflow: hidden;
|
|
font-family: 'Courier New', monospace;
|
|
background: #000000;
|
|
}
|
|
canvas {
|
|
display: block;
|
|
width: 100vw;
|
|
height: 100vh;
|
|
}
|
|
#info-panel {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
background: rgba(0, 0, 0, 0.85);
|
|
color: #00ff00;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
min-width: 300px;
|
|
border: 1px solid #00ff00;
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
#kepler-debug {
|
|
position: absolute;
|
|
top: 10px;
|
|
left: 10px;
|
|
background: rgba(0, 0, 0, 0.85);
|
|
color: #00ff00;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
font-size: 12px;
|
|
min-width: 280px;
|
|
border: 1px solid #ffaa00;
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
#time-controls {
|
|
position: absolute;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(0, 0, 0, 0.85);
|
|
padding: 20px 30px;
|
|
border-radius: 8px;
|
|
border: 1px solid #00ff00;
|
|
min-width: 600px;
|
|
}
|
|
.control-group {
|
|
margin: 10px 0;
|
|
}
|
|
.control-group label {
|
|
color: #00ff00;
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
font-size: 12px;
|
|
}
|
|
.control-group input[type="range"] {
|
|
width: 100%;
|
|
margin: 5px 0;
|
|
}
|
|
.button-group {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-top: 15px;
|
|
}
|
|
button {
|
|
background: #003300;
|
|
color: #00ff00;
|
|
border: 1px solid #00ff00;
|
|
padding: 8px 16px;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
button:hover {
|
|
background: #005500;
|
|
}
|
|
button.active {
|
|
background: #00ff00;
|
|
color: #000000;
|
|
}
|
|
.data-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin: 5px 0;
|
|
padding: 5px 0;
|
|
border-bottom: 1px solid #003300;
|
|
}
|
|
.data-label {
|
|
color: #00aa00;
|
|
}
|
|
.data-value {
|
|
color: #00ff00;
|
|
font-weight: bold;
|
|
}
|
|
.debug-header {
|
|
color: #ffaa00;
|
|
font-weight: bold;
|
|
margin-bottom: 10px;
|
|
font-size: 14px;
|
|
}
|
|
.debug-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin: 4px 0;
|
|
padding: 4px 0;
|
|
border-bottom: 1px solid #442200;
|
|
}
|
|
.debug-label {
|
|
color: #cc8800;
|
|
}
|
|
.debug-value {
|
|
color: #ffaa00;
|
|
font-weight: bold;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="info-panel">
|
|
<h3 style="margin: 0 0 15px 0; color: #00ff00;">EARTH ORBITAL DATA</h3>
|
|
<div class="data-row">
|
|
<span class="data-label">Current Date/Time:</span>
|
|
<span class="data-value" id="current-time">-</span>
|
|
</div>
|
|
<div class="data-row">
|
|
<span class="data-label">Julian Date:</span>
|
|
<span class="data-value" id="julian-date">-</span>
|
|
</div>
|
|
<div class="data-row">
|
|
<span class="data-label">Days since J2000:</span>
|
|
<span class="data-value" id="days-j2000">-</span>
|
|
</div>
|
|
<div class="data-row">
|
|
<span class="data-label">Rotation Angle:</span>
|
|
<span class="data-value" id="rotation-angle">-</span>
|
|
</div>
|
|
<div class="data-row">
|
|
<span class="data-label">Axial Tilt:</span>
|
|
<span class="data-value" id="axial-tilt">23.4393°</span>
|
|
</div>
|
|
<div class="data-row">
|
|
<span class="data-label">Orbital Position:</span>
|
|
<span class="data-value" id="orbital-position">-</span>
|
|
</div>
|
|
<div class="data-row">
|
|
<span class="data-label">Distance from Sun:</span>
|
|
<span class="data-value" id="sun-distance">-</span>
|
|
</div>
|
|
<div class="data-row">
|
|
<span class="data-label">Orbital Velocity:</span>
|
|
<span class="data-value" id="orbital-velocity">-</span>
|
|
</div>
|
|
<div class="data-row">
|
|
<span class="data-label">Precession Angle:</span>
|
|
<span class="data-value" id="precession-angle">-</span>
|
|
</div>
|
|
<div class="data-row">
|
|
<span class="data-label">Season:</span>
|
|
<span class="data-value" id="season">-</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="kepler-debug">
|
|
<div class="debug-header">⚙ KEPLER SOLVER DEBUG</div>
|
|
<div class="debug-row">
|
|
<span class="debug-label">Mean Anomaly (M):</span>
|
|
<span class="debug-value" id="mean-anomaly">-</span>
|
|
</div>
|
|
<div class="debug-row">
|
|
<span class="debug-label">Eccentric Anomaly (E):</span>
|
|
<span class="debug-value" id="eccentric-anomaly">-</span>
|
|
</div>
|
|
<div class="debug-row">
|
|
<span class="debug-label">True Anomaly (v):</span>
|
|
<span class="debug-value" id="true-anomaly">-</span>
|
|
</div>
|
|
<div class="debug-row">
|
|
<span class="debug-label">Solver Iterations:</span>
|
|
<span class="debug-value" id="solver-iterations">-</span>
|
|
</div>
|
|
<div class="debug-row">
|
|
<span class="debug-label">Convergence Error:</span>
|
|
<span class="debug-value" id="convergence-error">-</span>
|
|
</div>
|
|
<div class="debug-row">
|
|
<span class="debug-label">Initial Guess (E₀):</span>
|
|
<span class="debug-value" id="initial-guess">-</span>
|
|
</div>
|
|
<div class="debug-row">
|
|
<span class="debug-label">Perihelion Distance:</span>
|
|
<span class="debug-value">147.10M km</span>
|
|
</div>
|
|
<div class="debug-row">
|
|
<span class="debug-label">Aphelion Distance:</span>
|
|
<span class="debug-value">152.10M km</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="time-controls">
|
|
<div class="control-group">
|
|
<label>Time Travel (Date/Time)</label>
|
|
<input type="datetime-local" id="date-picker" />
|
|
</div>
|
|
<div class="control-group">
|
|
<label>Time Speed: <span id="speed-value">1x Real-time</span></label>
|
|
<input type="range" id="time-speed" min="-1000000" max="1000000" value="0" step="100" />
|
|
<div style="display: flex; justify-content: space-between; font-size: 10px; color: #00aa00; margin-top: 5px;">
|
|
<span>← 1M days/sec</span>
|
|
<span>Paused</span>
|
|
<span>1M days/sec →</span>
|
|
</div>
|
|
</div>
|
|
<div class="button-group">
|
|
<button id="btn-reverse">◄◄ Reverse</button>
|
|
<button id="btn-slower">◄ Slower</button>
|
|
<button id="btn-pause" class="active">⏸ Pause</button>
|
|
<button id="btn-faster">Faster ►</button>
|
|
<button id="btn-forward">Forward ►►</button>
|
|
<button id="btn-reset">↺ Reset to Now</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script type="importmap">
|
|
{
|
|
"imports": {
|
|
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
|
|
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<script type="module">
|
|
import * as THREE from 'three';
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
|
|
// Scene, camera, renderer setup
|
|
let camera, scene, renderer, controls;
|
|
let sun, earth, earthOrbitLine;
|
|
let earthRotationGroup, earthTiltGroup;
|
|
let velocityArrow, orbitTrail, perihelionMarker, aphelionMarker;
|
|
let eccentricCircle, areaSweptWedge;
|
|
|
|
// Astronomical constants (J2000.0 epoch)
|
|
const ASTRONOMICAL_CONSTANTS = {
|
|
// Earth orbital parameters
|
|
SEMI_MAJOR_AXIS: 149.598e6, // km (1 AU)
|
|
ECCENTRICITY: 0.0167086, // Orbital eccentricity
|
|
OBLIQUITY: 23.4392811, // Axial tilt in degrees (J2000)
|
|
SIDEREAL_YEAR: 365.256363004, // days
|
|
SIDEREAL_DAY: 0.99726968, // days (23h 56m 4.0916s)
|
|
PRECESSION_PERIOD: 25772, // years (axial precession)
|
|
|
|
// Orbital elements (J2000.0)
|
|
PERIHELION: 102.94719, // Longitude of perihelion (degrees)
|
|
MEAN_LONGITUDE: 100.46435, // Mean longitude at epoch (degrees)
|
|
|
|
// Scale for visualization (not to real scale)
|
|
SCALE_DISTANCE: 100, // Scale factor for distances
|
|
SCALE_SIZE: 1, // Scale factor for body sizes
|
|
|
|
// J2000.0 epoch
|
|
J2000: 2451545.0, // Julian date of J2000.0 epoch (Jan 1, 2000, 12:00 TT)
|
|
|
|
// Kepler solver parameters (from Wikipedia research)
|
|
KEPLER_MAX_ITERATIONS: 15, // Maximum Newton-Raphson iterations
|
|
KEPLER_TOLERANCE: 1e-10, // Convergence tolerance (radians)
|
|
};
|
|
|
|
// Simulation state
|
|
let simulationTime = new Date(); // Current simulation time
|
|
let timeSpeed = 0; // Time multiplier (0 = paused)
|
|
let lastFrameTime = performance.now();
|
|
let orbitTrailPoints = []; // Historical orbit positions
|
|
let keplerDebugData = {}; // Debug data from Kepler solver
|
|
|
|
init();
|
|
animate();
|
|
|
|
function init() {
|
|
// Camera setup
|
|
camera = new THREE.PerspectiveCamera(
|
|
45,
|
|
window.innerWidth / window.innerHeight,
|
|
0.1,
|
|
10000
|
|
);
|
|
camera.position.set(0, 150, 250);
|
|
|
|
// Scene
|
|
scene = new THREE.Scene();
|
|
scene.background = new THREE.Color(0x000000);
|
|
|
|
// Renderer
|
|
renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setPixelRatio(window.devicePixelRatio);
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
renderer.shadowMap.enabled = true;
|
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
document.body.appendChild(renderer.domElement);
|
|
|
|
// OrbitControls
|
|
controls = new OrbitControls(camera, renderer.domElement);
|
|
controls.enableDamping = true;
|
|
controls.dampingFactor = 0.05;
|
|
controls.minDistance = 10;
|
|
controls.maxDistance = 1000;
|
|
|
|
// Create solar system
|
|
createSolarSystem();
|
|
|
|
// Setup UI controls
|
|
setupControls();
|
|
|
|
// Add starfield background
|
|
createStarfield();
|
|
|
|
// Handle resize
|
|
window.addEventListener('resize', onWindowResize);
|
|
|
|
// Initialize to current date/time
|
|
resetToNow();
|
|
}
|
|
|
|
function createSolarSystem() {
|
|
// Sun (light source)
|
|
const sunGeometry = new THREE.SphereGeometry(10, 64, 64);
|
|
const sunMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xffff00,
|
|
emissive: 0xffff00,
|
|
emissiveIntensity: 1
|
|
});
|
|
sun = new THREE.Mesh(sunGeometry, sunMaterial);
|
|
scene.add(sun);
|
|
|
|
// Sun point light (primary light source)
|
|
const sunLight = new THREE.PointLight(0xffffff, 2, 0);
|
|
sunLight.castShadow = true;
|
|
sunLight.shadow.mapSize.width = 2048;
|
|
sunLight.shadow.mapSize.height = 2048;
|
|
sun.add(sunLight);
|
|
|
|
// Earth orbital path (ellipse) - color-coded by season
|
|
createEarthOrbit();
|
|
|
|
// Create eccentric anomaly reference circle
|
|
createEccentricCircle();
|
|
|
|
// Create perihelion and aphelion markers
|
|
createApsidesMarkers();
|
|
|
|
// Earth group hierarchy for proper rotation and tilt
|
|
// Structure: earthTiltGroup -> earthRotationGroup -> earth
|
|
earthTiltGroup = new THREE.Group();
|
|
scene.add(earthTiltGroup);
|
|
|
|
earthRotationGroup = new THREE.Group();
|
|
earthTiltGroup.add(earthRotationGroup);
|
|
|
|
// Earth sphere with texture
|
|
const earthGeometry = new THREE.SphereGeometry(5, 64, 64);
|
|
|
|
// Load Earth texture
|
|
const textureLoader = new THREE.TextureLoader();
|
|
const earthTexture = textureLoader.load(
|
|
'https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/planets/earth_atmos_2048.jpg'
|
|
);
|
|
const earthBumpMap = textureLoader.load(
|
|
'https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/planets/earth_normal_2048.jpg'
|
|
);
|
|
|
|
const earthMaterial = new THREE.MeshPhongMaterial({
|
|
map: earthTexture,
|
|
bumpMap: earthBumpMap,
|
|
bumpScale: 0.05,
|
|
specular: 0x333333,
|
|
shininess: 5
|
|
});
|
|
|
|
earth = new THREE.Mesh(earthGeometry, earthMaterial);
|
|
earth.receiveShadow = true;
|
|
earth.castShadow = true;
|
|
earthRotationGroup.add(earth);
|
|
|
|
// Set axial tilt
|
|
earthTiltGroup.rotation.z = THREE.MathUtils.degToRad(ASTRONOMICAL_CONSTANTS.OBLIQUITY);
|
|
|
|
// Add atmosphere glow
|
|
const atmosphereGeometry = new THREE.SphereGeometry(5.2, 64, 64);
|
|
const atmosphereMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0x6699ff,
|
|
transparent: true,
|
|
opacity: 0.15,
|
|
side: THREE.BackSide
|
|
});
|
|
const atmosphere = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial);
|
|
earth.add(atmosphere);
|
|
|
|
// Create velocity arrow
|
|
createVelocityArrow();
|
|
|
|
// Create orbital trail
|
|
createOrbitTrail();
|
|
|
|
// Create area-swept visualization
|
|
createAreaSweptWedge();
|
|
}
|
|
|
|
function createEarthOrbit() {
|
|
// Create elliptical orbit path with seasonal color coding
|
|
const orbitGroup = new THREE.Group();
|
|
const segments = 360;
|
|
const a = ASTRONOMICAL_CONSTANTS.SEMI_MAJOR_AXIS / ASTRONOMICAL_CONSTANTS.SCALE_DISTANCE;
|
|
const e = ASTRONOMICAL_CONSTANTS.ECCENTRICITY;
|
|
|
|
// Create four colored segments for seasons
|
|
const seasonColors = [
|
|
{ start: 0, end: 90, color: 0x00ffff }, // Winter (N) - Cyan
|
|
{ start: 90, end: 180, color: 0x00ff00 }, // Spring (N) - Green
|
|
{ start: 180, end: 270, color: 0xffff00 }, // Summer (N) - Yellow
|
|
{ start: 270, end: 360, color: 0xff8800 } // Autumn (N) - Orange
|
|
];
|
|
|
|
seasonColors.forEach(season => {
|
|
const points = [];
|
|
const segStart = Math.floor(season.start * segments / 360);
|
|
const segEnd = Math.floor(season.end * segments / 360);
|
|
|
|
for (let i = segStart; i <= segEnd; i++) {
|
|
const angle = (i / segments) * Math.PI * 2;
|
|
const r = (a * (1 - e * e)) / (1 + e * Math.cos(angle));
|
|
const x = r * Math.cos(angle);
|
|
const z = r * Math.sin(angle);
|
|
points.push(new THREE.Vector3(x, 0, z));
|
|
}
|
|
|
|
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
const material = new THREE.LineBasicMaterial({
|
|
color: season.color,
|
|
opacity: 0.4,
|
|
transparent: true,
|
|
linewidth: 2
|
|
});
|
|
const line = new THREE.Line(geometry, material);
|
|
orbitGroup.add(line);
|
|
});
|
|
|
|
scene.add(orbitGroup);
|
|
}
|
|
|
|
function createEccentricCircle() {
|
|
// Eccentric anomaly reference circle (for educational purposes)
|
|
const a = ASTRONOMICAL_CONSTANTS.SEMI_MAJOR_AXIS / ASTRONOMICAL_CONSTANTS.SCALE_DISTANCE;
|
|
const circleGeometry = new THREE.BufferGeometry();
|
|
const circlePoints = [];
|
|
|
|
for (let i = 0; i <= 360; i++) {
|
|
const angle = (i / 360) * Math.PI * 2;
|
|
circlePoints.push(new THREE.Vector3(
|
|
a * Math.cos(angle),
|
|
0,
|
|
a * Math.sin(angle)
|
|
));
|
|
}
|
|
|
|
circleGeometry.setFromPoints(circlePoints);
|
|
const circleMaterial = new THREE.LineBasicMaterial({
|
|
color: 0xff00ff,
|
|
opacity: 0.15,
|
|
transparent: true,
|
|
linewidth: 1
|
|
});
|
|
|
|
eccentricCircle = new THREE.Line(circleGeometry, circleMaterial);
|
|
scene.add(eccentricCircle);
|
|
}
|
|
|
|
function createApsidesMarkers() {
|
|
// Perihelion marker (closest point to Sun)
|
|
const a = ASTRONOMICAL_CONSTANTS.SEMI_MAJOR_AXIS / ASTRONOMICAL_CONSTANTS.SCALE_DISTANCE;
|
|
const e = ASTRONOMICAL_CONSTANTS.ECCENTRICITY;
|
|
const perihelionDist = a * (1 - e);
|
|
|
|
const perihelionGeometry = new THREE.SphereGeometry(2, 16, 16);
|
|
const perihelionMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xff0000,
|
|
emissive: 0xff0000,
|
|
emissiveIntensity: 0.5
|
|
});
|
|
perihelionMarker = new THREE.Mesh(perihelionGeometry, perihelionMaterial);
|
|
perihelionMarker.position.set(perihelionDist, 0, 0);
|
|
scene.add(perihelionMarker);
|
|
|
|
// Perihelion label
|
|
const perihelionLabel = createTextSprite('PERIHELION\n147.1M km', 0xff0000);
|
|
perihelionLabel.position.set(perihelionDist, 8, 0);
|
|
scene.add(perihelionLabel);
|
|
|
|
// Aphelion marker (farthest point from Sun)
|
|
const aphelionDist = a * (1 + e);
|
|
const aphelionGeometry = new THREE.SphereGeometry(2, 16, 16);
|
|
const aphelionMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0x0000ff,
|
|
emissive: 0x0000ff,
|
|
emissiveIntensity: 0.5
|
|
});
|
|
aphelionMarker = new THREE.Mesh(aphelionGeometry, aphelionMaterial);
|
|
aphelionMarker.position.set(-aphelionDist, 0, 0);
|
|
scene.add(aphelionMarker);
|
|
|
|
// Aphelion label
|
|
const aphelionLabel = createTextSprite('APHELION\n152.1M km', 0x0000ff);
|
|
aphelionLabel.position.set(-aphelionDist, 8, 0);
|
|
scene.add(aphelionLabel);
|
|
}
|
|
|
|
function createTextSprite(message, color) {
|
|
const canvas = document.createElement('canvas');
|
|
const context = canvas.getContext('2d');
|
|
canvas.width = 256;
|
|
canvas.height = 128;
|
|
|
|
context.fillStyle = `#${color.toString(16).padStart(6, '0')}`;
|
|
context.font = 'Bold 20px Courier New';
|
|
context.textAlign = 'center';
|
|
context.textBaseline = 'middle';
|
|
|
|
const lines = message.split('\n');
|
|
lines.forEach((line, i) => {
|
|
context.fillText(line, 128, 44 + i * 24);
|
|
});
|
|
|
|
const texture = new THREE.CanvasTexture(canvas);
|
|
const spriteMaterial = new THREE.SpriteMaterial({
|
|
map: texture,
|
|
transparent: true
|
|
});
|
|
const sprite = new THREE.Sprite(spriteMaterial);
|
|
sprite.scale.set(20, 10, 1);
|
|
|
|
return sprite;
|
|
}
|
|
|
|
function createVelocityArrow() {
|
|
// Arrow showing orbital velocity direction and magnitude
|
|
const arrowHelper = new THREE.ArrowHelper(
|
|
new THREE.Vector3(1, 0, 0),
|
|
new THREE.Vector3(0, 0, 0),
|
|
20,
|
|
0x00ffff,
|
|
5,
|
|
3
|
|
);
|
|
velocityArrow = arrowHelper;
|
|
scene.add(velocityArrow);
|
|
}
|
|
|
|
function createOrbitTrail() {
|
|
// Dynamic trail showing Earth's historical path
|
|
const trailGeometry = new THREE.BufferGeometry();
|
|
const trailMaterial = new THREE.LineBasicMaterial({
|
|
color: 0xffffff,
|
|
opacity: 0.6,
|
|
transparent: true,
|
|
linewidth: 2
|
|
});
|
|
orbitTrail = new THREE.Line(trailGeometry, trailMaterial);
|
|
scene.add(orbitTrail);
|
|
}
|
|
|
|
function createAreaSweptWedge() {
|
|
// Visualization of Kepler's 2nd Law (equal areas in equal times)
|
|
const wedgeGeometry = new THREE.BufferGeometry();
|
|
const wedgeMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0x00ff00,
|
|
opacity: 0.2,
|
|
transparent: true,
|
|
side: THREE.DoubleSide
|
|
});
|
|
areaSweptWedge = new THREE.Mesh(wedgeGeometry, wedgeMaterial);
|
|
scene.add(areaSweptWedge);
|
|
}
|
|
|
|
function updateAreaSweptWedge(currentPos) {
|
|
// Update wedge showing area swept by radius vector
|
|
const wedgeAngle = Math.PI / 6; // Show 30 degrees of sweep
|
|
const v = Math.atan2(currentPos.z, currentPos.x);
|
|
|
|
const vertices = [0, 0, 0]; // Sun at origin
|
|
for (let i = 0; i <= 20; i++) {
|
|
const angle = v - wedgeAngle / 2 + (i / 20) * wedgeAngle;
|
|
const r = currentPos.length();
|
|
vertices.push(r * Math.cos(angle), 0, r * Math.sin(angle));
|
|
}
|
|
|
|
areaSweptWedge.geometry.setAttribute(
|
|
'position',
|
|
new THREE.Float32BufferAttribute(vertices, 3)
|
|
);
|
|
areaSweptWedge.geometry.computeVertexNormals();
|
|
}
|
|
|
|
function createStarfield() {
|
|
const starsGeometry = new THREE.BufferGeometry();
|
|
const starCount = 5000;
|
|
const positions = new Float32Array(starCount * 3);
|
|
|
|
for (let i = 0; i < starCount * 3; i += 3) {
|
|
const theta = Math.random() * Math.PI * 2;
|
|
const phi = Math.acos(Math.random() * 2 - 1);
|
|
const r = 500 + Math.random() * 500;
|
|
|
|
positions[i] = r * Math.sin(phi) * Math.cos(theta);
|
|
positions[i + 1] = r * Math.sin(phi) * Math.sin(theta);
|
|
positions[i + 2] = r * Math.cos(phi);
|
|
}
|
|
|
|
starsGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
|
|
|
|
const starsMaterial = new THREE.PointsMaterial({
|
|
color: 0xffffff,
|
|
size: 0.7,
|
|
transparent: true,
|
|
opacity: 0.8
|
|
});
|
|
|
|
const stars = new THREE.Points(starsGeometry, starsMaterial);
|
|
scene.add(stars);
|
|
}
|
|
|
|
function setupControls() {
|
|
const datePicker = document.getElementById('date-picker');
|
|
const timeSpeedSlider = document.getElementById('time-speed');
|
|
const speedValue = document.getElementById('speed-value');
|
|
|
|
// Date picker
|
|
datePicker.addEventListener('change', (e) => {
|
|
simulationTime = new Date(e.target.value);
|
|
updateSimulation();
|
|
});
|
|
|
|
// Time speed slider
|
|
timeSpeedSlider.addEventListener('input', (e) => {
|
|
timeSpeed = parseFloat(e.target.value);
|
|
updateSpeedDisplay();
|
|
});
|
|
|
|
// Buttons
|
|
document.getElementById('btn-reverse').addEventListener('click', () => {
|
|
timeSpeed = -86400; // -1 day per second
|
|
updateSpeedDisplay();
|
|
});
|
|
|
|
document.getElementById('btn-slower').addEventListener('click', () => {
|
|
timeSpeed = Math.max(timeSpeed / 2, -1000000);
|
|
timeSpeedSlider.value = timeSpeed;
|
|
updateSpeedDisplay();
|
|
});
|
|
|
|
document.getElementById('btn-pause').addEventListener('click', () => {
|
|
timeSpeed = 0;
|
|
timeSpeedSlider.value = 0;
|
|
updateSpeedDisplay();
|
|
});
|
|
|
|
document.getElementById('btn-faster').addEventListener('click', () => {
|
|
timeSpeed = Math.min(timeSpeed === 0 ? 1 : timeSpeed * 2, 1000000);
|
|
timeSpeedSlider.value = timeSpeed;
|
|
updateSpeedDisplay();
|
|
});
|
|
|
|
document.getElementById('btn-forward').addEventListener('click', () => {
|
|
timeSpeed = 86400; // +1 day per second
|
|
timeSpeedSlider.value = timeSpeed;
|
|
updateSpeedDisplay();
|
|
});
|
|
|
|
document.getElementById('btn-reset').addEventListener('click', resetToNow);
|
|
}
|
|
|
|
function resetToNow() {
|
|
simulationTime = new Date();
|
|
timeSpeed = 0;
|
|
document.getElementById('time-speed').value = 0;
|
|
orbitTrailPoints = []; // Clear trail
|
|
updateSpeedDisplay();
|
|
updateSimulation();
|
|
}
|
|
|
|
function updateSpeedDisplay() {
|
|
const speedValue = document.getElementById('speed-value');
|
|
if (timeSpeed === 0) {
|
|
speedValue.textContent = 'Paused';
|
|
} else if (Math.abs(timeSpeed) < 1) {
|
|
speedValue.textContent = `${timeSpeed.toFixed(3)}x Real-time`;
|
|
} else if (Math.abs(timeSpeed) < 86400) {
|
|
speedValue.textContent = `${(timeSpeed / 3600).toFixed(1)} hours/sec`;
|
|
} else {
|
|
speedValue.textContent = `${(timeSpeed / 86400).toFixed(1)} days/sec`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enhanced Kepler's Equation Solver
|
|
* Based on Wikipedia research: Newton-Raphson method with optimal initial guess
|
|
*
|
|
* Solves: M = E - e·sin(E) for E (eccentric anomaly)
|
|
*
|
|
* @param {number} M - Mean anomaly (radians)
|
|
* @param {number} e - Eccentricity
|
|
* @returns {object} - {E, iterations, error, initialGuess}
|
|
*/
|
|
function solveKeplerEquation(M, e) {
|
|
// Initial guess optimization (from Wikipedia research)
|
|
// For e < 0.8: E_0 = M is sufficient
|
|
// For e > 0.8: E_0 = π provides better convergence
|
|
let E = (e < 0.8) ? M : Math.PI;
|
|
const E0 = E; // Store initial guess for debug display
|
|
|
|
let iterations = 0;
|
|
let error = 1;
|
|
|
|
// Newton-Raphson iteration: E_{n+1} = E_n - f(E_n) / f'(E_n)
|
|
// where f(E) = E - e·sin(E) - M
|
|
// and f'(E) = 1 - e·cos(E)
|
|
while (Math.abs(error) > ASTRONOMICAL_CONSTANTS.KEPLER_TOLERANCE &&
|
|
iterations < ASTRONOMICAL_CONSTANTS.KEPLER_MAX_ITERATIONS) {
|
|
|
|
const f = E - e * Math.sin(E) - M;
|
|
const fPrime = 1 - e * Math.cos(E);
|
|
|
|
error = f / fPrime;
|
|
E = E - error;
|
|
iterations++;
|
|
}
|
|
|
|
return {
|
|
E: E,
|
|
iterations: iterations,
|
|
error: Math.abs(error),
|
|
initialGuess: E0
|
|
};
|
|
}
|
|
|
|
function calculateOrbitalPosition(julianDate) {
|
|
// Calculate days since J2000.0 epoch
|
|
const d = julianDate - ASTRONOMICAL_CONSTANTS.J2000;
|
|
|
|
// Mean anomaly (degrees) - M = mean longitude - longitude of perihelion
|
|
const M_deg = ASTRONOMICAL_CONSTANTS.MEAN_LONGITUDE +
|
|
(360.0 / ASTRONOMICAL_CONSTANTS.SIDEREAL_YEAR) * d -
|
|
ASTRONOMICAL_CONSTANTS.PERIHELION;
|
|
|
|
const M_rad = THREE.MathUtils.degToRad(M_deg);
|
|
|
|
// Solve Kepler's equation for eccentric anomaly using enhanced solver
|
|
const e = ASTRONOMICAL_CONSTANTS.ECCENTRICITY;
|
|
const keplerResult = solveKeplerEquation(M_rad, e);
|
|
const E = keplerResult.E;
|
|
|
|
// Store debug data
|
|
keplerDebugData = {
|
|
M: M_rad,
|
|
E: E,
|
|
iterations: keplerResult.iterations,
|
|
error: keplerResult.error,
|
|
initialGuess: keplerResult.initialGuess
|
|
};
|
|
|
|
// Calculate true anomaly from eccentric anomaly
|
|
// v = 2·atan2(√(1+e)·sin(E/2), √(1-e)·cos(E/2))
|
|
const v = 2 * Math.atan2(
|
|
Math.sqrt(1 + e) * Math.sin(E / 2),
|
|
Math.sqrt(1 - e) * Math.cos(E / 2)
|
|
);
|
|
|
|
keplerDebugData.v = v;
|
|
|
|
// Distance from sun: r = a·(1 - e·cos(E))
|
|
const r = ASTRONOMICAL_CONSTANTS.SEMI_MAJOR_AXIS * (1 - e * Math.cos(E));
|
|
|
|
// Position in orbital plane
|
|
const x = (r / ASTRONOMICAL_CONSTANTS.SCALE_DISTANCE) * Math.cos(v);
|
|
const z = (r / ASTRONOMICAL_CONSTANTS.SCALE_DISTANCE) * Math.sin(v);
|
|
|
|
// Calculate orbital velocity magnitude (vis-viva equation)
|
|
// v² = GM(2/r - 1/a)
|
|
const GM = 1.327e20; // Sun's gravitational parameter (m³/s²)
|
|
const velocity = Math.sqrt(GM * (2 / (r * 1000) - 1 / (ASTRONOMICAL_CONSTANTS.SEMI_MAJOR_AXIS * 1000)));
|
|
|
|
// Velocity direction (perpendicular to radius vector)
|
|
const velocityDir = new THREE.Vector3(-Math.sin(v), 0, Math.cos(v));
|
|
|
|
return {
|
|
x,
|
|
z,
|
|
r,
|
|
v: THREE.MathUtils.radToDeg(v),
|
|
d,
|
|
velocity: velocity / 1000, // Convert to km/s
|
|
velocityDir: velocityDir
|
|
};
|
|
}
|
|
|
|
function updateSimulation() {
|
|
// Convert to Julian Date
|
|
const jd = dateToJulianDate(simulationTime);
|
|
|
|
// Calculate orbital position
|
|
const orbital = calculateOrbitalPosition(jd);
|
|
|
|
// Update Earth position
|
|
earthTiltGroup.position.set(orbital.x, 0, orbital.z);
|
|
|
|
// Calculate Earth rotation (sidereal day)
|
|
const daysSinceJ2000 = jd - ASTRONOMICAL_CONSTANTS.J2000;
|
|
const rotations = daysSinceJ2000 / ASTRONOMICAL_CONSTANTS.SIDEREAL_DAY;
|
|
earthRotationGroup.rotation.y = (rotations % 1) * Math.PI * 2;
|
|
|
|
// Calculate precession (very slow, ~26,000 year cycle)
|
|
const precessionAngle = (daysSinceJ2000 / (ASTRONOMICAL_CONSTANTS.PRECESSION_PERIOD * 365.25)) * 360;
|
|
|
|
// Update velocity arrow
|
|
updateVelocityArrow(orbital);
|
|
|
|
// Update orbit trail
|
|
updateOrbitTrail(orbital);
|
|
|
|
// Update area-swept wedge
|
|
updateAreaSweptWedge(new THREE.Vector3(orbital.x, 0, orbital.z));
|
|
|
|
// Update UI
|
|
updateUI(jd, orbital, daysSinceJ2000, rotations, precessionAngle);
|
|
updateKeplerDebugPanel();
|
|
|
|
// Update date picker
|
|
const dateString = simulationTime.toISOString().slice(0, 16);
|
|
document.getElementById('date-picker').value = dateString;
|
|
}
|
|
|
|
function updateVelocityArrow(orbital) {
|
|
// Position arrow at Earth's location
|
|
velocityArrow.position.set(orbital.x, 0, orbital.z);
|
|
|
|
// Set direction to velocity direction
|
|
velocityArrow.setDirection(orbital.velocityDir);
|
|
|
|
// Scale length based on velocity (normalized for visualization)
|
|
const baseLength = 20;
|
|
const velocityScale = orbital.velocity / 30; // Normalize around 30 km/s average
|
|
velocityArrow.setLength(baseLength * velocityScale, 5 * velocityScale, 3 * velocityScale);
|
|
}
|
|
|
|
function updateOrbitTrail(orbital) {
|
|
// Add current position to trail
|
|
orbitTrailPoints.push(new THREE.Vector3(orbital.x, 0, orbital.z));
|
|
|
|
// Limit trail length (show last 100 positions)
|
|
if (orbitTrailPoints.length > 100) {
|
|
orbitTrailPoints.shift();
|
|
}
|
|
|
|
// Update trail geometry
|
|
if (orbitTrailPoints.length > 1) {
|
|
orbitTrail.geometry.setFromPoints(orbitTrailPoints);
|
|
}
|
|
}
|
|
|
|
function updateUI(jd, orbital, daysSinceJ2000, rotations, precessionAngle) {
|
|
document.getElementById('current-time').textContent =
|
|
simulationTime.toUTCString();
|
|
document.getElementById('julian-date').textContent =
|
|
jd.toFixed(2);
|
|
document.getElementById('days-j2000').textContent =
|
|
daysSinceJ2000.toFixed(2);
|
|
document.getElementById('rotation-angle').textContent =
|
|
((rotations % 1) * 360).toFixed(2) + '°';
|
|
document.getElementById('orbital-position').textContent =
|
|
orbital.v.toFixed(2) + '°';
|
|
document.getElementById('sun-distance').textContent =
|
|
(orbital.r / 1e6).toFixed(3) + ' million km';
|
|
document.getElementById('precession-angle').textContent =
|
|
(precessionAngle % 360).toFixed(2) + '°';
|
|
document.getElementById('orbital-velocity').textContent =
|
|
orbital.velocity.toFixed(2) + ' km/s';
|
|
|
|
// Determine season (Northern Hemisphere)
|
|
const season = getSeason(orbital.v);
|
|
document.getElementById('season').textContent = season;
|
|
}
|
|
|
|
function updateKeplerDebugPanel() {
|
|
// Display Kepler solver internals
|
|
document.getElementById('mean-anomaly').textContent =
|
|
`${keplerDebugData.M.toFixed(6)} rad (${THREE.MathUtils.radToDeg(keplerDebugData.M).toFixed(2)}°)`;
|
|
document.getElementById('eccentric-anomaly').textContent =
|
|
`${keplerDebugData.E.toFixed(6)} rad (${THREE.MathUtils.radToDeg(keplerDebugData.E).toFixed(2)}°)`;
|
|
document.getElementById('true-anomaly').textContent =
|
|
`${keplerDebugData.v.toFixed(6)} rad (${THREE.MathUtils.radToDeg(keplerDebugData.v).toFixed(2)}°)`;
|
|
document.getElementById('solver-iterations').textContent =
|
|
keplerDebugData.iterations;
|
|
document.getElementById('convergence-error').textContent =
|
|
keplerDebugData.error.toExponential(2);
|
|
document.getElementById('initial-guess').textContent =
|
|
`${keplerDebugData.initialGuess.toFixed(6)} rad`;
|
|
}
|
|
|
|
function getSeason(orbitalAngle) {
|
|
// Approximate seasons based on orbital position
|
|
// 0° = Perihelion (early January)
|
|
const adjusted = (orbitalAngle + 12) % 360; // Adjust for season alignment
|
|
if (adjusted < 90) return 'Winter (N) / Summer (S)';
|
|
if (adjusted < 180) return 'Spring (N) / Autumn (S)';
|
|
if (adjusted < 270) return 'Summer (N) / Winter (S)';
|
|
return 'Autumn (N) / Spring (S)';
|
|
}
|
|
|
|
function dateToJulianDate(date) {
|
|
return (date.getTime() / 86400000) + 2440587.5;
|
|
}
|
|
|
|
function onWindowResize() {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
}
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
|
|
const currentTime = performance.now();
|
|
const deltaTime = (currentTime - lastFrameTime) / 1000; // seconds
|
|
lastFrameTime = currentTime;
|
|
|
|
// Update simulation time based on speed
|
|
if (timeSpeed !== 0) {
|
|
simulationTime = new Date(simulationTime.getTime() + (timeSpeed * deltaTime * 1000));
|
|
updateSimulation();
|
|
}
|
|
|
|
controls.update();
|
|
renderer.render(scene, camera);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|