import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // Scene const scene = new THREE.Scene(); // Camera const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 25, 45); camera.lookAt(scene.position); // Renderer const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // Controls const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled controls.dampingFactor = 0.05; controls.screenSpacePanning = false; controls.minDistance = 10; controls.maxDistance = 500; // Lighting const pointLight = new THREE.PointLight(0xffffff, 5, 3000); // Increased intensity and distance scene.add(pointLight); const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); // Increased ambient light scene.add(ambientLight); // Texture Loader const textureLoader = new THREE.TextureLoader(); // Stars const starVertices = []; for (let i = 0; i < 10000; i++) { const x = (Math.random() - 0.5) * 2000; const y = (Math.random() - 0.5) * 2000; const z = (Math.random() - 0.5) * 2000; starVertices.push(x, y, z); } const starGeometry = new THREE.BufferGeometry(); starGeometry.setAttribute('position', new THREE.Float32BufferAttribute(starVertices, 3)); const starMaterial = new THREE.PointsMaterial({ color: 0xffffff, size: 0.7 }); const stars = new THREE.Points(starGeometry, starMaterial); scene.add(stars); // --- Sun --- const sunTexture = textureLoader.load('assets/textures/sun.jpg'); const sunMaterial = new THREE.MeshBasicMaterial({ map: sunTexture }); const sun = new THREE.Mesh(new THREE.SphereGeometry(7, 32, 32), sunMaterial); scene.add(sun); // --- Planets & Orbits --- const segments = 128; const orbitMaterial = new THREE.LineBasicMaterial({ color: 0xffffff, opacity: 0.5, transparent: true }); function createPlanet(radius, textureFile, distance, orbitalPeriodDays) { const texture = textureLoader.load(`assets/textures/${textureFile}?v=${Date.now()}`); const geometry = new THREE.SphereGeometry(radius, 32, 32); const material = new THREE.MeshBasicMaterial({ map: texture }); // Reverted const planet = new THREE.Mesh(geometry, material); const pivot = new THREE.Object3D(); sun.add(pivot); pivot.add(planet); planet.position.x = distance; // Trail setup const trailPoints = []; const trailGeometry = new THREE.BufferGeometry(); const trailMaterial = new THREE.LineBasicMaterial({ color: 0xffffff, opacity: 0.5, transparent: true }); const trail = new THREE.Line(trailGeometry, trailMaterial); scene.add(trail); return { planet, pivot, orbitalPeriodDays, trail, trailPoints }; } const mercuryData = createPlanet(0.4, 'mercury.jpg', 7, 88); const venusData = createPlanet(0.9, 'venus.jpg', 11, 225); const earthData = createPlanet(1, 'earth.jpg', 15, 365); const marsData = createPlanet(0.7, 'mars.jpg', 20, 687); const jupiterData = createPlanet(3, 'jupiter.jpg', 30, 4333); const saturnData = createPlanet(2.5, 'saturn.jpg', 40, 10759); const uranusData = createPlanet(2, 'uranus.jpg', 50, 30687); const neptuneData = createPlanet(1.9, 'neptune.jpg', 60, 60190); // --- Saturn's Rings --- const ringGeometry = new THREE.RingGeometry(3.5, 5, 64); const ringMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, side: THREE.DoubleSide, transparent: true, opacity: 0.6 }); const ring = new THREE.Mesh(ringGeometry, ringMaterial); ring.rotation.x = -Math.PI / 2.5; // Tilt the rings saturnData.planet.add(ring); // --- Moon --- const moonGeometry = new THREE.SphereGeometry(0.25, 32, 32); const moonMaterial = new THREE.MeshBasicMaterial({ color: 0x888888 }); const moon = new THREE.Mesh(moonGeometry, moonMaterial); const moonPivot = new THREE.Object3D(); earthData.planet.add(moonPivot); moonPivot.add(moon); moon.position.x = 2; // --- Asteroid Belt --- const asteroidCount = 1500; const asteroidGeometry = new THREE.SphereGeometry(0.05, 8, 8); // A small sphere for asteroids const asteroidMaterial = new THREE.MeshStandardMaterial({ color: 0x999999, roughness: 0.9 }); const asteroidMesh = new THREE.InstancedMesh(asteroidGeometry, asteroidMaterial, asteroidCount); const beltMinRadius = 23; const beltMaxRadius = 28; const beltHeight = 1.5; // Vertical thickness const dummy = new THREE.Object3D(); const asteroidData = []; for (let i = 0; i < asteroidCount; i++) { const radius = Math.random() * (beltMaxRadius - beltMinRadius) + beltMinRadius; const angle = Math.random() * Math.PI * 2; const y = (Math.random() - 0.5) * beltHeight; const orbitSpeed = (Math.random() * 0.002 + 0.0005); // Slower than Mars, faster than Jupiter const rotationSpeed = Math.random() * 0.05; asteroidData.push({ radius, angle, y, orbitSpeed, rotationSpeed }); dummy.position.set( Math.cos(angle) * radius, y, Math.sin(angle) * radius ); dummy.updateMatrix(); asteroidMesh.setMatrixAt(i, dummy.matrix); } scene.add(asteroidMesh); const planets = [mercuryData, venusData, earthData, marsData, jupiterData, saturnData, uranusData, neptuneData]; // Planet Data const planetData = { [earthData.planet.uuid]: { name: 'Earth', mass: '5.97 × 10^24 kg', radius: '6,371 km', orbital_speed: '29.78 km/s', }, [mercuryData.planet.uuid]: { name: 'Mercury', mass: '3.285 × 10^23 kg', radius: '2,439.7 km', orbital_speed: '47.36 km/s', }, [venusData.planet.uuid]: { name: 'Venus', mass: '4.867 × 10^24 kg', radius: '6,051.8 km', orbital_speed: '35.02 km/s', }, [marsData.planet.uuid]: { name: 'Mars', mass: '6.39 × 10^23 kg', radius: '3,389.5 km', orbital_speed: '24.07 km/s', }, [jupiterData.planet.uuid]: { name: 'Jupiter', mass: '1.898 × 10^27 kg', radius: '69,911 km', orbital_speed: '13.07 km/s', }, [saturnData.planet.uuid]: { name: 'Saturn', mass: '5.683 × 10^26 kg', radius: '58,232 km', orbital_speed: '9.69 km/s', }, [uranusData.planet.uuid]: { name: 'Uranus', mass: '8.681 × 10^25 kg', radius: '25,362 km', orbital_speed: '6.81 km/s', }, [neptuneData.planet.uuid]: { name: 'Neptune', mass: '1.024 × 10^26 kg', radius: '24,622 km', orbital_speed: '5.43 km/s', } }; // --- Selection Outline --- let outlineMesh; const outlineMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00, side: THREE.BackSide }); const outlineScale = 1.15; function setOutline(object) { if (outlineMesh && outlineMesh.parent) { outlineMesh.parent.remove(outlineMesh); } outlineMesh = new THREE.Mesh(object.geometry, outlineMaterial); outlineMesh.scale.set(outlineScale, outlineScale, outlineScale); object.add(outlineMesh); } // Stats Panel const statsInfo = document.getElementById('stats-info'); const statsButtons = document.getElementById('stats-buttons'); function updateStats(planetUUID) { const data = planetData[planetUUID]; if (data) { statsInfo.innerHTML = `

