SolarSystem / app.js
AK51's picture
Upload 4 files
2337d23 verified
Raw
History Blame Contribute Delete
48.9 kB
// 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();
})();