AK51 commited on
Commit
2337d23
·
verified ·
1 Parent(s): b0cf478

Upload 4 files

Browse files
Files changed (4) hide show
  1. README.md +28 -10
  2. app.js +931 -0
  3. index.html +57 -19
  4. style.css +327 -28
README.md CHANGED
@@ -1,10 +1,28 @@
1
- ---
2
- title: SolarSystem
3
- emoji: 🏃
4
- colorFrom: red
5
- colorTo: purple
6
- sdk: static
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Solar System Explorer
3
+ emoji: 🪐
4
+ colorFrom: indigo
5
+ colorTo: blue
6
+ sdk: static
7
+ pinned: false
8
+ ---
9
+
10
+ # Solar System Explorer
11
+
12
+ An interactive solar system visualization built with vanilla HTML5 Canvas. Explore all 8 planets and the Sun with smooth animations, real orbital data, and detailed information panels.
13
+
14
+ ## Features
15
+
16
+ - Real-time orbital animation with adjustable speed
17
+ - Click or tap any planet to view details
18
+ - Drag to pan, scroll/pinch to zoom
19
+ - Quick navigation buttons for all planets
20
+ - Mobile-friendly with touch gestures
21
+ - Keyboard shortcuts (arrows to cycle planets, +/- to zoom, Esc to reset)
22
+
23
+ ## Deploy to Hugging Face
24
+
25
+ 1. Create a new Space on [huggingface.co/new-space](https://huggingface.co/new-space)
26
+ 2. Select **Static** as the SDK
27
+ 3. Upload `index.html`, `style.css`, and `app.js`
28
+ 4. Your space will be live immediately
app.js ADDED
@@ -0,0 +1,931 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Solar System Explorer - 3D Interactive Visualization with Moons
2
+ (function () {
3
+ 'use strict';
4
+
5
+ // ============================
6
+ // PLANET & MOON DATA
7
+ // ============================
8
+ const PLANETS = [
9
+ {
10
+ name: 'Sun', radius: 696340, distance: 0, period: 0,
11
+ color: 0xFDB813, emissive: 0xFDB813,
12
+ rotationPeriod: '25.4 days (equator)',
13
+ 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.',
14
+ stats: {
15
+ 'Type': 'G2V Main-sequence star',
16
+ 'Diameter': '1,391,000 km',
17
+ 'Mass': '1.989 × 10³⁰ kg',
18
+ 'Surface Temp': '5,500°C',
19
+ 'Core Temp': '~15 million°C',
20
+ 'Rotation Period': '25.4 days (equator)',
21
+ 'Age': '4.6 billion years',
22
+ 'Composition': '73% Hydrogen, 25% Helium',
23
+ 'Luminosity': '3.828 × 10²⁶ W'
24
+ },
25
+ moons: []
26
+ },
27
+ {
28
+ name: 'Mercury', radius: 2440, distance: 57.9, period: 0.24,
29
+ color: 0xb5b5b5, emissive: 0x222222,
30
+ rotationPeriod: '58.6 days',
31
+ 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.',
32
+ stats: {
33
+ 'Distance from Sun': '57.9 million km',
34
+ 'Orbital Period': '88 days',
35
+ 'Rotation Period': '58.6 Earth days',
36
+ 'Diameter': '4,879 km',
37
+ 'Mass': '3.30 × 10²³ kg',
38
+ 'Surface Gravity': '3.7 m/s²',
39
+ 'Surface Temp': '-180°C to 430°C',
40
+ 'Atmosphere': 'Virtually none',
41
+ 'Moons': '0',
42
+ 'Axial Tilt': '0.034°'
43
+ },
44
+ moons: []
45
+ },
46
+ {
47
+ name: 'Venus', radius: 6052, distance: 108.2, period: 0.62,
48
+ color: 0xe8cda0, emissive: 0x332200,
49
+ rotationPeriod: '243 days (retrograde)',
50
+ 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.',
51
+ stats: {
52
+ 'Distance from Sun': '108.2 million km',
53
+ 'Orbital Period': '225 days',
54
+ 'Rotation Period': '243 days (retrograde)',
55
+ 'Diameter': '12,104 km',
56
+ 'Mass': '4.87 × 10²⁴ kg',
57
+ 'Surface Gravity': '8.87 m/s²',
58
+ 'Surface Temp': '462°C (average)',
59
+ 'Atmosphere': '96.5% CO₂, 3.5% N₂',
60
+ 'Pressure': '92× Earth\'s',
61
+ 'Moons': '0',
62
+ 'Axial Tilt': '177.4°'
63
+ },
64
+ moons: []
65
+ },
66
+ {
67
+ name: 'Earth', radius: 6371, distance: 149.6, period: 1.0,
68
+ color: 0x4b9cd3, emissive: 0x112244,
69
+ rotationPeriod: '23.93 hours',
70
+ 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.',
71
+ stats: {
72
+ 'Distance from Sun': '149.6 million km',
73
+ 'Orbital Period': '365.25 days',
74
+ 'Rotation Period': '23 h 56 min',
75
+ 'Diameter': '12,742 km',
76
+ 'Mass': '5.97 × 10²⁴ kg',
77
+ 'Surface Gravity': '9.81 m/s²',
78
+ 'Surface Temp': '-89°C to 57°C',
79
+ 'Atmosphere': '78% N₂, 21% O₂',
80
+ 'Moons': '1',
81
+ 'Axial Tilt': '23.44°',
82
+ 'Magnetic Field': 'Yes (dipolar)'
83
+ },
84
+ moons: [
85
+ { name: 'Moon', radius: 1737, distance: 0.384, period: 27.3, color: 0xaaaaaa,
86
+ 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.',
87
+ 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' }
88
+ }
89
+ ]
90
+ },
91
+ {
92
+ name: 'Mars', radius: 3390, distance: 227.9, period: 1.88,
93
+ color: 0xc1440e, emissive: 0x331100,
94
+ rotationPeriod: '24.6 hours',
95
+ 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.',
96
+ stats: {
97
+ 'Distance from Sun': '227.9 million km',
98
+ 'Orbital Period': '687 days',
99
+ 'Rotation Period': '24 h 37 min',
100
+ 'Diameter': '6,779 km',
101
+ 'Mass': '6.42 × 10²³ kg',
102
+ 'Surface Gravity': '3.72 m/s²',
103
+ 'Surface Temp': '-153°C to 20°C',
104
+ 'Atmosphere': '95% CO₂, 2.7% N₂',
105
+ 'Moons': '2',
106
+ 'Axial Tilt': '25.19°'
107
+ },
108
+ moons: [
109
+ { name: 'Phobos', radius: 11.3, distance: 0.009, period: 0.32, color: 0x8a7d6b,
110
+ 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.',
111
+ 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' }
112
+ },
113
+ { name: 'Deimos', radius: 6.2, distance: 0.023, period: 1.26, color: 0x9e9483,
114
+ description: 'Deimos is the smaller and outermost moon of Mars. Its smooth surface is covered in thick regolith that fills in most craters.',
115
+ 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' }
116
+ }
117
+ ]
118
+ },
119
+ {
120
+ name: 'Jupiter', radius: 69911, distance: 778.5, period: 11.86,
121
+ color: 0xc88b3a, emissive: 0x221100,
122
+ rotationPeriod: '9.93 hours',
123
+ 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.',
124
+ stats: {
125
+ 'Distance from Sun': '778.5 million km',
126
+ 'Orbital Period': '11.86 years',
127
+ 'Rotation Period': '9 h 56 min',
128
+ 'Diameter': '139,820 km',
129
+ 'Mass': '1.90 × 10²⁷ kg',
130
+ 'Surface Gravity': '24.79 m/s²',
131
+ 'Cloud Temp': '-145°C',
132
+ 'Atmosphere': '90% H₂, 10% He',
133
+ 'Known Moons': '95',
134
+ 'Great Red Spot': '~16,000 km wide',
135
+ 'Axial Tilt': '3.13°'
136
+ },
137
+ moons: [
138
+ { name: 'Io', radius: 1822, distance: 0.422, period: 1.77, color: 0xe5c94e,
139
+ 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.',
140
+ 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' }
141
+ },
142
+ { name: 'Europa', radius: 1561, distance: 0.671, period: 3.55, color: 0xc8b89a,
143
+ 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.',
144
+ 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' }
145
+ },
146
+ { name: 'Ganymede', radius: 2634, distance: 1.07, period: 7.15, color: 0x8a8070,
147
+ 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.',
148
+ 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' }
149
+ },
150
+ { name: 'Callisto', radius: 2410, distance: 1.88, period: 16.69, color: 0x6b6560,
151
+ 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.',
152
+ 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' }
153
+ }
154
+ ]
155
+ },
156
+ {
157
+ name: 'Saturn', radius: 58232, distance: 1434, period: 29.46,
158
+ color: 0xe4d191, emissive: 0x222200, hasRings: true,
159
+ rotationPeriod: '10.7 hours',
160
+ 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.',
161
+ stats: {
162
+ 'Distance from Sun': '1.43 billion km',
163
+ 'Orbital Period': '29.46 years',
164
+ 'Rotation Period': '10 h 42 min',
165
+ 'Diameter': '116,460 km',
166
+ 'Mass': '5.68 × 10²⁶ kg',
167
+ 'Surface Gravity': '10.44 m/s²',
168
+ 'Cloud Temp': '-178°C',
169
+ 'Atmosphere': '96% H₂, 3% He',
170
+ 'Known Moons': '146',
171
+ 'Ring Span': '~282,000 km',
172
+ 'Density': '0.687 g/cm³ (less than water)',
173
+ 'Axial Tilt': '26.73°'
174
+ },
175
+ moons: [
176
+ { name: 'Titan', radius: 2575, distance: 1.222, period: 15.95, color: 0xd4a54a,
177
+ 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.',
178
+ 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' }
179
+ },
180
+ { name: 'Enceladus', radius: 252, distance: 0.238, period: 1.37, color: 0xffffff,
181
+ 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.',
182
+ 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' }
183
+ },
184
+ { name: 'Mimas', radius: 198, distance: 0.186, period: 0.94, color: 0xbbbbbb,
185
+ 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.',
186
+ 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)' }
187
+ },
188
+ { name: 'Rhea', radius: 764, distance: 0.527, period: 4.52, color: 0xcccccc,
189
+ 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.',
190
+ 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' }
191
+ },
192
+ { name: 'Iapetus', radius: 735, distance: 3.56, period: 79.3, color: 0x665544,
193
+ 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.',
194
+ 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)' }
195
+ }
196
+ ]
197
+ },
198
+ {
199
+ name: 'Uranus', radius: 25362, distance: 2871, period: 84.01,
200
+ color: 0x7de7e7, emissive: 0x113333,
201
+ rotationPeriod: '17.2 hours (retrograde)',
202
+ 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.',
203
+ stats: {
204
+ 'Distance from Sun': '2.87 billion km',
205
+ 'Orbital Period': '84 years',
206
+ 'Rotation Period': '17 h 14 min (retrograde)',
207
+ 'Diameter': '50,724 km',
208
+ 'Mass': '8.68 × 10²⁵ kg',
209
+ 'Surface Gravity': '8.87 m/s²',
210
+ 'Cloud Temp': '-224°C',
211
+ 'Atmosphere': '83% H₂, 15% He, 2% CH₄',
212
+ 'Known Moons': '28',
213
+ 'Axial Tilt': '97.77°',
214
+ 'Rings': '13 known rings'
215
+ },
216
+ moons: [
217
+ { name: 'Titania', radius: 789, distance: 0.436, period: 8.71, color: 0xbbaa99,
218
+ 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.',
219
+ 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' }
220
+ },
221
+ { name: 'Oberon', radius: 761, distance: 0.584, period: 13.46, color: 0x998877,
222
+ 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.',
223
+ 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' }
224
+ },
225
+ { name: 'Miranda', radius: 236, distance: 0.129, period: 1.41, color: 0xaaaaaa,
226
+ 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.',
227
+ 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' }
228
+ },
229
+ { name: 'Ariel', radius: 579, distance: 0.191, period: 2.52, color: 0xcccccc,
230
+ description: 'Ariel is the brightest and geologically youngest moon of Uranus, with extensive valleys and relatively few large craters suggesting resurfacing events.',
231
+ 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' }
232
+ }
233
+ ]
234
+ },
235
+ {
236
+ name: 'Neptune', radius: 24622, distance: 4495, period: 164.8,
237
+ color: 0x4b70dd, emissive: 0x111133,
238
+ rotationPeriod: '16.1 hours',
239
+ 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.',
240
+ stats: {
241
+ 'Distance from Sun': '4.5 billion km',
242
+ 'Orbital Period': '164.8 years',
243
+ 'Rotation Period': '16 h 6 min',
244
+ 'Diameter': '49,244 km',
245
+ 'Mass': '1.02 × 10²⁶ kg',
246
+ 'Surface Gravity': '11.15 m/s²',
247
+ 'Cloud Temp': '-214°C',
248
+ 'Atmosphere': '80% H₂, 19% He, 1.5% CH₄',
249
+ 'Known Moons': '16',
250
+ 'Wind Speeds': 'Up to 2,100 km/h',
251
+ 'Axial Tilt': '28.32°',
252
+ 'Rings': '5 known rings'
253
+ },
254
+ moons: [
255
+ { name: 'Triton', radius: 1353, distance: 0.355, period: 5.88, color: 0xcce0e8,
256
+ 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.',
257
+ 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' }
258
+ },
259
+ { name: 'Proteus', radius: 210, distance: 0.118, period: 1.12, color: 0x777777,
260
+ 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.',
261
+ 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' }
262
+ },
263
+ { name: 'Nereid', radius: 170, distance: 5.51, period: 360.1, color: 0x999999,
264
+ 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.',
265
+ 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' }
266
+ }
267
+ ]
268
+ }
269
+ ];
270
+
271
+ // ============================
272
+ // SCALE FUNCTIONS
273
+ // ============================
274
+ function niceDistance(distMKm) {
275
+ if (distMKm === 0) return 0;
276
+ return 10 + Math.pow(distMKm, 0.45) * 2.5;
277
+ }
278
+
279
+ function niceSize(radiusKm, isSun) {
280
+ if (isSun) return 5;
281
+ return Math.max(0.4, Math.pow(radiusKm / 6371, 0.5) * 1.2);
282
+ }
283
+
284
+ function realDistance(distMKm) {
285
+ return distMKm / 10;
286
+ }
287
+
288
+ function realSize(radiusKm) {
289
+ // 1 unit = 100,000 km. Sun=6.96, Jupiter=0.70, Earth=0.064
290
+ const raw = radiusKm / 100000;
291
+ // Cap sun so it doesn't overwhelm at real-distance scale
292
+ if (radiusKm > 100000) return Math.min(raw, 3.0);
293
+ // Ensure tiny planets remain visible
294
+ return Math.max(raw, 0.03);
295
+ }
296
+
297
+ // ============================
298
+ // THREE.JS SETUP
299
+ // ============================
300
+ const scene = new THREE.Scene();
301
+ const cam = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.01, 100000);
302
+ cam.position.set(0, 80, 120);
303
+
304
+ const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
305
+ renderer.setSize(window.innerWidth, window.innerHeight);
306
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
307
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
308
+ document.body.insertBefore(renderer.domElement, document.getElementById('ui-overlay'));
309
+
310
+ const controls = new THREE.OrbitControls(cam, renderer.domElement);
311
+ controls.enableDamping = true;
312
+ controls.dampingFactor = 0.08;
313
+ controls.minDistance = 0.5;
314
+ controls.maxDistance = 50000;
315
+ controls.enablePan = true;
316
+ controls.zoomSpeed = 1.2;
317
+ controls.rotateSpeed = 0.6;
318
+
319
+ window.addEventListener('resize', () => {
320
+ cam.aspect = window.innerWidth / window.innerHeight;
321
+ cam.updateProjectionMatrix();
322
+ renderer.setSize(window.innerWidth, window.innerHeight);
323
+ });
324
+
325
+ // ============================
326
+ // STARFIELD
327
+ // ============================
328
+ (function createStarfield() {
329
+ const geo = new THREE.BufferGeometry();
330
+ const positions = [];
331
+ for (let i = 0; i < 8000; i++) {
332
+ const r = 5000 + Math.random() * 15000;
333
+ const theta = Math.random() * Math.PI * 2;
334
+ const phi = Math.acos(2 * Math.random() - 1);
335
+ positions.push(r * Math.sin(phi) * Math.cos(theta), r * Math.sin(phi) * Math.sin(theta), r * Math.cos(phi));
336
+ }
337
+ geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
338
+ scene.add(new THREE.Points(geo, new THREE.PointsMaterial({ color: 0xffffff, size: 1.5, sizeAttenuation: true })));
339
+ })();
340
+
341
+ // ============================
342
+ // LIGHTING
343
+ // ============================
344
+ const ambientLight = new THREE.AmbientLight(0x333344, 0.4);
345
+ scene.add(ambientLight);
346
+ const sunLight = new THREE.PointLight(0xffffff, 2, 0, 1);
347
+ sunLight.position.set(0, 0, 0);
348
+ scene.add(sunLight);
349
+
350
+ // ============================
351
+ // CREATE PLANET MESHES
352
+ // ============================
353
+ const planetMeshes = [];
354
+ const orbitLines = [];
355
+ const labelDivs = [];
356
+
357
+ function createGlowTexture() {
358
+ const size = 256;
359
+ const cvs = document.createElement('canvas');
360
+ cvs.width = size; cvs.height = size;
361
+ const c = cvs.getContext('2d');
362
+ const g = c.createRadialGradient(size/2, size/2, 0, size/2, size/2, size/2);
363
+ g.addColorStop(0, 'rgba(255,200,50,1)');
364
+ g.addColorStop(0.2, 'rgba(255,150,30,0.6)');
365
+ g.addColorStop(0.5, 'rgba(255,100,10,0.2)');
366
+ g.addColorStop(1, 'rgba(255,50,0,0)');
367
+ c.fillStyle = g;
368
+ c.fillRect(0, 0, size, size);
369
+ return new THREE.CanvasTexture(cvs);
370
+ }
371
+
372
+ PLANETS.forEach((data, index) => {
373
+ const isSun = index === 0;
374
+ const radius = niceSize(data.radius, isSun);
375
+ const segments = isSun ? 64 : 32;
376
+
377
+ const geo = new THREE.SphereGeometry(radius, segments, segments);
378
+ let mat;
379
+ if (isSun) {
380
+ mat = new THREE.MeshBasicMaterial({ color: data.color });
381
+ } else {
382
+ mat = new THREE.MeshStandardMaterial({
383
+ color: data.color, emissive: data.emissive,
384
+ emissiveIntensity: 0.15, roughness: 0.8, metalness: 0.1
385
+ });
386
+ }
387
+ const mesh = new THREE.Mesh(geo, mat);
388
+ mesh.userData = { index, data, type: 'planet' };
389
+ scene.add(mesh);
390
+ planetMeshes.push(mesh);
391
+
392
+ if (isSun) {
393
+ const spriteMat = new THREE.SpriteMaterial({
394
+ map: createGlowTexture(), color: 0xffaa33,
395
+ transparent: true, blending: THREE.AdditiveBlending, depthWrite: false
396
+ });
397
+ const sprite = new THREE.Sprite(spriteMat);
398
+ sprite.scale.set(radius * 6, radius * 6, 1);
399
+ mesh.add(sprite);
400
+ }
401
+
402
+ if (data.hasRings) {
403
+ const ringGeo = new THREE.RingGeometry(radius * 1.3, radius * 2.3, 64);
404
+ const ringMat = new THREE.MeshBasicMaterial({ color: 0xc8b070, side: THREE.DoubleSide, transparent: true, opacity: 0.6 });
405
+ const ring = new THREE.Mesh(ringGeo, ringMat);
406
+ ring.rotation.x = Math.PI / 2.2;
407
+ mesh.add(ring);
408
+ }
409
+
410
+ // Orbit line
411
+ if (data.distance > 0) {
412
+ const orbitR = niceDistance(data.distance);
413
+ const orbitGeo = new THREE.BufferGeometry();
414
+ const pts = [];
415
+ for (let j = 0; j <= 128; j++) {
416
+ const a = (j / 128) * Math.PI * 2;
417
+ pts.push(Math.cos(a) * orbitR, 0, Math.sin(a) * orbitR);
418
+ }
419
+ orbitGeo.setAttribute('position', new THREE.Float32BufferAttribute(pts, 3));
420
+ const orbitLine = new THREE.Line(orbitGeo, new THREE.LineBasicMaterial({ color: 0x4499ff, transparent: true, opacity: 0.4 }));
421
+ scene.add(orbitLine);
422
+ orbitLines.push(orbitLine);
423
+ } else {
424
+ orbitLines.push(null);
425
+ }
426
+
427
+ // Label
428
+ const label = document.createElement('div');
429
+ label.className = 'planet-label';
430
+ label.textContent = data.name;
431
+ document.getElementById('ui-overlay').appendChild(label);
432
+ labelDivs.push(label);
433
+ });
434
+
435
+ // ============================
436
+ // MOON SYSTEM (created on-demand when planet is selected)
437
+ // ============================
438
+ let activeMoons = []; // { mesh, orbitLine, label, data }
439
+ let activeMoonParentIdx = null;
440
+
441
+ function clearMoons() {
442
+ activeMoons.forEach(m => {
443
+ scene.remove(m.mesh);
444
+ if (m.orbitLine) scene.remove(m.orbitLine);
445
+ if (m.label && m.label.parentNode) m.label.parentNode.removeChild(m.label);
446
+ });
447
+ activeMoons = [];
448
+ activeMoonParentIdx = null;
449
+ }
450
+
451
+ function createMoonsForPlanet(planetIndex) {
452
+ clearMoons();
453
+ const planetData = PLANETS[planetIndex];
454
+ if (!planetData.moons || planetData.moons.length === 0) return;
455
+
456
+ activeMoonParentIdx = planetIndex;
457
+ const parentMesh = planetMeshes[planetIndex];
458
+ const parentRadius = getPlanetRadius(planetData, planetIndex === 0) * parentMesh.scale.x;
459
+
460
+ planetData.moons.forEach((moonData, mi) => {
461
+ let moonOrbitR, moonRadius;
462
+
463
+ if (useRealSize) {
464
+ // Real proportional size: moon radius relative to parent
465
+ moonRadius = realSize(moonData.radius);
466
+ // Ensure tiny moons are still visible
467
+ moonRadius = Math.max(moonRadius, 0.015);
468
+ } else {
469
+ // Nice view: exaggerated for visibility
470
+ moonRadius = Math.max(0.15, parentRadius * 0.12 * Math.pow(moonData.radius / Math.max(planetData.radius * 0.05, 1), 0.25));
471
+ // Cap moon so it never exceeds 40% of planet size
472
+ moonRadius = Math.min(moonRadius, parentRadius * 0.4);
473
+ }
474
+
475
+ // Orbit radius
476
+ if (useRealDistance && moonData.distance) {
477
+ // Real distance: moon distance in million km / 10
478
+ moonOrbitR = realDistance(moonData.distance);
479
+ // Ensure minimum visible separation
480
+ moonOrbitR = Math.max(moonOrbitR, parentRadius + moonRadius + 0.5);
481
+ } else {
482
+ moonOrbitR = parentRadius * (2.5 + mi * 1.8);
483
+ }
484
+
485
+ // Moon sphere
486
+ const geo = new THREE.SphereGeometry(moonRadius, 16, 16);
487
+ const mat = new THREE.MeshStandardMaterial({
488
+ color: moonData.color, roughness: 0.9, metalness: 0.05, emissive: 0x111111, emissiveIntensity: 0.1
489
+ });
490
+ const mesh = new THREE.Mesh(geo, mat);
491
+ mesh.userData = { type: 'moon', moonIndex: mi, planetIndex, data: moonData };
492
+ scene.add(mesh);
493
+
494
+ // Moon orbit line
495
+ const orbitGeo = new THREE.BufferGeometry();
496
+ const pts = [];
497
+ for (let j = 0; j <= 64; j++) {
498
+ const a = (j / 64) * Math.PI * 2;
499
+ pts.push(Math.cos(a) * moonOrbitR, 0, Math.sin(a) * moonOrbitR);
500
+ }
501
+ orbitGeo.setAttribute('position', new THREE.Float32BufferAttribute(pts, 3));
502
+ const orbitLine = new THREE.Line(orbitGeo, new THREE.LineBasicMaterial({ color: 0x4499ff, transparent: true, opacity: 0.5 }));
503
+ scene.add(orbitLine);
504
+
505
+ // Moon label
506
+ const label = document.createElement('div');
507
+ label.className = 'planet-label';
508
+ label.textContent = moonData.name;
509
+ label.style.fontSize = '10px';
510
+ label.style.color = 'rgba(255,255,255,0.7)';
511
+ document.getElementById('ui-overlay').appendChild(label);
512
+
513
+ activeMoons.push({ mesh, orbitLine, label, data: moonData, orbitRadius: moonOrbitR, moonIndex: mi });
514
+ });
515
+ }
516
+
517
+ function updateMoons() {
518
+ if (activeMoonParentIdx === null) return;
519
+ const parentMesh = planetMeshes[activeMoonParentIdx];
520
+ const parentPos = parentMesh.position;
521
+
522
+ activeMoons.forEach((moon, i) => {
523
+ const angle = (time / (moon.data.period * 8 + 5)) * Math.PI * 2;
524
+ // Small inclination per moon for visual variety (±5°)
525
+ const inclination = (i * 0.05 - 0.05);
526
+ moon.mesh.position.set(
527
+ parentPos.x + Math.cos(angle) * moon.orbitRadius,
528
+ parentPos.y + Math.sin(angle) * moon.orbitRadius * inclination,
529
+ parentPos.z + Math.sin(angle) * moon.orbitRadius
530
+ );
531
+ moon.mesh.rotation.y += 0.01 * timeSpeed;
532
+
533
+ // Update orbit line position (centered on parent)
534
+ if (moon.orbitLine) {
535
+ moon.orbitLine.position.copy(parentPos);
536
+ moon.orbitLine.rotation.x = inclination;
537
+ moon.orbitLine.visible = showOrbits;
538
+ }
539
+ });
540
+ }
541
+
542
+ function updateMoonLabels() {
543
+ const halfW = window.innerWidth / 2;
544
+ const halfH = window.innerHeight / 2;
545
+
546
+ activeMoons.forEach(moon => {
547
+ if (!showLabels) { moon.label.style.display = 'none'; return; }
548
+ const pos = moon.mesh.position.clone();
549
+ pos.y += 0.5;
550
+ pos.project(cam);
551
+ if (pos.z > 1) { moon.label.style.display = 'none'; return; }
552
+ moon.label.style.display = 'block';
553
+ moon.label.style.left = ((pos.x * halfW) + halfW) + 'px';
554
+ moon.label.style.top = (-(pos.y * halfH) + halfH) + 'px';
555
+ });
556
+ }
557
+
558
+ // ============================
559
+ // STATE
560
+ // ============================
561
+ let timeSpeed = 1;
562
+ let time = 0;
563
+ let useRealDistance = false;
564
+ let useRealSize = false;
565
+ let showOrbits = true;
566
+ let showLabels = true;
567
+ let selectedPlanet = null;
568
+ let animatingCamera = false;
569
+ let cameraTarget = new THREE.Vector3();
570
+ let cameraDestination = new THREE.Vector3();
571
+
572
+ // ============================
573
+ // UPDATE PLANET POSITIONS
574
+ // ============================
575
+ function getOrbitalRadius(data) {
576
+ if (data.distance === 0) return 0;
577
+ return useRealDistance ? realDistance(data.distance) : niceDistance(data.distance);
578
+ }
579
+
580
+ function getPlanetRadius(data, isSun) {
581
+ if (useRealSize) return realSize(data.radius);
582
+ return niceSize(data.radius, isSun);
583
+ }
584
+
585
+ function updatePlanets() {
586
+ PLANETS.forEach((data, i) => {
587
+ const mesh = planetMeshes[i];
588
+ const isSun = i === 0;
589
+ const orbitalR = getOrbitalRadius(data);
590
+
591
+ if (orbitalR > 0) {
592
+ const angle = (time / (data.period * 200)) * Math.PI * 2;
593
+ mesh.position.x = Math.cos(angle) * orbitalR;
594
+ mesh.position.z = Math.sin(angle) * orbitalR;
595
+ mesh.position.y = 0;
596
+ } else {
597
+ mesh.position.set(0, 0, 0);
598
+ }
599
+
600
+ const targetRadius = getPlanetRadius(data, isSun);
601
+ const baseNiceRadius = niceSize(data.radius, isSun);
602
+ mesh.scale.setScalar(targetRadius / baseNiceRadius);
603
+
604
+ mesh.rotation.y += 0.002 * timeSpeed;
605
+
606
+ const orbitLine = orbitLines[i];
607
+ if (orbitLine) {
608
+ const positions = orbitLine.geometry.attributes.position.array;
609
+ for (let j = 0; j <= 128; j++) {
610
+ const a = (j / 128) * Math.PI * 2;
611
+ positions[j * 3] = Math.cos(a) * orbitalR;
612
+ positions[j * 3 + 1] = 0;
613
+ positions[j * 3 + 2] = Math.sin(a) * orbitalR;
614
+ }
615
+ orbitLine.geometry.attributes.position.needsUpdate = true;
616
+ orbitLine.visible = showOrbits;
617
+ }
618
+ });
619
+ }
620
+
621
+ // ============================
622
+ // LABELS
623
+ // ============================
624
+ function updateLabels() {
625
+ const halfW = window.innerWidth / 2;
626
+ const halfH = window.innerHeight / 2;
627
+ PLANETS.forEach((data, i) => {
628
+ const label = labelDivs[i];
629
+ if (!showLabels) { label.style.display = 'none'; return; }
630
+ const mesh = planetMeshes[i];
631
+ const pos = mesh.position.clone();
632
+ pos.y += getPlanetRadius(data, i === 0) * mesh.scale.x + 0.8;
633
+ pos.project(cam);
634
+ if (pos.z > 1) { label.style.display = 'none'; return; }
635
+ label.style.display = 'block';
636
+ label.style.left = ((pos.x * halfW) + halfW) + 'px';
637
+ label.style.top = (-(pos.y * halfH) + halfH) + 'px';
638
+ });
639
+ }
640
+
641
+ // ============================
642
+ // CAMERA (follows selected planet continuously)
643
+ // ============================
644
+ let followOffset = new THREE.Vector3(); // offset from planet to camera
645
+ let followingMoon = null; // reference to moon object if following a moon
646
+
647
+ function flyToBody(position, radius) {
648
+ const offset = radius * 4 + 5;
649
+ cameraTarget.copy(position);
650
+ cameraDestination.set(
651
+ position.x + offset * 0.5,
652
+ position.y + offset * 0.6,
653
+ position.z + offset
654
+ );
655
+ followOffset.set(offset * 0.5, offset * 0.3, offset);
656
+ followingMoon = null;
657
+ animatingCamera = true;
658
+ }
659
+
660
+ function flyToMoon(moonObj) {
661
+ const pos = moonObj.mesh.position.clone();
662
+ const offset = moonObj.orbitRadius * 0.4 + 2;
663
+ cameraTarget.copy(pos);
664
+ cameraDestination.set(pos.x + offset * 0.4, pos.y + offset * 0.5, pos.z + offset);
665
+ followingMoon = moonObj;
666
+ animatingCamera = true;
667
+ }
668
+
669
+ function animateCamera() {
670
+ // Continuous following: keep the selected planet (or moon) centered
671
+ if (selectedPlanet !== null && !animatingCamera) {
672
+ let targetPos;
673
+ if (followingMoon) {
674
+ targetPos = followingMoon.mesh.position;
675
+ } else {
676
+ targetPos = planetMeshes[selectedPlanet].position;
677
+ }
678
+ // Move both controls.target and camera by the planet's movement delta
679
+ const delta = targetPos.clone().sub(controls.target);
680
+ controls.target.copy(targetPos);
681
+ cam.position.add(delta);
682
+ }
683
+
684
+ // Fly-to animation (initial transition)
685
+ if (animatingCamera) {
686
+ let currentTargetPos;
687
+ if (followingMoon) {
688
+ currentTargetPos = followingMoon.mesh.position.clone();
689
+ } else if (selectedPlanet !== null) {
690
+ currentTargetPos = planetMeshes[selectedPlanet].position.clone();
691
+ } else {
692
+ currentTargetPos = cameraTarget;
693
+ }
694
+ const currentDest = currentTargetPos.clone().add(followOffset);
695
+
696
+ cam.position.lerp(currentDest, 0.05);
697
+ controls.target.lerp(currentTargetPos, 0.05);
698
+
699
+ if (cam.position.distanceTo(currentDest) < 0.3) {
700
+ animatingCamera = false;
701
+ }
702
+ }
703
+ }
704
+
705
+ // ============================
706
+ // RAYCASTER
707
+ // ============================
708
+ const raycaster = new THREE.Raycaster();
709
+ const mouse = new THREE.Vector2();
710
+ let pointerDownPos = { x: 0, y: 0 };
711
+
712
+ renderer.domElement.addEventListener('pointerdown', (e) => {
713
+ pointerDownPos = { x: e.clientX, y: e.clientY };
714
+ });
715
+
716
+ renderer.domElement.addEventListener('pointerup', (e) => {
717
+ const dx = e.clientX - pointerDownPos.x;
718
+ const dy = e.clientY - pointerDownPos.y;
719
+ if (Math.abs(dx) > 5 || Math.abs(dy) > 5) return;
720
+
721
+ mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
722
+ mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
723
+ raycaster.setFromCamera(mouse, cam);
724
+
725
+ // Check moons first
726
+ const moonMeshes = activeMoons.map(m => m.mesh);
727
+ const moonHits = raycaster.intersectObjects(moonMeshes);
728
+ if (moonHits.length > 0) {
729
+ const hit = moonHits[0].object;
730
+ const moonObj = activeMoons.find(m => m.mesh === hit);
731
+ if (moonObj) {
732
+ selectMoon(moonObj);
733
+ return;
734
+ }
735
+ }
736
+
737
+ // Then planets
738
+ const planetHits = raycaster.intersectObjects(planetMeshes);
739
+ if (planetHits.length > 0) {
740
+ const hit = planetHits[0].object;
741
+ selectPlanet(hit.userData.index);
742
+ }
743
+ });
744
+
745
+ // ============================
746
+ // SELECTION & INFO PANEL
747
+ // ============================
748
+ function selectPlanet(index) {
749
+ selectedPlanet = index;
750
+ const data = PLANETS[index];
751
+
752
+ // Auto-enable real scale toggles
753
+ if (!useRealDistance) {
754
+ useRealDistance = true;
755
+ document.getElementById('toggle-real-distance').checked = true;
756
+ }
757
+ if (!useRealSize) {
758
+ useRealSize = true;
759
+ document.getElementById('toggle-real-size').checked = true;
760
+ }
761
+
762
+ showInfo(data);
763
+ const mesh = planetMeshes[index];
764
+ const radius = getPlanetRadius(data, index === 0) * mesh.scale.x;
765
+ flyToBody(mesh.position, radius);
766
+ createMoonsForPlanet(index);
767
+ updateNavButtons();
768
+ }
769
+
770
+ function selectMoon(moonObj) {
771
+ showInfo(moonObj.data);
772
+ const offset = moonObj.orbitRadius * 0.4 + 2;
773
+ followOffset.set(offset * 0.4, offset * 0.5, offset);
774
+ flyToMoon(moonObj);
775
+ }
776
+
777
+ function showInfo(body) {
778
+ const panel = document.getElementById('info-panel');
779
+ document.getElementById('info-name').textContent = body.name;
780
+ let html = '';
781
+ if (body.stats) {
782
+ Object.entries(body.stats).forEach(([key, val]) => {
783
+ html += `<div class="stat"><span class="stat-label">${key}</span><span>${val}</span></div>`;
784
+ });
785
+ }
786
+ if (body.description) {
787
+ html += `<p class="description">${body.description}</p>`;
788
+ }
789
+ document.getElementById('info-body').innerHTML = html;
790
+ panel.classList.remove('hidden');
791
+ }
792
+
793
+ document.getElementById('close-info').addEventListener('click', () => {
794
+ document.getElementById('info-panel').classList.add('hidden');
795
+ selectedPlanet = null;
796
+ followingMoon = null;
797
+ clearMoons();
798
+ updateNavButtons();
799
+ });
800
+
801
+ document.getElementById('btn-reset').addEventListener('click', () => {
802
+ animatingCamera = false;
803
+ selectedPlanet = null;
804
+ followingMoon = null;
805
+ cam.position.set(0, 80, 120);
806
+ controls.target.set(0, 0, 0);
807
+ controls.update();
808
+ document.getElementById('info-panel').classList.add('hidden');
809
+ clearMoons();
810
+ updateNavButtons();
811
+ });
812
+
813
+ // Speed
814
+ const speedSlider = document.getElementById('speed-slider');
815
+ const speedValueEl = document.getElementById('speed-value');
816
+ const playPauseBtn = document.getElementById('btn-play-pause');
817
+ let isPaused = false;
818
+ let savedSpeed = 1;
819
+
820
+ speedSlider.addEventListener('input', () => {
821
+ timeSpeed = parseFloat(speedSlider.value);
822
+ speedValueEl.textContent = timeSpeed.toFixed(1) + 'x';
823
+ if (timeSpeed > 0) {
824
+ isPaused = false;
825
+ savedSpeed = timeSpeed;
826
+ playPauseBtn.textContent = '⏸';
827
+ }
828
+ });
829
+
830
+ playPauseBtn.addEventListener('click', () => {
831
+ if (isPaused) {
832
+ // Resume
833
+ isPaused = false;
834
+ timeSpeed = savedSpeed || 1;
835
+ speedSlider.value = timeSpeed;
836
+ speedValueEl.textContent = timeSpeed.toFixed(1) + 'x';
837
+ playPauseBtn.textContent = '⏸';
838
+ } else {
839
+ // Pause
840
+ isPaused = true;
841
+ savedSpeed = timeSpeed || 1;
842
+ timeSpeed = 0;
843
+ speedSlider.value = 0;
844
+ speedValueEl.textContent = '0.0x';
845
+ playPauseBtn.textContent = '▶';
846
+ }
847
+ });
848
+
849
+ // Toggles
850
+ document.getElementById('toggle-real-distance').addEventListener('change', (e) => {
851
+ useRealDistance = e.target.checked;
852
+ if (useRealDistance) { cam.position.set(0, 300, 500); controls.target.set(0, 0, 0); }
853
+ else { cam.position.set(0, 80, 120); controls.target.set(0, 0, 0); }
854
+ controls.update();
855
+ if (selectedPlanet !== null) createMoonsForPlanet(selectedPlanet);
856
+ });
857
+
858
+ document.getElementById('toggle-real-size').addEventListener('change', (e) => {
859
+ useRealSize = e.target.checked;
860
+ if (selectedPlanet !== null) createMoonsForPlanet(selectedPlanet);
861
+ });
862
+
863
+ document.getElementById('toggle-orbits').addEventListener('change', (e) => { showOrbits = e.target.checked; });
864
+ document.getElementById('toggle-labels').addEventListener('change', (e) => { showLabels = e.target.checked; });
865
+
866
+ // Full illumination toggle - removes shadows so planets are fully visible
867
+ document.getElementById('toggle-fulllight').addEventListener('change', (e) => {
868
+ if (e.target.checked) {
869
+ ambientLight.intensity = 2.0;
870
+ sunLight.intensity = 0.3;
871
+ } else {
872
+ ambientLight.intensity = 0.4;
873
+ sunLight.intensity = 2.0;
874
+ }
875
+ });
876
+
877
+ // Planet nav buttons
878
+ const navContainer = document.getElementById('planet-nav');
879
+ PLANETS.forEach((planet, i) => {
880
+ const btn = document.createElement('button');
881
+ btn.textContent = planet.name;
882
+ btn.dataset.index = i;
883
+ btn.addEventListener('click', () => selectPlanet(i));
884
+ navContainer.appendChild(btn);
885
+ });
886
+
887
+ function updateNavButtons() {
888
+ navContainer.querySelectorAll('button').forEach((btn, i) => {
889
+ btn.classList.toggle('active', i === selectedPlanet);
890
+ });
891
+ }
892
+
893
+ // Keyboard
894
+ document.addEventListener('keydown', (e) => {
895
+ switch (e.key) {
896
+ case 'Escape':
897
+ document.getElementById('info-panel').classList.add('hidden');
898
+ selectedPlanet = null;
899
+ followingMoon = null;
900
+ clearMoons();
901
+ updateNavButtons();
902
+ break;
903
+ case 'ArrowRight':
904
+ if (selectedPlanet === null) selectedPlanet = -1;
905
+ selectPlanet((selectedPlanet + 1) % PLANETS.length);
906
+ break;
907
+ case 'ArrowLeft':
908
+ if (selectedPlanet === null) selectedPlanet = 1;
909
+ selectPlanet((selectedPlanet - 1 + PLANETS.length) % PLANETS.length);
910
+ break;
911
+ }
912
+ });
913
+
914
+ // ============================
915
+ // ANIMATION LOOP
916
+ // ============================
917
+ function animate() {
918
+ requestAnimationFrame(animate);
919
+ time += timeSpeed * 0.1;
920
+ updatePlanets();
921
+ updateMoons();
922
+ animateCamera();
923
+ updateLabels();
924
+ updateMoonLabels();
925
+ controls.update();
926
+ renderer.render(scene, cam);
927
+ }
928
+
929
+ animate();
930
+
931
+ })();
index.html CHANGED
@@ -1,19 +1,57 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
6
+ <title>Solar System Explorer</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ </head>
9
+ <body>
10
+ <div id="ui-overlay">
11
+ <div id="title-bar">
12
+ <h1> Solar System Explorer</h1>
13
+ </div>
14
+ <div id="controls">
15
+ <button id="btn-reset" aria-label="Reset View" title="Reset View">⟲</button>
16
+ </div>
17
+ <div id="toggle-panel">
18
+ <div class="toggle-row">
19
+ <label for="toggle-real-distance">Real Distance Scale</label>
20
+ <input type="checkbox" id="toggle-real-distance">
21
+ </div>
22
+ <div class="toggle-row">
23
+ <label for="toggle-real-size">Real Size Scale</label>
24
+ <input type="checkbox" id="toggle-real-size">
25
+ </div>
26
+ <div class="toggle-row">
27
+ <label for="toggle-orbits">Show Orbits</label>
28
+ <input type="checkbox" id="toggle-orbits" checked>
29
+ </div>
30
+ <div class="toggle-row">
31
+ <label for="toggle-labels">Show Labels</label>
32
+ <input type="checkbox" id="toggle-labels" checked>
33
+ </div>
34
+ <div class="toggle-row">
35
+ <label for="toggle-fulllight">Full Illumination</label>
36
+ <input type="checkbox" id="toggle-fulllight">
37
+ </div>
38
+ </div>
39
+ <div id="speed-control">
40
+ <button id="btn-play-pause" aria-label="Play/Pause" title="Play/Pause">⏸</button>
41
+ <label for="speed-slider">Speed</label>
42
+ <input type="range" id="speed-slider" min="0" max="2" step="0.05" value="1">
43
+ <span id="speed-value">1.0x</span>
44
+ </div>
45
+ <div id="credit">Created by Andy Kong</div>
46
+ <div id="planet-nav"></div>
47
+ <div id="info-panel" class="hidden">
48
+ <button id="close-info" aria-label="Close">&times;</button>
49
+ <h2 id="info-name"></h2>
50
+ <div id="info-body"></div>
51
+ </div>
52
+ </div>
53
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
54
+ <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
55
+ <script src="app.js"></script>
56
+ </body>
57
+ </html>
style.css CHANGED
@@ -1,28 +1,327 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
28
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ overflow: hidden;
9
+ background: #000;
10
+ font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
11
+ color: #fff;
12
+ }
13
+
14
+ canvas {
15
+ display: block;
16
+ }
17
+
18
+ #ui-overlay {
19
+ position: fixed;
20
+ top: 0;
21
+ left: 0;
22
+ width: 100%;
23
+ height: 100%;
24
+ pointer-events: none;
25
+ z-index: 10;
26
+ }
27
+
28
+ #ui-overlay > * {
29
+ pointer-events: auto;
30
+ }
31
+
32
+ #title-bar {
33
+ position: absolute;
34
+ top: 12px;
35
+ left: 16px;
36
+ }
37
+
38
+ #title-bar h1 {
39
+ font-size: 1.1rem;
40
+ font-weight: 300;
41
+ letter-spacing: 2px;
42
+ text-transform: uppercase;
43
+ text-shadow: 0 0 10px rgba(100,180,255,0.5);
44
+ }
45
+
46
+ #credit {
47
+ position: absolute;
48
+ bottom: 4px;
49
+ left: 50%;
50
+ transform: translateX(-50%);
51
+ font-size: 0.65rem;
52
+ color: rgba(255,255,255,0.4);
53
+ letter-spacing: 0.5px;
54
+ }
55
+
56
+ #controls {
57
+ position: absolute;
58
+ top: 12px;
59
+ right: 16px;
60
+ display: flex;
61
+ gap: 8px;
62
+ }
63
+
64
+ #controls button {
65
+ width: 36px;
66
+ height: 36px;
67
+ border-radius: 50%;
68
+ border: 1px solid rgba(255,255,255,0.2);
69
+ background: rgba(10,20,40,0.7);
70
+ color: #fff;
71
+ font-size: 1.2rem;
72
+ cursor: pointer;
73
+ backdrop-filter: blur(4px);
74
+ transition: background 0.2s, border-color 0.2s;
75
+ }
76
+
77
+ #controls button:hover {
78
+ background: rgba(30,60,120,0.8);
79
+ border-color: rgba(100,180,255,0.6);
80
+ }
81
+
82
+ #toggle-panel {
83
+ position: absolute;
84
+ top: 50px;
85
+ right: 16px;
86
+ background: rgba(5,15,35,0.8);
87
+ border: 1px solid rgba(255,255,255,0.12);
88
+ border-radius: 10px;
89
+ padding: 12px 14px;
90
+ backdrop-filter: blur(6px);
91
+ }
92
+
93
+ .toggle-row {
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: space-between;
97
+ gap: 12px;
98
+ padding: 4px 0;
99
+ }
100
+
101
+ .toggle-row label {
102
+ font-size: 0.75rem;
103
+ color: #bbb;
104
+ white-space: nowrap;
105
+ }
106
+
107
+ .toggle-row input[type="checkbox"] {
108
+ accent-color: #4ab4ff;
109
+ width: 16px;
110
+ height: 16px;
111
+ cursor: pointer;
112
+ }
113
+
114
+ #speed-control {
115
+ position: absolute;
116
+ bottom: 20px;
117
+ left: 50%;
118
+ transform: translateX(-50%);
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 10px;
122
+ background: rgba(10,20,40,0.7);
123
+ border: 1px solid rgba(255,255,255,0.15);
124
+ border-radius: 20px;
125
+ padding: 8px 16px;
126
+ backdrop-filter: blur(4px);
127
+ font-size: 0.8rem;
128
+ }
129
+
130
+ #btn-play-pause {
131
+ width: 30px;
132
+ height: 30px;
133
+ border-radius: 50%;
134
+ border: 1px solid rgba(255,255,255,0.25);
135
+ background: rgba(20,40,80,0.6);
136
+ color: #fff;
137
+ font-size: 1rem;
138
+ cursor: pointer;
139
+ display: flex;
140
+ align-items: center;
141
+ justify-content: center;
142
+ transition: background 0.2s, border-color 0.2s;
143
+ }
144
+
145
+ #btn-play-pause:hover {
146
+ background: rgba(30,60,120,0.8);
147
+ border-color: rgba(100,180,255,0.6);
148
+ }
149
+
150
+ #speed-slider {
151
+ width: 100px;
152
+ accent-color: #4ab4ff;
153
+ }
154
+
155
+ #speed-value {
156
+ min-width: 32px;
157
+ text-align: center;
158
+ }
159
+
160
+ #planet-nav {
161
+ position: absolute;
162
+ bottom: 64px;
163
+ left: 50%;
164
+ transform: translateX(-50%);
165
+ display: flex;
166
+ gap: 6px;
167
+ flex-wrap: wrap;
168
+ justify-content: center;
169
+ max-width: 90vw;
170
+ }
171
+
172
+ #planet-nav button {
173
+ padding: 5px 12px;
174
+ border-radius: 14px;
175
+ border: 1px solid rgba(255,255,255,0.2);
176
+ background: rgba(10,20,40,0.7);
177
+ color: #ccc;
178
+ font-size: 0.75rem;
179
+ cursor: pointer;
180
+ backdrop-filter: blur(4px);
181
+ transition: all 0.2s;
182
+ letter-spacing: 0.5px;
183
+ }
184
+
185
+ #planet-nav button:hover,
186
+ #planet-nav button.active {
187
+ background: rgba(30,60,120,0.8);
188
+ border-color: rgba(100,180,255,0.6);
189
+ color: #fff;
190
+ }
191
+
192
+ #info-panel {
193
+ position: absolute;
194
+ top: 60px;
195
+ left: 16px;
196
+ width: 300px;
197
+ max-height: calc(100vh - 160px);
198
+ overflow-y: auto;
199
+ background: rgba(5,15,35,0.9);
200
+ border: 1px solid rgba(100,180,255,0.2);
201
+ border-radius: 12px;
202
+ padding: 20px;
203
+ backdrop-filter: blur(10px);
204
+ transition: opacity 0.3s, transform 0.3s;
205
+ }
206
+
207
+ #info-panel.hidden {
208
+ opacity: 0;
209
+ transform: translateX(-20px);
210
+ pointer-events: none;
211
+ }
212
+
213
+ #close-info {
214
+ position: absolute;
215
+ top: 10px;
216
+ right: 12px;
217
+ background: none;
218
+ border: none;
219
+ color: #aaa;
220
+ font-size: 1.4rem;
221
+ cursor: pointer;
222
+ }
223
+
224
+ #close-info:hover {
225
+ color: #fff;
226
+ }
227
+
228
+ #info-name {
229
+ font-size: 1.3rem;
230
+ font-weight: 400;
231
+ margin-bottom: 12px;
232
+ color: #4ab4ff;
233
+ }
234
+
235
+ #info-body {
236
+ font-size: 0.82rem;
237
+ line-height: 1.6;
238
+ color: #ccc;
239
+ }
240
+
241
+ #info-body .stat {
242
+ display: flex;
243
+ justify-content: space-between;
244
+ padding: 4px 0;
245
+ border-bottom: 1px solid rgba(255,255,255,0.06);
246
+ }
247
+
248
+ #info-body .stat-label {
249
+ color: #888;
250
+ }
251
+
252
+ #info-body .description {
253
+ margin-top: 12px;
254
+ font-style: italic;
255
+ color: #aaa;
256
+ }
257
+
258
+ /* Mobile adjustments */
259
+ @media (max-width: 600px) {
260
+ #title-bar h1 {
261
+ font-size: 0.85rem;
262
+ }
263
+
264
+ #toggle-panel {
265
+ top: 46px;
266
+ right: 10px;
267
+ padding: 8px 10px;
268
+ }
269
+
270
+ .toggle-row label {
271
+ font-size: 0.68rem;
272
+ }
273
+
274
+ #info-panel {
275
+ top: auto;
276
+ bottom: 110px;
277
+ left: 10px;
278
+ right: 10px;
279
+ width: auto;
280
+ max-height: 35vh;
281
+ }
282
+
283
+ #planet-nav {
284
+ bottom: 58px;
285
+ gap: 4px;
286
+ }
287
+
288
+ #planet-nav button {
289
+ padding: 4px 8px;
290
+ font-size: 0.65rem;
291
+ }
292
+
293
+ #speed-control {
294
+ bottom: 12px;
295
+ padding: 6px 12px;
296
+ }
297
+
298
+ #speed-slider {
299
+ width: 70px;
300
+ }
301
+
302
+ #controls button {
303
+ width: 30px;
304
+ height: 30px;
305
+ font-size: 1rem;
306
+ }
307
+ }
308
+
309
+ /* Scrollbar */
310
+ #info-panel::-webkit-scrollbar {
311
+ width: 4px;
312
+ }
313
+
314
+ #info-panel::-webkit-scrollbar-thumb {
315
+ background: rgba(100,180,255,0.3);
316
+ border-radius: 2px;
317
+ }
318
+
319
+ .planet-label {
320
+ position: absolute;
321
+ color: rgba(255,255,255,0.85);
322
+ font-size: 11px;
323
+ pointer-events: none;
324
+ text-shadow: 0 0 4px rgba(0,0,0,0.9);
325
+ white-space: nowrap;
326
+ transform: translate(-50%, -100%);
327
+ }