// Solar System Explorer - 3D Interactive Visualization with Moons (function () { 'use strict'; // ============================ // PLANET & MOON DATA // ============================ const PLANETS = [ { name: 'Sun', radius: 696340, distance: 0, period: 0, color: 0xFDB813, emissive: 0xFDB813, rotationPeriod: '25.4 days (equator)', description: 'The Sun is the star at the center of our Solar System, containing 99.86% of its total mass. It is a nearly perfect sphere of hot plasma generating energy via nuclear fusion.', stats: { 'Type': 'G2V Main-sequence star', 'Diameter': '1,391,000 km', 'Mass': '1.989 × 10³⁰ kg', 'Surface Temp': '5,500°C', 'Core Temp': '~15 million°C', 'Rotation Period': '25.4 days (equator)', 'Age': '4.6 billion years', 'Composition': '73% Hydrogen, 25% Helium', 'Luminosity': '3.828 × 10²⁶ W' }, moons: [] }, { name: 'Mercury', radius: 2440, distance: 57.9, period: 0.24, color: 0xb5b5b5, emissive: 0x222222, rotationPeriod: '58.6 days', description: 'Mercury is the smallest planet and closest to the Sun. Despite its proximity, it is not the hottest planet because it lacks a substantial atmosphere to trap heat.', stats: { 'Distance from Sun': '57.9 million km', 'Orbital Period': '88 days', 'Rotation Period': '58.6 Earth days', 'Diameter': '4,879 km', 'Mass': '3.30 × 10²³ kg', 'Surface Gravity': '3.7 m/s²', 'Surface Temp': '-180°C to 430°C', 'Atmosphere': 'Virtually none', 'Moons': '0', 'Axial Tilt': '0.034°' }, moons: [] }, { name: 'Venus', radius: 6052, distance: 108.2, period: 0.62, color: 0xe8cda0, emissive: 0x332200, rotationPeriod: '243 days (retrograde)', description: 'Venus is the hottest planet with surface temperatures of 462°C, caused by a runaway greenhouse effect. It rotates backwards and a day on Venus is longer than its year.', stats: { 'Distance from Sun': '108.2 million km', 'Orbital Period': '225 days', 'Rotation Period': '243 days (retrograde)', 'Diameter': '12,104 km', 'Mass': '4.87 × 10²⁴ kg', 'Surface Gravity': '8.87 m/s²', 'Surface Temp': '462°C (average)', 'Atmosphere': '96.5% CO₂, 3.5% N₂', 'Pressure': '92× Earth\'s', 'Moons': '0', 'Axial Tilt': '177.4°' }, moons: [] }, { name: 'Earth', radius: 6371, distance: 149.6, period: 1.0, color: 0x4b9cd3, emissive: 0x112244, rotationPeriod: '23.93 hours', description: 'Earth is the only known planet to harbor life, with a dynamic atmosphere, liquid water oceans covering 71% of its surface, and a magnetic field that shields it from solar wind.', stats: { 'Distance from Sun': '149.6 million km', 'Orbital Period': '365.25 days', 'Rotation Period': '23 h 56 min', 'Diameter': '12,742 km', 'Mass': '5.97 × 10²⁴ kg', 'Surface Gravity': '9.81 m/s²', 'Surface Temp': '-89°C to 57°C', 'Atmosphere': '78% N₂, 21% O₂', 'Moons': '1', 'Axial Tilt': '23.44°', 'Magnetic Field': 'Yes (dipolar)' }, moons: [ { name: 'Moon', radius: 1737, distance: 0.384, period: 27.3, color: 0xaaaaaa, description: 'The Moon is Earth\'s only natural satellite. It stabilizes Earth\'s axial tilt, causes ocean tides, and is the only celestial body beyond Earth visited by humans.', stats: { 'Diameter': '3,474 km', 'Mass': '7.35 × 10²² kg', 'Orbital Period': '27.3 days', 'Rotation Period': '27.3 days (tidally locked)', 'Distance': '384,400 km', 'Surface Gravity': '1.62 m/s²', 'Escape Velocity': '2.38 km/s', 'Surface Temp': '-173°C to 127°C', 'Atmosphere': 'None (trace exosphere)', 'Composition': 'Silicate rock, iron core', 'Discovered': 'Known since antiquity', 'Notable': 'Tidally locked, human-visited' } } ] }, { name: 'Mars', radius: 3390, distance: 227.9, period: 1.88, color: 0xc1440e, emissive: 0x331100, rotationPeriod: '24.6 hours', description: 'Mars, the Red Planet, has the tallest volcano (Olympus Mons, 21.9 km) and the deepest canyon (Valles Marineris, 7 km deep) in the solar system. Evidence suggests it once had liquid water.', stats: { 'Distance from Sun': '227.9 million km', 'Orbital Period': '687 days', 'Rotation Period': '24 h 37 min', 'Diameter': '6,779 km', 'Mass': '6.42 × 10²³ kg', 'Surface Gravity': '3.72 m/s²', 'Surface Temp': '-153°C to 20°C', 'Atmosphere': '95% CO₂, 2.7% N₂', 'Moons': '2', 'Axial Tilt': '25.19°' }, moons: [ { name: 'Phobos', radius: 11.3, distance: 0.009, period: 0.32, color: 0x8a7d6b, description: 'Phobos is the larger of Mars\'s two moons. It orbits so close to Mars that it completes an orbit in just 7.6 hours — faster than Mars rotates. It will break apart in ~50 million years.', stats: { 'Dimensions': '27 × 22 × 18 km', 'Mass': '1.07 × 10¹⁶ kg', 'Orbital Period': '7.66 hours', 'Rotation Period': '7.66 hours (tidally locked)', 'Distance': '9,376 km', 'Surface Gravity': '0.0057 m/s²', 'Escape Velocity': '11.39 m/s', 'Composition': 'Carbon-rich rock (C-type)', 'Surface Temp': '-40°C to -4°C', 'Discovered': '1877 by Asaph Hall', 'Notable': 'Stickney crater (9 km), spiraling inward' } }, { name: 'Deimos', radius: 6.2, distance: 0.023, period: 1.26, color: 0x9e9483, description: 'Deimos is the smaller and outermost moon of Mars. Its smooth surface is covered in thick regolith that fills in most craters.', stats: { 'Dimensions': '15 × 12 × 11 km', 'Mass': '1.48 × 10¹⁵ kg', 'Orbital Period': '30.3 hours', 'Rotation Period': '30.3 hours (tidally locked)', 'Distance': '23,460 km', 'Surface Gravity': '0.003 m/s²', 'Escape Velocity': '5.56 m/s', 'Composition': 'Carbon-rich rock (C-type)', 'Surface Temp': '-40°C', 'Discovered': '1877 by Asaph Hall', 'Notable': 'Slowly drifting away from Mars' } } ] }, { name: 'Jupiter', radius: 69911, distance: 778.5, period: 11.86, color: 0xc88b3a, emissive: 0x221100, rotationPeriod: '9.93 hours', description: 'Jupiter is the largest planet, with a mass 2.5× that of all other planets combined. It has the fastest rotation and a powerful magnetic field 20,000× stronger than Earth\'s.', stats: { 'Distance from Sun': '778.5 million km', 'Orbital Period': '11.86 years', 'Rotation Period': '9 h 56 min', 'Diameter': '139,820 km', 'Mass': '1.90 × 10²⁷ kg', 'Surface Gravity': '24.79 m/s²', 'Cloud Temp': '-145°C', 'Atmosphere': '90% H₂, 10% He', 'Known Moons': '95', 'Great Red Spot': '~16,000 km wide', 'Axial Tilt': '3.13°' }, moons: [ { name: 'Io', radius: 1822, distance: 0.422, period: 1.77, color: 0xe5c94e, description: 'Io is the most volcanically active body in the solar system, with over 400 active volcanoes driven by intense tidal heating from Jupiter\'s gravity.', stats: { 'Diameter': '3,643 km', 'Mass': '8.93 × 10²² kg', 'Orbital Period': '1.77 days', 'Rotation Period': '1.77 days (tidally locked)', 'Distance': '421,700 km', 'Surface Gravity': '1.80 m/s²', 'Escape Velocity': '2.56 km/s', 'Volcanism': '400+ active volcanoes', 'Surface Temp': '-143°C (up to 1,700°C at vents)', 'Atmosphere': 'Thin SO₂', 'Discovered': '1610 by Galileo', 'Notable': 'Most geologically active body in solar system' } }, { name: 'Europa', radius: 1561, distance: 0.671, period: 3.55, color: 0xc8b89a, description: 'Europa has a smooth icy surface concealing a global subsurface ocean that may contain 2-3× more water than all of Earth\'s oceans. A prime target in the search for extraterrestrial life.', stats: { 'Diameter': '3,122 km', 'Mass': '4.80 × 10²² kg', 'Orbital Period': '3.55 days', 'Rotation Period': '3.55 days (tidally locked)', 'Distance': '671,034 km', 'Surface Gravity': '1.31 m/s²', 'Escape Velocity': '2.03 km/s', 'Ice Shell': '~15-25 km thick', 'Ocean Depth': '~60-150 km', 'Surface Temp': '-160°C', 'Discovered': '1610 by Galileo', 'Notable': 'Possible conditions for life' } }, { name: 'Ganymede', radius: 2634, distance: 1.07, period: 7.15, color: 0x8a8070, description: 'Ganymede is the largest moon in the solar system — larger than Mercury. It is the only moon known to generate its own magnetic field.', stats: { 'Diameter': '5,268 km', 'Mass': '1.48 × 10²³ kg', 'Orbital Period': '7.15 days', 'Rotation Period': '7.15 days (tidally locked)', 'Distance': '1.07 million km', 'Surface Gravity': '1.43 m/s²', 'Escape Velocity': '2.74 km/s', 'Magnetic Field': 'Yes (own magnetosphere)', 'Composition': 'Rock and ice (60/40)', 'Surface Temp': '-163°C', 'Discovered': '1610 by Galileo', 'Notable': 'Largest moon in solar system' } }, { name: 'Callisto', radius: 2410, distance: 1.88, period: 16.69, color: 0x6b6560, description: 'Callisto is the most heavily cratered body in the solar system. Its surface is ~4 billion years old with no signs of geological activity.', stats: { 'Diameter': '4,821 km', 'Mass': '1.08 × 10²³ kg', 'Orbital Period': '16.69 days', 'Rotation Period': '16.69 days (tidally locked)', 'Distance': '1.88 million km', 'Surface Gravity': '1.24 m/s²', 'Escape Velocity': '2.44 km/s', 'Surface Temp': '-139°C', 'Composition': 'Ice and rock (50/50)', 'Discovered': '1610 by Galileo', 'Notable': 'Most cratered body, possible subsurface ocean' } } ] }, { name: 'Saturn', radius: 58232, distance: 1434, period: 29.46, color: 0xe4d191, emissive: 0x222200, hasRings: true, rotationPeriod: '10.7 hours', description: 'Saturn is the least dense planet — it could float in water. Its ring system spans up to 282,000 km but is only about 10 meters thick on average.', stats: { 'Distance from Sun': '1.43 billion km', 'Orbital Period': '29.46 years', 'Rotation Period': '10 h 42 min', 'Diameter': '116,460 km', 'Mass': '5.68 × 10²⁶ kg', 'Surface Gravity': '10.44 m/s²', 'Cloud Temp': '-178°C', 'Atmosphere': '96% H₂, 3% He', 'Known Moons': '146', 'Ring Span': '~282,000 km', 'Density': '0.687 g/cm³ (less than water)', 'Axial Tilt': '26.73°' }, moons: [ { name: 'Titan', radius: 2575, distance: 1.222, period: 15.95, color: 0xd4a54a, description: 'Titan is the only moon with a dense atmosphere and the only body besides Earth with stable surface liquid (methane/ethane lakes and seas). Its thick orange haze hides a complex landscape.', stats: { 'Diameter': '5,150 km', 'Mass': '1.35 × 10²³ kg', 'Orbital Period': '15.95 days', 'Rotation Period': '15.95 days (tidally locked)', 'Distance': '1.22 million km', 'Surface Gravity': '1.35 m/s²', 'Escape Velocity': '2.64 km/s', 'Atmosphere': '95% N₂, 5% CH₄ (1.5× Earth pressure)', 'Surface Temp': '-179°C', 'Surface Liquid': 'Methane/ethane lakes & seas', 'Discovered': '1655 by Huygens', 'Notable': 'Dense atmosphere, hydrocarbon cycle' } }, { name: 'Enceladus', radius: 252, distance: 0.238, period: 1.37, color: 0xffffff, description: 'Enceladus has geysers shooting water ice from a global subsurface ocean through cracks at its south pole. It is considered one of the most likely places to find extraterrestrial life.', stats: { 'Diameter': '504 km', 'Mass': '1.08 × 10²⁰ kg', 'Orbital Period': '1.37 days', 'Rotation Period': '1.37 days (tidally locked)', 'Distance': '238,000 km', 'Surface Gravity': '0.113 m/s²', 'Escape Velocity': '0.239 km/s', 'Surface Temp': '-198°C', 'Geysers': 'Water ice jets (south pole)', 'Ocean': 'Global subsurface (beneath ~30 km ice)', 'Discovered': '1789 by Herschel', 'Notable': 'Hydrothermal vents possible, astrobiology target' } }, { name: 'Mimas', radius: 198, distance: 0.186, period: 0.94, color: 0xbbbbbb, description: 'Mimas has a giant impact crater (Herschel) spanning 1/3 of its diameter, making it resemble the Death Star. The impact nearly destroyed it.', stats: { 'Diameter': '396 km', 'Mass': '3.75 × 10¹⁹ kg', 'Orbital Period': '22.6 hours', 'Rotation Period': '22.6 hours (tidally locked)', 'Distance': '185,520 km', 'Surface Gravity': '0.064 m/s²', 'Escape Velocity': '0.159 km/s', 'Surface Temp': '-209°C', 'Composition': 'Mostly water ice', 'Discovered': '1789 by Herschel', 'Notable': 'Herschel crater (130 km wide, 10 km deep)' } }, { name: 'Rhea', radius: 764, distance: 0.527, period: 4.52, color: 0xcccccc, description: 'Rhea is Saturn\'s second-largest moon, a heavily cratered ice world. There is unconfirmed evidence it may have a thin ring system of its own.', stats: { 'Diameter': '1,528 km', 'Mass': '2.31 × 10²¹ kg', 'Orbital Period': '4.52 days', 'Rotation Period': '4.52 days (tidally locked)', 'Distance': '527,000 km', 'Surface Gravity': '0.264 m/s²', 'Escape Velocity': '0.635 km/s', 'Surface Temp': '-174°C to -220°C', 'Composition': '75% water ice, 25% rock', 'Discovered': '1672 by Cassini', 'Notable': 'May have own thin ring system' } }, { name: 'Iapetus', radius: 735, distance: 3.56, period: 79.3, color: 0x665544, description: 'Iapetus has a dramatic two-tone appearance — one hemisphere is as dark as coal, the other as bright as snow. It also has a massive equatorial ridge up to 20 km high.', stats: { 'Diameter': '1,470 km', 'Mass': '1.81 × 10²¹ kg', 'Orbital Period': '79.3 days', 'Rotation Period': '79.3 days (tidally locked)', 'Distance': '3.56 million km', 'Surface Gravity': '0.223 m/s²', 'Escape Velocity': '0.573 km/s', 'Surface Temp': '-143°C to -173°C', 'Composition': 'Water ice and dark material', 'Discovered': '1671 by Cassini', 'Notable': 'Two-tone coloring, equatorial ridge (20 km high)' } } ] }, { name: 'Uranus', radius: 25362, distance: 2871, period: 84.01, color: 0x7de7e7, emissive: 0x113333, rotationPeriod: '17.2 hours (retrograde)', description: 'Uranus rotates on its side with an axial tilt of 98°, likely from a massive ancient collision. It is classified as an ice giant due to its composition of water, methane, and ammonia ices.', stats: { 'Distance from Sun': '2.87 billion km', 'Orbital Period': '84 years', 'Rotation Period': '17 h 14 min (retrograde)', 'Diameter': '50,724 km', 'Mass': '8.68 × 10²⁵ kg', 'Surface Gravity': '8.87 m/s²', 'Cloud Temp': '-224°C', 'Atmosphere': '83% H₂, 15% He, 2% CH₄', 'Known Moons': '28', 'Axial Tilt': '97.77°', 'Rings': '13 known rings' }, moons: [ { name: 'Titania', radius: 789, distance: 0.436, period: 8.71, color: 0xbbaa99, description: 'Titania is the largest moon of Uranus. Its surface shows a mix of impact craters and extensive fault systems (grabens) indicating past geological activity.', stats: { 'Diameter': '1,578 km', 'Mass': '3.53 × 10²¹ kg', 'Orbital Period': '8.71 days', 'Rotation Period': '8.71 days (tidally locked)', 'Distance': '436,000 km', 'Surface Gravity': '0.379 m/s²', 'Escape Velocity': '0.773 km/s', 'Surface Temp': '-203°C', 'Composition': '50% ice, 30% rock, 20% organics', 'Discovered': '1787 by Herschel', 'Notable': 'Largest Uranian moon, canyons up to 1,500 km long' } }, { name: 'Oberon', radius: 761, distance: 0.584, period: 13.46, color: 0x998877, description: 'Oberon is the outermost major moon of Uranus, heavily cratered with dark material on crater floors. A mountain ~11 km high was observed on its limb.', stats: { 'Diameter': '1,523 km', 'Mass': '3.01 × 10²¹ kg', 'Orbital Period': '13.46 days', 'Rotation Period': '13.46 days (tidally locked)', 'Distance': '584,000 km', 'Surface Gravity': '0.346 m/s²', 'Escape Velocity': '0.727 km/s', 'Surface Temp': '-198°C', 'Composition': '50% ice, 30% rock, 20% organics', 'Discovered': '1787 by Herschel', 'Notable': 'Mountain ~11 km high, dark crater floors' } }, { name: 'Miranda', radius: 236, distance: 0.129, period: 1.41, color: 0xaaaaaa, description: 'Miranda has the most extreme and varied terrain of any moon, including Verona Rupes — the tallest known cliff in the solar system at 20 km, where a dropped object would fall for 12 minutes.', stats: { 'Diameter': '472 km', 'Mass': '6.59 × 10¹⁹ kg', 'Orbital Period': '1.41 days', 'Rotation Period': '1.41 days (tidally locked)', 'Distance': '129,390 km', 'Surface Gravity': '0.079 m/s²', 'Escape Velocity': '0.193 km/s', 'Surface Temp': '-187°C', 'Composition': 'Water ice and rock', 'Discovered': '1948 by Kuiper', 'Notable': 'Verona Rupes (20 km cliff), coronae features' } }, { name: 'Ariel', radius: 579, distance: 0.191, period: 2.52, color: 0xcccccc, description: 'Ariel is the brightest and geologically youngest moon of Uranus, with extensive valleys and relatively few large craters suggesting resurfacing events.', stats: { 'Diameter': '1,158 km', 'Mass': '1.35 × 10²¹ kg', 'Orbital Period': '2.52 days', 'Rotation Period': '2.52 days (tidally locked)', 'Distance': '190,900 km', 'Surface Gravity': '0.269 m/s²', 'Escape Velocity': '0.559 km/s', 'Surface Temp': '-213°C', 'Composition': '50% ice, 30% rock, 20% organics', 'Discovered': '1851 by Lassell', 'Notable': 'Brightest Uranian moon, youngest surface' } } ] }, { name: 'Neptune', radius: 24622, distance: 4495, period: 164.8, color: 0x4b70dd, emissive: 0x111133, rotationPeriod: '16.1 hours', description: 'Neptune has the strongest sustained winds of any planet (up to 2,100 km/h). It radiates 2.6× more energy than it receives from the Sun, suggesting an internal heat source.', stats: { 'Distance from Sun': '4.5 billion km', 'Orbital Period': '164.8 years', 'Rotation Period': '16 h 6 min', 'Diameter': '49,244 km', 'Mass': '1.02 × 10²⁶ kg', 'Surface Gravity': '11.15 m/s²', 'Cloud Temp': '-214°C', 'Atmosphere': '80% H₂, 19% He, 1.5% CH₄', 'Known Moons': '16', 'Wind Speeds': 'Up to 2,100 km/h', 'Axial Tilt': '28.32°', 'Rings': '5 known rings' }, moons: [ { name: 'Triton', radius: 1353, distance: 0.355, period: 5.88, color: 0xcce0e8, description: 'Triton orbits Neptune retrograde, strongly suggesting it was captured from the Kuiper Belt. It has active nitrogen geysers and is slowly spiraling inward toward Neptune.', stats: { 'Diameter': '2,707 km', 'Mass': '2.14 × 10²² kg', 'Orbital Period': '5.88 days (retrograde)', 'Rotation Period': '5.88 days (tidally locked)', 'Distance': '354,759 km', 'Surface Gravity': '0.779 m/s²', 'Escape Velocity': '1.455 km/s', 'Surface Temp': '-235°C (coldest known surface)', 'Atmosphere': 'Thin N₂ (~0.00001 atm)', 'Geysers': 'Active nitrogen (8 km high)', 'Composition': 'Nitrogen ice, water ice, CO₂ ice', 'Discovered': '1846 by Lassell', 'Notable': 'Captured KBO, will break apart in ~3.6B years' } }, { name: 'Proteus', radius: 210, distance: 0.118, period: 1.12, color: 0x777777, description: 'Proteus is one of the darkest objects in the solar system, reflecting only 6% of light. It is irregularly shaped and the largest of Neptune\'s inner moons.', stats: { 'Diameter': '~420 km', 'Mass': '~5 × 10¹⁹ kg', 'Orbital Period': '1.12 days', 'Rotation Period': '1.12 days (tidally locked)', 'Distance': '117,647 km', 'Surface Gravity': '~0.07 m/s²', 'Shape': 'Irregular (box-like)', 'Albedo': '0.06 (extremely dark)', 'Discovered': '1989 by Voyager 2', 'Notable': 'Largest non-spherical moon in solar system' } }, { name: 'Nereid', radius: 170, distance: 5.51, period: 360.1, color: 0x999999, description: 'Nereid has one of the most eccentric orbits of any known moon, ranging from 1.4M to 9.7M km from Neptune. It may be a captured asteroid or Kuiper Belt object.', stats: { 'Diameter': '~340 km', 'Mass': '~3 × 10¹⁹ kg', 'Orbital Period': '360.1 days', 'Rotation Period': '~11.5 hours', 'Distance': '5.51 million km', 'Orbital Eccentricity': '0.7512 (very high)', 'Albedo': '0.155', 'Discovered': '1949 by Kuiper', 'Notable': 'Most eccentric orbit of any moon, possibly captured' } } ] } ]; // ============================ // SCALE FUNCTIONS // ============================ function niceDistance(distMKm) { if (distMKm === 0) return 0; return 10 + Math.pow(distMKm, 0.45) * 2.5; } function niceSize(radiusKm, isSun) { if (isSun) return 5; return Math.max(0.4, Math.pow(radiusKm / 6371, 0.5) * 1.2); } function realDistance(distMKm) { return distMKm / 10; } function realSize(radiusKm) { // 1 unit = 100,000 km. Sun=6.96, Jupiter=0.70, Earth=0.064 const raw = radiusKm / 100000; // Cap sun so it doesn't overwhelm at real-distance scale if (radiusKm > 100000) return Math.min(raw, 3.0); // Ensure tiny planets remain visible return Math.max(raw, 0.03); } // ============================ // THREE.JS SETUP // ============================ const scene = new THREE.Scene(); const cam = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.01, 100000); cam.position.set(0, 80, 120); const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.toneMapping = THREE.ACESFilmicToneMapping; document.body.insertBefore(renderer.domElement, document.getElementById('ui-overlay')); const controls = new THREE.OrbitControls(cam, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.08; controls.minDistance = 0.5; controls.maxDistance = 50000; controls.enablePan = true; controls.zoomSpeed = 1.2; controls.rotateSpeed = 0.6; window.addEventListener('resize', () => { cam.aspect = window.innerWidth / window.innerHeight; cam.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); // ============================ // STARFIELD // ============================ (function createStarfield() { const geo = new THREE.BufferGeometry(); const positions = []; for (let i = 0; i < 8000; i++) { const r = 5000 + Math.random() * 15000; const theta = Math.random() * Math.PI * 2; const phi = Math.acos(2 * Math.random() - 1); positions.push(r * Math.sin(phi) * Math.cos(theta), r * Math.sin(phi) * Math.sin(theta), r * Math.cos(phi)); } geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); scene.add(new THREE.Points(geo, new THREE.PointsMaterial({ color: 0xffffff, size: 1.5, sizeAttenuation: true }))); })(); // ============================ // LIGHTING // ============================ const ambientLight = new THREE.AmbientLight(0x333344, 0.4); scene.add(ambientLight); const sunLight = new THREE.PointLight(0xffffff, 2, 0, 1); sunLight.position.set(0, 0, 0); scene.add(sunLight); // ============================ // CREATE PLANET MESHES // ============================ const planetMeshes = []; const orbitLines = []; const labelDivs = []; function createGlowTexture() { const size = 256; const cvs = document.createElement('canvas'); cvs.width = size; cvs.height = size; const c = cvs.getContext('2d'); const g = c.createRadialGradient(size/2, size/2, 0, size/2, size/2, size/2); g.addColorStop(0, 'rgba(255,200,50,1)'); g.addColorStop(0.2, 'rgba(255,150,30,0.6)'); g.addColorStop(0.5, 'rgba(255,100,10,0.2)'); g.addColorStop(1, 'rgba(255,50,0,0)'); c.fillStyle = g; c.fillRect(0, 0, size, size); return new THREE.CanvasTexture(cvs); } PLANETS.forEach((data, index) => { const isSun = index === 0; const radius = niceSize(data.radius, isSun); const segments = isSun ? 64 : 32; const geo = new THREE.SphereGeometry(radius, segments, segments); let mat; if (isSun) { mat = new THREE.MeshBasicMaterial({ color: data.color }); } else { mat = new THREE.MeshStandardMaterial({ color: data.color, emissive: data.emissive, emissiveIntensity: 0.15, roughness: 0.8, metalness: 0.1 }); } const mesh = new THREE.Mesh(geo, mat); mesh.userData = { index, data, type: 'planet' }; scene.add(mesh); planetMeshes.push(mesh); if (isSun) { const spriteMat = new THREE.SpriteMaterial({ map: createGlowTexture(), color: 0xffaa33, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false }); const sprite = new THREE.Sprite(spriteMat); sprite.scale.set(radius * 6, radius * 6, 1); mesh.add(sprite); } if (data.hasRings) { const ringGeo = new THREE.RingGeometry(radius * 1.3, radius * 2.3, 64); const ringMat = new THREE.MeshBasicMaterial({ color: 0xc8b070, side: THREE.DoubleSide, transparent: true, opacity: 0.6 }); const ring = new THREE.Mesh(ringGeo, ringMat); ring.rotation.x = Math.PI / 2.2; mesh.add(ring); } // Orbit line if (data.distance > 0) { const orbitR = niceDistance(data.distance); const orbitGeo = new THREE.BufferGeometry(); const pts = []; for (let j = 0; j <= 128; j++) { const a = (j / 128) * Math.PI * 2; pts.push(Math.cos(a) * orbitR, 0, Math.sin(a) * orbitR); } orbitGeo.setAttribute('position', new THREE.Float32BufferAttribute(pts, 3)); const orbitLine = new THREE.Line(orbitGeo, new THREE.LineBasicMaterial({ color: 0x4499ff, transparent: true, opacity: 0.4 })); scene.add(orbitLine); orbitLines.push(orbitLine); } else { orbitLines.push(null); } // Label const label = document.createElement('div'); label.className = 'planet-label'; label.textContent = data.name; document.getElementById('ui-overlay').appendChild(label); labelDivs.push(label); }); // ============================ // MOON SYSTEM (created on-demand when planet is selected) // ============================ let activeMoons = []; // { mesh, orbitLine, label, data } let activeMoonParentIdx = null; function clearMoons() { activeMoons.forEach(m => { scene.remove(m.mesh); if (m.orbitLine) scene.remove(m.orbitLine); if (m.label && m.label.parentNode) m.label.parentNode.removeChild(m.label); }); activeMoons = []; activeMoonParentIdx = null; } function createMoonsForPlanet(planetIndex) { clearMoons(); const planetData = PLANETS[planetIndex]; if (!planetData.moons || planetData.moons.length === 0) return; activeMoonParentIdx = planetIndex; const parentMesh = planetMeshes[planetIndex]; const parentRadius = getPlanetRadius(planetData, planetIndex === 0) * parentMesh.scale.x; planetData.moons.forEach((moonData, mi) => { let moonOrbitR, moonRadius; if (useRealSize) { // Real proportional size: moon radius relative to parent moonRadius = realSize(moonData.radius); // Ensure tiny moons are still visible moonRadius = Math.max(moonRadius, 0.015); } else { // Nice view: exaggerated for visibility moonRadius = Math.max(0.15, parentRadius * 0.12 * Math.pow(moonData.radius / Math.max(planetData.radius * 0.05, 1), 0.25)); // Cap moon so it never exceeds 40% of planet size moonRadius = Math.min(moonRadius, parentRadius * 0.4); } // Orbit radius if (useRealDistance && moonData.distance) { // Real distance: moon distance in million km / 10 moonOrbitR = realDistance(moonData.distance); // Ensure minimum visible separation moonOrbitR = Math.max(moonOrbitR, parentRadius + moonRadius + 0.5); } else { moonOrbitR = parentRadius * (2.5 + mi * 1.8); } // Moon sphere const geo = new THREE.SphereGeometry(moonRadius, 16, 16); const mat = new THREE.MeshStandardMaterial({ color: moonData.color, roughness: 0.9, metalness: 0.05, emissive: 0x111111, emissiveIntensity: 0.1 }); const mesh = new THREE.Mesh(geo, mat); mesh.userData = { type: 'moon', moonIndex: mi, planetIndex, data: moonData }; scene.add(mesh); // Moon orbit line const orbitGeo = new THREE.BufferGeometry(); const pts = []; for (let j = 0; j <= 64; j++) { const a = (j / 64) * Math.PI * 2; pts.push(Math.cos(a) * moonOrbitR, 0, Math.sin(a) * moonOrbitR); } orbitGeo.setAttribute('position', new THREE.Float32BufferAttribute(pts, 3)); const orbitLine = new THREE.Line(orbitGeo, new THREE.LineBasicMaterial({ color: 0x4499ff, transparent: true, opacity: 0.5 })); scene.add(orbitLine); // Moon label const label = document.createElement('div'); label.className = 'planet-label'; label.textContent = moonData.name; label.style.fontSize = '10px'; label.style.color = 'rgba(255,255,255,0.7)'; document.getElementById('ui-overlay').appendChild(label); activeMoons.push({ mesh, orbitLine, label, data: moonData, orbitRadius: moonOrbitR, moonIndex: mi }); }); } function updateMoons() { if (activeMoonParentIdx === null) return; const parentMesh = planetMeshes[activeMoonParentIdx]; const parentPos = parentMesh.position; activeMoons.forEach((moon, i) => { const angle = (time / (moon.data.period * 8 + 5)) * Math.PI * 2; // Small inclination per moon for visual variety (±5°) const inclination = (i * 0.05 - 0.05); moon.mesh.position.set( parentPos.x + Math.cos(angle) * moon.orbitRadius, parentPos.y + Math.sin(angle) * moon.orbitRadius * inclination, parentPos.z + Math.sin(angle) * moon.orbitRadius ); moon.mesh.rotation.y += 0.01 * timeSpeed; // Update orbit line position (centered on parent) if (moon.orbitLine) { moon.orbitLine.position.copy(parentPos); moon.orbitLine.rotation.x = inclination; moon.orbitLine.visible = showOrbits; } }); } function updateMoonLabels() { const halfW = window.innerWidth / 2; const halfH = window.innerHeight / 2; activeMoons.forEach(moon => { if (!showLabels) { moon.label.style.display = 'none'; return; } const pos = moon.mesh.position.clone(); pos.y += 0.5; pos.project(cam); if (pos.z > 1) { moon.label.style.display = 'none'; return; } moon.label.style.display = 'block'; moon.label.style.left = ((pos.x * halfW) + halfW) + 'px'; moon.label.style.top = (-(pos.y * halfH) + halfH) + 'px'; }); } // ============================ // STATE // ============================ let timeSpeed = 1; let time = 0; let useRealDistance = false; let useRealSize = false; let showOrbits = true; let showLabels = true; let selectedPlanet = null; let animatingCamera = false; let cameraTarget = new THREE.Vector3(); let cameraDestination = new THREE.Vector3(); // ============================ // UPDATE PLANET POSITIONS // ============================ function getOrbitalRadius(data) { if (data.distance === 0) return 0; return useRealDistance ? realDistance(data.distance) : niceDistance(data.distance); } function getPlanetRadius(data, isSun) { if (useRealSize) return realSize(data.radius); return niceSize(data.radius, isSun); } function updatePlanets() { PLANETS.forEach((data, i) => { const mesh = planetMeshes[i]; const isSun = i === 0; const orbitalR = getOrbitalRadius(data); if (orbitalR > 0) { const angle = (time / (data.period * 200)) * Math.PI * 2; mesh.position.x = Math.cos(angle) * orbitalR; mesh.position.z = Math.sin(angle) * orbitalR; mesh.position.y = 0; } else { mesh.position.set(0, 0, 0); } const targetRadius = getPlanetRadius(data, isSun); const baseNiceRadius = niceSize(data.radius, isSun); mesh.scale.setScalar(targetRadius / baseNiceRadius); mesh.rotation.y += 0.002 * timeSpeed; const orbitLine = orbitLines[i]; if (orbitLine) { const positions = orbitLine.geometry.attributes.position.array; for (let j = 0; j <= 128; j++) { const a = (j / 128) * Math.PI * 2; positions[j * 3] = Math.cos(a) * orbitalR; positions[j * 3 + 1] = 0; positions[j * 3 + 2] = Math.sin(a) * orbitalR; } orbitLine.geometry.attributes.position.needsUpdate = true; orbitLine.visible = showOrbits; } }); } // ============================ // LABELS // ============================ function updateLabels() { const halfW = window.innerWidth / 2; const halfH = window.innerHeight / 2; PLANETS.forEach((data, i) => { const label = labelDivs[i]; if (!showLabels) { label.style.display = 'none'; return; } const mesh = planetMeshes[i]; const pos = mesh.position.clone(); pos.y += getPlanetRadius(data, i === 0) * mesh.scale.x + 0.8; pos.project(cam); if (pos.z > 1) { label.style.display = 'none'; return; } label.style.display = 'block'; label.style.left = ((pos.x * halfW) + halfW) + 'px'; label.style.top = (-(pos.y * halfH) + halfH) + 'px'; }); } // ============================ // CAMERA (follows selected planet continuously) // ============================ let followOffset = new THREE.Vector3(); // offset from planet to camera let followingMoon = null; // reference to moon object if following a moon function flyToBody(position, radius) { const offset = radius * 4 + 5; cameraTarget.copy(position); cameraDestination.set( position.x + offset * 0.5, position.y + offset * 0.6, position.z + offset ); followOffset.set(offset * 0.5, offset * 0.3, offset); followingMoon = null; animatingCamera = true; } function flyToMoon(moonObj) { const pos = moonObj.mesh.position.clone(); const offset = moonObj.orbitRadius * 0.4 + 2; cameraTarget.copy(pos); cameraDestination.set(pos.x + offset * 0.4, pos.y + offset * 0.5, pos.z + offset); followingMoon = moonObj; animatingCamera = true; } function animateCamera() { // Continuous following: keep the selected planet (or moon) centered if (selectedPlanet !== null && !animatingCamera) { let targetPos; if (followingMoon) { targetPos = followingMoon.mesh.position; } else { targetPos = planetMeshes[selectedPlanet].position; } // Move both controls.target and camera by the planet's movement delta const delta = targetPos.clone().sub(controls.target); controls.target.copy(targetPos); cam.position.add(delta); } // Fly-to animation (initial transition) if (animatingCamera) { let currentTargetPos; if (followingMoon) { currentTargetPos = followingMoon.mesh.position.clone(); } else if (selectedPlanet !== null) { currentTargetPos = planetMeshes[selectedPlanet].position.clone(); } else { currentTargetPos = cameraTarget; } const currentDest = currentTargetPos.clone().add(followOffset); cam.position.lerp(currentDest, 0.05); controls.target.lerp(currentTargetPos, 0.05); if (cam.position.distanceTo(currentDest) < 0.3) { animatingCamera = false; } } } // ============================ // RAYCASTER // ============================ const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); let pointerDownPos = { x: 0, y: 0 }; renderer.domElement.addEventListener('pointerdown', (e) => { pointerDownPos = { x: e.clientX, y: e.clientY }; }); renderer.domElement.addEventListener('pointerup', (e) => { const dx = e.clientX - pointerDownPos.x; const dy = e.clientY - pointerDownPos.y; if (Math.abs(dx) > 5 || Math.abs(dy) > 5) return; mouse.x = (e.clientX / window.innerWidth) * 2 - 1; mouse.y = -(e.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, cam); // Check moons first const moonMeshes = activeMoons.map(m => m.mesh); const moonHits = raycaster.intersectObjects(moonMeshes); if (moonHits.length > 0) { const hit = moonHits[0].object; const moonObj = activeMoons.find(m => m.mesh === hit); if (moonObj) { selectMoon(moonObj); return; } } // Then planets const planetHits = raycaster.intersectObjects(planetMeshes); if (planetHits.length > 0) { const hit = planetHits[0].object; selectPlanet(hit.userData.index); } }); // ============================ // SELECTION & INFO PANEL // ============================ function selectPlanet(index) { selectedPlanet = index; const data = PLANETS[index]; // Auto-enable real scale toggles if (!useRealDistance) { useRealDistance = true; document.getElementById('toggle-real-distance').checked = true; } if (!useRealSize) { useRealSize = true; document.getElementById('toggle-real-size').checked = true; } showInfo(data); const mesh = planetMeshes[index]; const radius = getPlanetRadius(data, index === 0) * mesh.scale.x; flyToBody(mesh.position, radius); createMoonsForPlanet(index); updateNavButtons(); } function selectMoon(moonObj) { showInfo(moonObj.data); const offset = moonObj.orbitRadius * 0.4 + 2; followOffset.set(offset * 0.4, offset * 0.5, offset); flyToMoon(moonObj); } function showInfo(body) { const panel = document.getElementById('info-panel'); document.getElementById('info-name').textContent = body.name; let html = ''; if (body.stats) { Object.entries(body.stats).forEach(([key, val]) => { html += `
${body.description}
`; } document.getElementById('info-body').innerHTML = html; panel.classList.remove('hidden'); } document.getElementById('close-info').addEventListener('click', () => { document.getElementById('info-panel').classList.add('hidden'); selectedPlanet = null; followingMoon = null; clearMoons(); updateNavButtons(); }); document.getElementById('btn-reset').addEventListener('click', () => { animatingCamera = false; selectedPlanet = null; followingMoon = null; cam.position.set(0, 80, 120); controls.target.set(0, 0, 0); controls.update(); document.getElementById('info-panel').classList.add('hidden'); clearMoons(); updateNavButtons(); }); // Speed const speedSlider = document.getElementById('speed-slider'); const speedValueEl = document.getElementById('speed-value'); const playPauseBtn = document.getElementById('btn-play-pause'); let isPaused = false; let savedSpeed = 1; speedSlider.addEventListener('input', () => { timeSpeed = parseFloat(speedSlider.value); speedValueEl.textContent = timeSpeed.toFixed(1) + 'x'; if (timeSpeed > 0) { isPaused = false; savedSpeed = timeSpeed; playPauseBtn.textContent = '⏸'; } }); playPauseBtn.addEventListener('click', () => { if (isPaused) { // Resume isPaused = false; timeSpeed = savedSpeed || 1; speedSlider.value = timeSpeed; speedValueEl.textContent = timeSpeed.toFixed(1) + 'x'; playPauseBtn.textContent = '⏸'; } else { // Pause isPaused = true; savedSpeed = timeSpeed || 1; timeSpeed = 0; speedSlider.value = 0; speedValueEl.textContent = '0.0x'; playPauseBtn.textContent = '▶'; } }); // Toggles document.getElementById('toggle-real-distance').addEventListener('change', (e) => { useRealDistance = e.target.checked; if (useRealDistance) { cam.position.set(0, 300, 500); controls.target.set(0, 0, 0); } else { cam.position.set(0, 80, 120); controls.target.set(0, 0, 0); } controls.update(); if (selectedPlanet !== null) createMoonsForPlanet(selectedPlanet); }); document.getElementById('toggle-real-size').addEventListener('change', (e) => { useRealSize = e.target.checked; if (selectedPlanet !== null) createMoonsForPlanet(selectedPlanet); }); document.getElementById('toggle-orbits').addEventListener('change', (e) => { showOrbits = e.target.checked; }); document.getElementById('toggle-labels').addEventListener('change', (e) => { showLabels = e.target.checked; }); // Full illumination toggle - removes shadows so planets are fully visible document.getElementById('toggle-fulllight').addEventListener('change', (e) => { if (e.target.checked) { ambientLight.intensity = 2.0; sunLight.intensity = 0.3; } else { ambientLight.intensity = 0.4; sunLight.intensity = 2.0; } }); // Planet nav buttons const navContainer = document.getElementById('planet-nav'); PLANETS.forEach((planet, i) => { const btn = document.createElement('button'); btn.textContent = planet.name; btn.dataset.index = i; btn.addEventListener('click', () => selectPlanet(i)); navContainer.appendChild(btn); }); function updateNavButtons() { navContainer.querySelectorAll('button').forEach((btn, i) => { btn.classList.toggle('active', i === selectedPlanet); }); } // Keyboard document.addEventListener('keydown', (e) => { switch (e.key) { case 'Escape': document.getElementById('info-panel').classList.add('hidden'); selectedPlanet = null; followingMoon = null; clearMoons(); updateNavButtons(); break; case 'ArrowRight': if (selectedPlanet === null) selectedPlanet = -1; selectPlanet((selectedPlanet + 1) % PLANETS.length); break; case 'ArrowLeft': if (selectedPlanet === null) selectedPlanet = 1; selectPlanet((selectedPlanet - 1 + PLANETS.length) % PLANETS.length); break; } }); // ============================ // ANIMATION LOOP // ============================ function animate() { requestAnimationFrame(animate); time += timeSpeed * 0.1; updatePlanets(); updateMoons(); animateCamera(); updateLabels(); updateMoonLabels(); controls.update(); renderer.render(scene, cam); } animate(); })();