${data.name}

Mass: ${data.mass}

Radius: ${data.radius}

Orbital Speed: ${data.orbital_speed}

`; } } statsButtons.addEventListener('click', (event) => { if (event.target.tagName === 'BUTTON') { const planetName = event.target.dataset.planet; let planetObject, planetUUID; statsButtons.querySelectorAll('button').forEach(btn => btn.classList.remove('active')); event.target.classList.add('active'); if (planetName === 'earth') { planetObject = earthData.planet; planetUUID = earthData.planet.uuid; } else if (planetName === 'mercury') { planetObject = mercuryData.planet; planetUUID = mercuryData.planet.uuid; } else if (planetName === 'venus') { planetObject = venusData.planet; planetUUID = venusData.planet.uuid; } else if (planetName === 'mars') { planetObject = marsData.planet; planetUUID = marsData.planet.uuid; } else if (planetName === 'jupiter') { planetObject = jupiterData.planet; planetUUID = jupiterData.planet.uuid; } else if (planetName === 'saturn') { planetObject = saturnData.planet; planetUUID = saturnData.planet.uuid; } else if (planetName === 'uranus') { planetObject = uranusData.planet; planetUUID = uranusData.planet.uuid; } else if (planetName === 'neptune') { planetObject = neptuneData.planet; planetUUID = neptuneData.planet.uuid; } if (planetUUID) { updateStats(planetUUID); if (planetObject) { setOutline(planetObject); } } } }); // Display Earth's stats by default and set button to active updateStats(earthData.planet.uuid); statsButtons.querySelector('button[data-planet="earth"]').classList.add('active'); setOutline(earthData.planet); // Initial outline for Earth // --- Controls Logic --- let isPaused = false; const pauseButton = document.getElementById('pause-button'); const speedToggleButton = document.getElementById('speed-toggle-button'); pauseButton.addEventListener('click', () => { isPaused = !isPaused; pauseButton.textContent = isPaused ? 'Play' : 'Pause'; }); const speeds = [1, 100, 500, 1000, 10000]; let currentSpeedIndex = 0; let timeScale = speeds[currentSpeedIndex]; // Start with 1x speed speedToggleButton.addEventListener('click', () => { currentSpeedIndex = (currentSpeedIndex + 1) % speeds.length; timeScale = speeds[currentSpeedIndex]; speedToggleButton.textContent = 'Speed: ' + timeScale + 'x'; }); // --- Time & Simulation --- const clock = new THREE.Clock(); const timeContainer = document.getElementById('time-container'); let simulationTime = new Date(); const TIME_SPEED_FACTOR = 60; // 1 real second = 1 simulation minute at 1x const MS_IN_A_DAY = 24 * 60 * 60 * 1000; const MOON_ORBIT_DAYS = 27.3; function updateTimeDisplay() { const hours = String(simulationTime.getHours()).padStart(2, '0'); const minutes = String(simulationTime.getMinutes()).padStart(2, '0'); const seconds = String(simulationTime.getSeconds()).padStart(2, '0'); const day = String(simulationTime.getDate()).padStart(2, '0'); const month = String(simulationTime.getMonth() + 1).padStart(2, '0'); // Month is 0-indexed const year = simulationTime.getFullYear(); timeContainer.textContent = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } // Animation loop function animate() { requestAnimationFrame(animate); // Get time delta for frame-rate independent animation let delta = clock.getDelta(); // Clamp the delta to a maximum value to prevent large jumps, e.g., on the first frame or after tab refocus. delta = Math.min(delta, 0.1); // Update controls controls.update(); if (!isPaused) { // Advance simulation time using milliseconds for precision const incrementMs = delta * 1000 * timeScale * TIME_SPEED_FACTOR; simulationTime.setTime(simulationTime.getTime() + incrementMs); // Sun's self-rotation (can be slower and independent of timeScale) sun.rotation.y += 0.005 * delta; const maxTrailPoints = 200; const worldPosition = new THREE.Vector3(); planets.forEach(p => { const orbitalPeriodMs = p.orbitalPeriodDays * MS_IN_A_DAY; const angleIncrement = (incrementMs / orbitalPeriodMs) * (2 * Math.PI); p.pivot.rotation.y += angleIncrement; // Planet's self-rotation (can also be constant) p.planet.rotation.y += 0.05 * delta; // Update trail p.planet.getWorldPosition(worldPosition); p.trailPoints.push(worldPosition.clone()); if (p.trailPoints.length > maxTrailPoints) { p.trailPoints.shift(); } p.trail.geometry.setFromPoints(p.trailPoints); }); // Moon's orbit (also scaled by timeScale) const moonOrbitalPeriodMs = MOON_ORBIT_DAYS * MS_IN_A_DAY; const moonAngleIncrement = (incrementMs / moonOrbitalPeriodMs) * (2 * Math.PI); moonPivot.rotation.y += moonAngleIncrement; } // Update time display every frame updateTimeDisplay(); renderer.render(scene, camera); } animate(); // Handle window resize window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); // Initial call to set time updateTimeDisplay();