import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // --- SCENE, CAMERA, RENDERER, CONTROLS --- const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 50000); camera.position.set(0, 25, 45); camera.lookAt(scene.position); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.05; controls.screenSpacePanning = false; controls.minDistance = 1; controls.maxDistance = 20000; // --- LIGHTING --- const pointLight = new THREE.PointLight(0xffffff, 5, 60000); scene.add(pointLight); const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight); // --- TEXTURES & ASSETS --- const textureLoader = new THREE.TextureLoader(); // --- STARS BACKGROUND --- const starVertices = []; for (let i = 0; i < 15000; i++) { const x = (Math.random() - 0.5) * 25000; const y = (Math.random() - 0.5) * 25000; const z = (Math.random() - 0.5) * 25000; 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: 1.5 }); const stars = new THREE.Points(starGeometry, starMaterial); scene.add(stars); // --- SCALE CONSTANTS --- const SCALES = { artistic: { sunSize: 5, planetScale: 1, orbitScale: 1, moonOrbitScale: 1, }, realistic: { sunSize: 0.5, planetScale: 0.01, orbitScale: 100, moonOrbitScale: 0.1, } }; let currentScale = 'artistic'; // --- CELESTIAL BODY DATA & CREATION --- const celestialBodies = []; // --- SUN --- const sunTexture = textureLoader.load('assets/textures/sun.jpg'); const sunMaterial = new THREE.MeshBasicMaterial({ map: sunTexture }); const sun = new THREE.Mesh(new THREE.SphereGeometry(1, 32, 32), sunMaterial); sun.name = 'Sun'; scene.add(sun); celestialBodies.push({ name: 'Sun', object: sun, data: { radius: 696340 } }); // Real radius in km function createPlanet(name, textureFile, data) { const texture = textureLoader.load(`assets/textures/${textureFile}?v=${Date.now()}`); const geometry = new THREE.SphereGeometry(data.radius, 32, 32); const material = new THREE.MeshBasicMaterial({ map: texture }); const planet = new THREE.Mesh(geometry, material); planet.name = name; const pivot = new THREE.Object3D(); sun.add(pivot); pivot.add(planet); const planetObj = { name, object: planet, pivot, data, }; celestialBodies.push(planetObj); return planetObj; } function createMoon(name, color, data, parentPlanetObject) { const geometry = new THREE.SphereGeometry(data.radius, 32, 32); const material = new THREE.MeshBasicMaterial({ color }); const moon = new THREE.Mesh(geometry, material); moon.name = name; const pivot = new THREE.Object3D(); parentPlanetObject.add(pivot); pivot.add(moon); const moonObj = { name, object: moon, pivot, data, parent: parentPlanetObject }; celestialBodies.push(moonObj); return moonObj; } function createRings(innerRadius, outerRadius, color, opacity, tilt) { const ringGeometry = new THREE.RingGeometry(innerRadius, outerRadius, 64); const ringMaterial = new THREE.MeshBasicMaterial({ color: color, side: THREE.DoubleSide, transparent: true, opacity: opacity }); const ring = new THREE.Mesh(ringGeometry, ringMaterial); ring.rotation.x = tilt; return ring; } const celestialData = { mercury: { radius: 0.4, distance: 7, period: 88 }, venus: { radius: 0.9, distance: 11, period: 225 }, earth: { radius: 1, distance: 15, period: 365 }, mars: { radius: 0.7, distance: 20, period: 687 }, jupiter: { radius: 3, distance: 30, period: 4333 }, saturn: { radius: 2.5, distance: 40, period: 10759 }, uranus: { radius: 2, distance: 50, period: 30687 }, neptune: { radius: 1.9, distance: 60, period: 60190 }, moon: { radius: 0.25, distance: 2, period: 27.3 }, io: { radius: 0.2, distance: 3.5, period: 1.769 }, europa: { radius: 0.2, distance: 4.0, period: 3.551 }, ganymede: { radius: 0.3, distance: 4.5, period: 7.155 }, callisto: { radius: 0.3, distance: 5.0, period: 16.689 }, titan: { radius: 0.4, distance: 6.0, period: 15.95 }, triton: { radius: 0.35, distance: 4.0, period: 5.88 }, }; // --- INSTANTIATE BODIES --- const mercury = createPlanet('Mercury', 'mercury.jpg', celestialData.mercury); const venus = createPlanet('Venus', 'venus.jpg', celestialData.venus); const earth = createPlanet('Earth', 'earth.jpg', celestialData.earth); const mars = createPlanet('Mars', 'mars.jpg', celestialData.mars); const jupiter = createPlanet('Jupiter', 'jupiter.jpg', celestialData.jupiter); const saturn = createPlanet('Saturn', 'saturn.jpg', celestialData.saturn); const uranus = createPlanet('Uranus', 'uranus.jpg', celestialData.uranus); const neptune = createPlanet('Neptune', 'neptune.jpg', celestialData.neptune); createMoon('Moon', 0x888888, celestialData.moon, earth.object); createMoon('Io', 0xffff00, celestialData.io, jupiter.object); createMoon('Europa', 0xffa500, celestialData.europa, jupiter.object); createMoon('Ganymede', 0x00ff00, celestialData.ganymede, jupiter.object); createMoon('Callisto', 0x0000ff, celestialData.callisto, jupiter.object); createMoon('Titan', 0xFFAC1C, celestialData.titan, saturn.object); createMoon('Triton', 0xADD8E6, celestialData.triton, neptune.object); // --- RINGS --- saturn.object.add(createRings(3.5, 5, 0xffffff, 0.6, -Math.PI / 2.5)); jupiter.object.add(createRings(3.5, 4.5, 0x8c7853, 0.4, -Math.PI / 2)); uranus.object.add(createRings(2.5, 3.5, 0xadd8e6, 0.3, -Math.PI / 2)); neptune.object.add(createRings(2.4, 3, 0xadd8e6, 0.2, -Math.PI / 2.5)); // --- UI & CONTROLS --- const scaleToggle = document.getElementById('scale-toggle'); const focusSelect = document.getElementById('focus-select'); const dateDisplay = document.getElementById('date-container'); const slowDownButton = document.getElementById('slow-down'); const pauseToggleButton = document.getElementById('pause-toggle'); const speedUpButton = document.getElementById('speed-up'); const speedDisplay = document.getElementById('speed-display'); // Populate Focus Dropdown celestialBodies.forEach(body => { const option = document.createElement('option'); option.value = body.name; option.textContent = body.name; focusSelect.appendChild(option); }); function focusOnObject(name) { const body = celestialBodies.find(b => b.name === name); if (!body) return; const target = new THREE.Vector3(); body.object.getWorldPosition(target); controls.target.copy(target); const size = body.object.geometry.parameters.radius * body.object.scale.x; const offset = size * 5; const cameraPosition = new THREE.Vector3(target.x + offset, target.y + offset / 2, target.z + offset); camera.position.copy(cameraPosition); controls.update(); } focusSelect.addEventListener('change', (event) => { focusOnObject(event.target.value); }); scaleToggle.addEventListener('change', (event) => { const scale = event.target.checked ? 'realistic' : 'artistic'; updateScale(scale); }); // --- TIME CONTROLS --- let simulationSpeed = 1; // Represents days per second let isPaused = false; let currentDate = new Date(); function updateSpeedDisplay() { speedDisplay.textContent = `Speed: ${simulationSpeed}x`; } pauseToggleButton.addEventListener('click', () => { isPaused = !isPaused; pauseToggleButton.textContent = isPaused ? 'Resume' : 'Pause'; }); slowDownButton.addEventListener('click', () => { simulationSpeed /= 2; updateSpeedDisplay(); }); speedUpButton.addEventListener('click', () => { simulationSpeed *= 2; updateSpeedDisplay(); }); // --- SCALE UPDATE LOGIC --- function updateScale(scaleType) { currentScale = scaleType; const scaleConfig = SCALES[scaleType]; sun.scale.setScalar(scaleConfig.sunSize); celestialBodies.forEach(body => { if (body.name === 'Sun') return; const { object, pivot, data } = body; object.scale.setScalar(scaleConfig.planetScale); if (pivot) { if (body.parent) { object.position.x = data.distance * scaleConfig.moonOrbitScale; } else { object.position.x = data.distance * scaleConfig.orbitScale; } } }); if (scaleType === 'realistic') { controls.maxDistance = 400000; controls.minDistance = 0.01; } else { controls.maxDistance = 1000; controls.minDistance = 1; } focusOnObject(focusSelect.value); } // Initial setup updateScale('artistic'); updateSpeedDisplay(); // --- LABELS --- const labels = celestialBodies.map(body => ({ object: body.object, element: document.getElementById(`label-${body.name.toLowerCase()}`), })).filter(label => label.element); // --- ANIMATION LOOP --- const clock = new THREE.Clock(); let simulationTime = 0; const ORBIT_SPEED_FACTOR = 0.1; // Slows down overall orbit speed function animate() { requestAnimationFrame(animate); const deltaTime = clock.getDelta(); if (!isPaused) { const timePassed = deltaTime * simulationSpeed; simulationTime += timePassed; // Advance current date by days currentDate.setDate(currentDate.getDate() + timePassed); } // Update Date Display dateDisplay.textContent = currentDate.toDateString(); sun.rotation.y += 0.0005 * simulationSpeed * deltaTime; celestialBodies.forEach(body => { if (!body.pivot || !body.data.period) return; const angle = (simulationTime * ORBIT_SPEED_FACTOR) / body.data.period * (2 * Math.PI); body.pivot.rotation.y = angle; body.object.rotation.y += 0.05 * deltaTime * simulationSpeed; }); // Update labels labels.forEach(labelData => { const { object, element } = labelData; if (!element) return; const vector = new THREE.Vector3(); object.getWorldPosition(vector); vector.project(camera); const x = (vector.x * 0.5 + 0.5) * window.innerWidth; const y = (vector.y * -0.5 + 0.5) * window.innerHeight; element.style.transform = `translate(-50%, -50%) translate(${x}px, ${y}px)`; }); controls.update(); 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); });