Spaces:
Running
Running
Upload 4 files
Browse files
README.md
CHANGED
|
@@ -1,10 +1,28 @@
|
|
| 1 |
-
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: static
|
| 7 |
-
pinned: false
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">×</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 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 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 |
+
}
|