771 lines
31 KiB
HTML
771 lines
31 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 - Enhanced Visual Realism (Iteration 13)</title>
|
|
<!--
|
|
WEB LEARNING SOURCE: https://mattloftus.github.io/2016/02/03/threejs-p2/
|
|
Three.js Earth-Moon System Tutorial
|
|
|
|
KEY LEARNINGS APPLIED:
|
|
1. Multi-layer texture approach: Base texture + transparent cloud layer as separate sphere
|
|
- Cloud layer at radius 5.15 (slightly larger than Earth at 5.0)
|
|
- Opacity 0.15 for realistic transparency without obscuring surface
|
|
|
|
2. Material properties for realism:
|
|
- MeshPhongMaterial with specular highlights (0x333333) for ocean reflections
|
|
- Shininess: 5 for controlled reflection characteristics
|
|
- Separate materials for day/night textures with shader-based blending
|
|
|
|
3. Enhanced lighting setup:
|
|
- Ambient light (0x222222) for base illumination without washing out shadows
|
|
- Point light at Sun with proper intensity (2.5) for realistic solar illumination
|
|
- Shadow mapping enabled for accurate day/night terminator
|
|
|
|
UNIQUE ENHANCEMENTS FOR ITERATION 13:
|
|
- Multi-texture Earth with day/night/clouds/specular/normal maps
|
|
- Custom shader for day/night texture blending based on sun position
|
|
- Animated cloud layer (slower rotation than Earth)
|
|
- Enhanced atmospheric glow with gradient shader
|
|
- Lens flare effect from Sun
|
|
- Camera preset system for different viewing angles
|
|
- Premium visual quality while maintaining 60fps performance
|
|
-->
|
|
<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;
|
|
}
|
|
#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;
|
|
flex-wrap: wrap;
|
|
}
|
|
button {
|
|
background: #003300;
|
|
color: #00ff00;
|
|
border: 1px solid #00ff00;
|
|
padding: 8px 16px;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 11px;
|
|
}
|
|
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;
|
|
}
|
|
#camera-presets {
|
|
margin-top: 15px;
|
|
padding-top: 15px;
|
|
border-top: 1px solid #003300;
|
|
}
|
|
#camera-presets h4 {
|
|
margin: 0 0 10px 0;
|
|
color: #00ff00;
|
|
font-size: 12px;
|
|
}
|
|
</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="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">Paused</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 id="camera-presets">
|
|
<h4>CAMERA PRESETS:</h4>
|
|
<div class="button-group">
|
|
<button id="cam-default">Default View</button>
|
|
<button id="cam-equinox">Equinox View</button>
|
|
<button id="cam-solstice">Solstice View</button>
|
|
<button id="cam-polar">Polar View</button>
|
|
<button id="cam-follow">Following Earth</button>
|
|
</div>
|
|
</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';
|
|
import { Lensflare, LensflareElement } from 'three/addons/objects/Lensflare.js';
|
|
|
|
// Scene, camera, renderer setup
|
|
let camera, scene, renderer, controls;
|
|
let sun, earth, earthClouds, earthOrbitLine;
|
|
let earthRotationGroup, earthTiltGroup;
|
|
let followEarthMode = false;
|
|
|
|
// Astronomical constants (J2000.0 epoch)
|
|
const ASTRONOMICAL_CONSTANTS = {
|
|
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)
|
|
PERIHELION: 102.94719, // Longitude of perihelion (degrees)
|
|
MEAN_LONGITUDE: 100.46435, // Mean longitude at epoch (degrees)
|
|
SCALE_DISTANCE: 100, // Scale factor for distances
|
|
SCALE_SIZE: 1, // Scale factor for body sizes
|
|
J2000: 2451545.0, // Julian date of J2000.0 epoch
|
|
};
|
|
|
|
// Simulation state
|
|
let simulationTime = new Date();
|
|
let timeSpeed = 0;
|
|
let lastFrameTime = performance.now();
|
|
let cloudRotation = 0;
|
|
|
|
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 with enhanced settings for premium visuals
|
|
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;
|
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
renderer.toneMappingExposure = 1.2;
|
|
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) - learning from tutorial: proper emissive material
|
|
const sunGeometry = new THREE.SphereGeometry(10, 64, 64);
|
|
const sunMaterial = new THREE.MeshBasicMaterial({
|
|
color: 0xffaa33,
|
|
emissive: 0xffaa33,
|
|
emissiveIntensity: 1
|
|
});
|
|
sun = new THREE.Mesh(sunGeometry, sunMaterial);
|
|
scene.add(sun);
|
|
|
|
// Sun point light - tutorial learning: balanced intensity for realistic illumination
|
|
const sunLight = new THREE.PointLight(0xffffff, 2.5, 0);
|
|
sunLight.castShadow = true;
|
|
sunLight.shadow.mapSize.width = 2048;
|
|
sunLight.shadow.mapSize.height = 2048;
|
|
sunLight.shadow.camera.near = 0.5;
|
|
sunLight.shadow.camera.far = 500;
|
|
sun.add(sunLight);
|
|
|
|
// Add lens flare effect - premium visual enhancement
|
|
addLensFlare(sun);
|
|
|
|
// Ambient light - tutorial learning: subtle ambient (0x222222) prevents pure black shadows
|
|
const ambientLight = new THREE.AmbientLight(0x222222, 0.3);
|
|
scene.add(ambientLight);
|
|
|
|
// Earth orbital path (ellipse)
|
|
createEarthOrbit();
|
|
|
|
// Earth group hierarchy for proper rotation and tilt
|
|
earthTiltGroup = new THREE.Group();
|
|
scene.add(earthTiltGroup);
|
|
|
|
earthRotationGroup = new THREE.Group();
|
|
earthTiltGroup.add(earthRotationGroup);
|
|
|
|
// Create multi-textured Earth with day/night blending
|
|
createEarthWithMultiTextures();
|
|
|
|
// Set axial tilt
|
|
earthTiltGroup.rotation.z = THREE.MathUtils.degToRad(ASTRONOMICAL_CONSTANTS.OBLIQUITY);
|
|
}
|
|
|
|
function createEarthWithMultiTextures() {
|
|
const earthGeometry = new THREE.SphereGeometry(5, 64, 64);
|
|
const textureLoader = new THREE.TextureLoader();
|
|
|
|
// Custom shader for day/night texture blending
|
|
// This allows smooth transition based on sun position
|
|
const earthShaderMaterial = new THREE.ShaderMaterial({
|
|
uniforms: {
|
|
dayTexture: { value: textureLoader.load('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/planets/earth_atmos_2048.jpg') },
|
|
nightTexture: { value: textureLoader.load('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/planets/earth_lights_2048.png') },
|
|
normalMap: { value: textureLoader.load('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/planets/earth_normal_2048.jpg') },
|
|
specularMap: { value: textureLoader.load('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/planets/earth_specular_2048.jpg') },
|
|
sunDirection: { value: new THREE.Vector3(0, 0, 0) }
|
|
},
|
|
vertexShader: `
|
|
varying vec3 vNormal;
|
|
varying vec2 vUv;
|
|
varying vec3 vPosition;
|
|
|
|
void main() {
|
|
vNormal = normalize(normalMatrix * normal);
|
|
vUv = uv;
|
|
vPosition = (modelMatrix * vec4(position, 1.0)).xyz;
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
}
|
|
`,
|
|
fragmentShader: `
|
|
uniform sampler2D dayTexture;
|
|
uniform sampler2D nightTexture;
|
|
uniform sampler2D normalMap;
|
|
uniform sampler2D specularMap;
|
|
uniform vec3 sunDirection;
|
|
|
|
varying vec3 vNormal;
|
|
varying vec2 vUv;
|
|
varying vec3 vPosition;
|
|
|
|
void main() {
|
|
// Calculate lighting
|
|
vec3 sunDir = normalize(sunDirection - vPosition);
|
|
float sunDot = dot(vNormal, sunDir);
|
|
|
|
// Day/night blend factor (smooth transition)
|
|
float blend = smoothstep(-0.1, 0.1, sunDot);
|
|
|
|
// Sample textures
|
|
vec4 dayColor = texture2D(dayTexture, vUv);
|
|
vec4 nightColor = texture2D(nightTexture, vUv);
|
|
vec4 normalColor = texture2D(normalMap, vUv);
|
|
vec4 specularColor = texture2D(specularMap, vUv);
|
|
|
|
// Blend day and night textures
|
|
vec4 finalColor = mix(nightColor, dayColor, blend);
|
|
|
|
// Apply basic lighting
|
|
float lightIntensity = max(sunDot, 0.0);
|
|
finalColor.rgb *= 0.3 + 0.7 * lightIntensity;
|
|
|
|
// Add specular highlights on oceans (water reflects more)
|
|
if (sunDot > 0.0) {
|
|
float specular = specularColor.r * pow(max(sunDot, 0.0), 5.0) * 0.5;
|
|
finalColor.rgb += vec3(specular);
|
|
}
|
|
|
|
gl_FragColor = finalColor;
|
|
}
|
|
`,
|
|
side: THREE.FrontSide
|
|
});
|
|
|
|
earth = new THREE.Mesh(earthGeometry, earthShaderMaterial);
|
|
earth.receiveShadow = true;
|
|
earth.castShadow = true;
|
|
earthRotationGroup.add(earth);
|
|
|
|
// Cloud layer - tutorial learning: separate transparent sphere slightly larger than Earth
|
|
const cloudGeometry = new THREE.SphereGeometry(5.15, 64, 64);
|
|
const cloudTexture = textureLoader.load('https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/planets/earth_clouds_2048.png');
|
|
|
|
// Tutorial learning: opacity 0.15 for realistic cloud transparency
|
|
const cloudMaterial = new THREE.MeshPhongMaterial({
|
|
map: cloudTexture,
|
|
transparent: true,
|
|
opacity: 0.15,
|
|
side: THREE.FrontSide,
|
|
depthWrite: false
|
|
});
|
|
|
|
earthClouds = new THREE.Mesh(cloudGeometry, cloudMaterial);
|
|
earthRotationGroup.add(earthClouds);
|
|
|
|
// Enhanced atmospheric glow with custom shader
|
|
const atmosphereGeometry = new THREE.SphereGeometry(5.4, 64, 64);
|
|
const atmosphereMaterial = new THREE.ShaderMaterial({
|
|
uniforms: {
|
|
glowColor: { value: new THREE.Color(0x6699ff) }
|
|
},
|
|
vertexShader: `
|
|
varying vec3 vNormal;
|
|
void main() {
|
|
vNormal = normalize(normalMatrix * normal);
|
|
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
|
}
|
|
`,
|
|
fragmentShader: `
|
|
uniform vec3 glowColor;
|
|
varying vec3 vNormal;
|
|
void main() {
|
|
float intensity = pow(0.6 - dot(vNormal, vec3(0.0, 0.0, 1.0)), 2.0);
|
|
gl_FragColor = vec4(glowColor, 1.0) * intensity;
|
|
}
|
|
`,
|
|
side: THREE.BackSide,
|
|
blending: THREE.AdditiveBlending,
|
|
transparent: true
|
|
});
|
|
|
|
const atmosphere = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial);
|
|
earth.add(atmosphere);
|
|
}
|
|
|
|
function addLensFlare(light) {
|
|
// Create lens flare effect from the Sun
|
|
const textureLoader = new THREE.TextureLoader();
|
|
|
|
// Create simple lens flare textures programmatically
|
|
const createLensFlareTexture = () => {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = 512;
|
|
canvas.height = 512;
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
const gradient = ctx.createRadialGradient(256, 256, 0, 256, 256, 256);
|
|
gradient.addColorStop(0, 'rgba(255,255,255,1)');
|
|
gradient.addColorStop(0.2, 'rgba(255,255,200,0.8)');
|
|
gradient.addColorStop(0.5, 'rgba(255,200,100,0.3)');
|
|
gradient.addColorStop(1, 'rgba(255,150,50,0)');
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.fillRect(0, 0, 512, 512);
|
|
|
|
const texture = new THREE.CanvasTexture(canvas);
|
|
return texture;
|
|
};
|
|
|
|
const lensflare = new Lensflare();
|
|
lensflare.addElement(new LensflareElement(createLensFlareTexture(), 700, 0, light.material.color));
|
|
lensflare.addElement(new LensflareElement(createLensFlareTexture(), 60, 0.6));
|
|
lensflare.addElement(new LensflareElement(createLensFlareTexture(), 70, 0.7));
|
|
lensflare.addElement(new LensflareElement(createLensFlareTexture(), 120, 0.9));
|
|
lensflare.addElement(new LensflareElement(createLensFlareTexture(), 70, 1.0));
|
|
|
|
light.add(lensflare);
|
|
}
|
|
|
|
function createEarthOrbit() {
|
|
const orbitPoints = [];
|
|
const segments = 360;
|
|
const a = ASTRONOMICAL_CONSTANTS.SEMI_MAJOR_AXIS / ASTRONOMICAL_CONSTANTS.SCALE_DISTANCE;
|
|
const e = ASTRONOMICAL_CONSTANTS.ECCENTRICITY;
|
|
|
|
for (let i = 0; i <= segments; 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);
|
|
orbitPoints.push(new THREE.Vector3(x, 0, z));
|
|
}
|
|
|
|
const orbitGeometry = new THREE.BufferGeometry().setFromPoints(orbitPoints);
|
|
const orbitMaterial = new THREE.LineBasicMaterial({
|
|
color: 0x00ff00,
|
|
opacity: 0.3,
|
|
transparent: true
|
|
});
|
|
earthOrbitLine = new THREE.Line(orbitGeometry, orbitMaterial);
|
|
scene.add(earthOrbitLine);
|
|
}
|
|
|
|
function createStarfield() {
|
|
// Tutorial learning: starfield as enormous sphere for background
|
|
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');
|
|
|
|
datePicker.addEventListener('change', (e) => {
|
|
simulationTime = new Date(e.target.value);
|
|
updateSimulation();
|
|
});
|
|
|
|
timeSpeedSlider.addEventListener('input', (e) => {
|
|
timeSpeed = parseFloat(e.target.value);
|
|
updateSpeedDisplay();
|
|
});
|
|
|
|
// Time control buttons
|
|
document.getElementById('btn-reverse').addEventListener('click', () => {
|
|
timeSpeed = -86400;
|
|
timeSpeedSlider.value = timeSpeed;
|
|
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;
|
|
timeSpeedSlider.value = timeSpeed;
|
|
updateSpeedDisplay();
|
|
});
|
|
|
|
document.getElementById('btn-reset').addEventListener('click', resetToNow);
|
|
|
|
// Camera preset buttons
|
|
document.getElementById('cam-default').addEventListener('click', () => {
|
|
followEarthMode = false;
|
|
setCameraPosition(0, 150, 250);
|
|
});
|
|
|
|
document.getElementById('cam-equinox').addEventListener('click', () => {
|
|
followEarthMode = false;
|
|
// Side view showing equal day/night
|
|
setCameraPosition(250, 0, 0);
|
|
});
|
|
|
|
document.getElementById('cam-solstice').addEventListener('click', () => {
|
|
followEarthMode = false;
|
|
// View showing maximum tilt - angled view
|
|
setCameraPosition(150, 150, 150);
|
|
});
|
|
|
|
document.getElementById('cam-polar').addEventListener('click', () => {
|
|
followEarthMode = false;
|
|
// Top-down view showing rotation
|
|
setCameraPosition(0, 300, 0);
|
|
});
|
|
|
|
document.getElementById('cam-follow').addEventListener('click', () => {
|
|
followEarthMode = true;
|
|
});
|
|
}
|
|
|
|
function setCameraPosition(x, y, z) {
|
|
camera.position.set(x, y, z);
|
|
controls.target.set(0, 0, 0);
|
|
controls.update();
|
|
}
|
|
|
|
function resetToNow() {
|
|
simulationTime = new Date();
|
|
timeSpeed = 0;
|
|
document.getElementById('time-speed').value = 0;
|
|
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`;
|
|
}
|
|
}
|
|
|
|
function calculateOrbitalPosition(julianDate) {
|
|
const d = julianDate - ASTRONOMICAL_CONSTANTS.J2000;
|
|
const M = ASTRONOMICAL_CONSTANTS.MEAN_LONGITUDE +
|
|
(360.0 / ASTRONOMICAL_CONSTANTS.SIDEREAL_YEAR) * d -
|
|
ASTRONOMICAL_CONSTANTS.PERIHELION;
|
|
|
|
// Solve Kepler's equation
|
|
let E = THREE.MathUtils.degToRad(M);
|
|
const e = ASTRONOMICAL_CONSTANTS.ECCENTRICITY;
|
|
for (let i = 0; i < 10; i++) {
|
|
E = E - (E - e * Math.sin(E) - THREE.MathUtils.degToRad(M)) / (1 - e * Math.cos(E));
|
|
}
|
|
|
|
const v = 2 * Math.atan2(
|
|
Math.sqrt(1 + e) * Math.sin(E / 2),
|
|
Math.sqrt(1 - e) * Math.cos(E / 2)
|
|
);
|
|
|
|
const r = ASTRONOMICAL_CONSTANTS.SEMI_MAJOR_AXIS * (1 - e * Math.cos(E));
|
|
const x = (r / ASTRONOMICAL_CONSTANTS.SCALE_DISTANCE) * Math.cos(v);
|
|
const z = (r / ASTRONOMICAL_CONSTANTS.SCALE_DISTANCE) * Math.sin(v);
|
|
|
|
return { x, z, r, v: THREE.MathUtils.radToDeg(v), d };
|
|
}
|
|
|
|
function updateSimulation() {
|
|
const jd = dateToJulianDate(simulationTime);
|
|
const orbital = calculateOrbitalPosition(jd);
|
|
|
|
earthTiltGroup.position.set(orbital.x, 0, orbital.z);
|
|
|
|
const daysSinceJ2000 = jd - ASTRONOMICAL_CONSTANTS.J2000;
|
|
const rotations = daysSinceJ2000 / ASTRONOMICAL_CONSTANTS.SIDEREAL_DAY;
|
|
earthRotationGroup.rotation.y = (rotations % 1) * Math.PI * 2;
|
|
|
|
// Update sun direction for shader
|
|
const sunDirection = new THREE.Vector3(0, 0, 0);
|
|
earth.material.uniforms.sunDirection.value = sunDirection;
|
|
|
|
const precessionAngle = (daysSinceJ2000 / (ASTRONOMICAL_CONSTANTS.PRECESSION_PERIOD * 365.25)) * 360;
|
|
|
|
updateUI(jd, orbital, daysSinceJ2000, rotations, precessionAngle);
|
|
|
|
const dateString = simulationTime.toISOString().slice(0, 16);
|
|
document.getElementById('date-picker').value = dateString;
|
|
}
|
|
|
|
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) + '°';
|
|
|
|
const velocity = Math.sqrt(1.327e20 * (2 / orbital.r - 1 / ASTRONOMICAL_CONSTANTS.SEMI_MAJOR_AXIS)) / 1000;
|
|
document.getElementById('orbital-velocity').textContent = velocity.toFixed(2) + ' km/s';
|
|
|
|
const season = getSeason(orbital.v);
|
|
document.getElementById('season').textContent = season;
|
|
}
|
|
|
|
function getSeason(orbitalAngle) {
|
|
const adjusted = (orbitalAngle + 12) % 360;
|
|
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;
|
|
lastFrameTime = currentTime;
|
|
|
|
if (timeSpeed !== 0) {
|
|
simulationTime = new Date(simulationTime.getTime() + (timeSpeed * deltaTime * 1000));
|
|
updateSimulation();
|
|
}
|
|
|
|
// Animate clouds - tutorial learning: independent rotation for cloud layer
|
|
// Clouds rotate slightly slower than Earth (0.0005 rad/frame from tutorial)
|
|
cloudRotation += 0.0003;
|
|
earthClouds.rotation.y = cloudRotation;
|
|
|
|
// Follow Earth camera mode
|
|
if (followEarthMode) {
|
|
const earthPos = earthTiltGroup.position;
|
|
const offset = new THREE.Vector3(30, 20, 30);
|
|
camera.position.copy(earthPos).add(offset);
|
|
controls.target.copy(earthPos);
|
|
}
|
|
|
|
controls.update();
|
|
renderer.render(scene, camera);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|