Spaces:
Running
Running
| // 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 += `<div class="stat"><span class="stat-label">${key}</span><span>${val}</span></div>`; | |
| }); | |
| } | |
| if (body.description) { | |
| html += `<p class="description">${body.description}</p>`; | |
| } | |
| 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(); | |
| })(); | |