334 lines
11 KiB
JavaScript
334 lines
11 KiB
JavaScript
import * as THREE from 'three';
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
|
|
// --- SCENE, CAMERA, RENDERER, CONTROLS ---
|
|
const scene = new THREE.Scene();
|
|
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 50000);
|
|
camera.position.set(0, 25, 45);
|
|
camera.lookAt(scene.position);
|
|
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
document.body.appendChild(renderer.domElement);
|
|
|
|
const controls = new OrbitControls(camera, renderer.domElement);
|
|
controls.enableDamping = true;
|
|
controls.dampingFactor = 0.05;
|
|
controls.screenSpacePanning = false;
|
|
controls.minDistance = 1;
|
|
controls.maxDistance = 20000;
|
|
|
|
// --- LIGHTING ---
|
|
const pointLight = new THREE.PointLight(0xffffff, 5, 60000);
|
|
scene.add(pointLight);
|
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
|
scene.add(ambientLight);
|
|
|
|
// --- TEXTURES & ASSETS ---
|
|
const textureLoader = new THREE.TextureLoader();
|
|
|
|
// --- STARS BACKGROUND ---
|
|
const starVertices = [];
|
|
for (let i = 0; i < 15000; i++) {
|
|
const x = (Math.random() - 0.5) * 25000;
|
|
const y = (Math.random() - 0.5) * 25000;
|
|
const z = (Math.random() - 0.5) * 25000;
|
|
starVertices.push(x, y, z);
|
|
}
|
|
const starGeometry = new THREE.BufferGeometry();
|
|
starGeometry.setAttribute('position', new THREE.Float32BufferAttribute(starVertices, 3));
|
|
const starMaterial = new THREE.PointsMaterial({ color: 0xffffff, size: 1.5 });
|
|
const stars = new THREE.Points(starGeometry, starMaterial);
|
|
scene.add(stars);
|
|
|
|
// --- SCALE CONSTANTS ---
|
|
const SCALES = {
|
|
artistic: {
|
|
sunSize: 5,
|
|
planetScale: 1,
|
|
orbitScale: 1,
|
|
moonOrbitScale: 1,
|
|
},
|
|
realistic: {
|
|
sunSize: 0.5,
|
|
planetScale: 0.01,
|
|
orbitScale: 100,
|
|
moonOrbitScale: 0.1,
|
|
}
|
|
};
|
|
let currentScale = 'artistic';
|
|
|
|
// --- CELESTIAL BODY DATA & CREATION ---
|
|
const celestialBodies = [];
|
|
|
|
// --- SUN ---
|
|
const sunTexture = textureLoader.load('assets/textures/sun.jpg');
|
|
const sunMaterial = new THREE.MeshBasicMaterial({ map: sunTexture });
|
|
const sun = new THREE.Mesh(new THREE.SphereGeometry(1, 32, 32), sunMaterial);
|
|
sun.name = 'Sun';
|
|
scene.add(sun);
|
|
celestialBodies.push({ name: 'Sun', object: sun, data: { radius: 696340 } }); // Real radius in km
|
|
|
|
function createPlanet(name, textureFile, data) {
|
|
const texture = textureLoader.load(`assets/textures/${textureFile}?v=${Date.now()}`);
|
|
const geometry = new THREE.SphereGeometry(data.radius, 32, 32);
|
|
const material = new THREE.MeshBasicMaterial({ map: texture });
|
|
const planet = new THREE.Mesh(geometry, material);
|
|
planet.name = name;
|
|
|
|
const pivot = new THREE.Object3D();
|
|
sun.add(pivot);
|
|
pivot.add(planet);
|
|
|
|
const planetObj = {
|
|
name,
|
|
object: planet,
|
|
pivot,
|
|
data,
|
|
};
|
|
celestialBodies.push(planetObj);
|
|
return planetObj;
|
|
}
|
|
|
|
function createMoon(name, color, data, parentPlanetObject) {
|
|
const geometry = new THREE.SphereGeometry(data.radius, 32, 32);
|
|
const material = new THREE.MeshBasicMaterial({ color });
|
|
const moon = new THREE.Mesh(geometry, material);
|
|
moon.name = name;
|
|
|
|
const pivot = new THREE.Object3D();
|
|
parentPlanetObject.add(pivot);
|
|
pivot.add(moon);
|
|
|
|
const moonObj = {
|
|
name,
|
|
object: moon,
|
|
pivot,
|
|
data,
|
|
parent: parentPlanetObject
|
|
};
|
|
celestialBodies.push(moonObj);
|
|
return moonObj;
|
|
}
|
|
|
|
function createRings(innerRadius, outerRadius, color, opacity, tilt) {
|
|
const ringGeometry = new THREE.RingGeometry(innerRadius, outerRadius, 64);
|
|
const ringMaterial = new THREE.MeshBasicMaterial({
|
|
color: color,
|
|
side: THREE.DoubleSide,
|
|
transparent: true,
|
|
opacity: opacity
|
|
});
|
|
const ring = new THREE.Mesh(ringGeometry, ringMaterial);
|
|
ring.rotation.x = tilt;
|
|
return ring;
|
|
}
|
|
|
|
const celestialData = {
|
|
mercury: { radius: 0.4, distance: 7, period: 88 },
|
|
venus: { radius: 0.9, distance: 11, period: 225 },
|
|
earth: { radius: 1, distance: 15, period: 365 },
|
|
mars: { radius: 0.7, distance: 20, period: 687 },
|
|
jupiter: { radius: 3, distance: 30, period: 4333 },
|
|
saturn: { radius: 2.5, distance: 40, period: 10759 },
|
|
uranus: { radius: 2, distance: 50, period: 30687 },
|
|
neptune: { radius: 1.9, distance: 60, period: 60190 },
|
|
moon: { radius: 0.25, distance: 2, period: 27.3 },
|
|
io: { radius: 0.2, distance: 3.5, period: 1.769 },
|
|
europa: { radius: 0.2, distance: 4.0, period: 3.551 },
|
|
ganymede: { radius: 0.3, distance: 4.5, period: 7.155 },
|
|
callisto: { radius: 0.3, distance: 5.0, period: 16.689 },
|
|
titan: { radius: 0.4, distance: 6.0, period: 15.95 },
|
|
triton: { radius: 0.35, distance: 4.0, period: 5.88 },
|
|
};
|
|
|
|
// --- INSTANTIATE BODIES ---
|
|
const mercury = createPlanet('Mercury', 'mercury.jpg', celestialData.mercury);
|
|
const venus = createPlanet('Venus', 'venus.jpg', celestialData.venus);
|
|
const earth = createPlanet('Earth', 'earth.jpg', celestialData.earth);
|
|
const mars = createPlanet('Mars', 'mars.jpg', celestialData.mars);
|
|
const jupiter = createPlanet('Jupiter', 'jupiter.jpg', celestialData.jupiter);
|
|
const saturn = createPlanet('Saturn', 'saturn.jpg', celestialData.saturn);
|
|
const uranus = createPlanet('Uranus', 'uranus.jpg', celestialData.uranus);
|
|
const neptune = createPlanet('Neptune', 'neptune.jpg', celestialData.neptune);
|
|
|
|
createMoon('Moon', 0x888888, celestialData.moon, earth.object);
|
|
createMoon('Io', 0xffff00, celestialData.io, jupiter.object);
|
|
createMoon('Europa', 0xffa500, celestialData.europa, jupiter.object);
|
|
createMoon('Ganymede', 0x00ff00, celestialData.ganymede, jupiter.object);
|
|
createMoon('Callisto', 0x0000ff, celestialData.callisto, jupiter.object);
|
|
createMoon('Titan', 0xFFAC1C, celestialData.titan, saturn.object);
|
|
createMoon('Triton', 0xADD8E6, celestialData.triton, neptune.object);
|
|
|
|
// --- RINGS ---
|
|
saturn.object.add(createRings(3.5, 5, 0xffffff, 0.6, -Math.PI / 2.5));
|
|
jupiter.object.add(createRings(3.5, 4.5, 0x8c7853, 0.4, -Math.PI / 2));
|
|
uranus.object.add(createRings(2.5, 3.5, 0xadd8e6, 0.3, -Math.PI / 2));
|
|
neptune.object.add(createRings(2.4, 3, 0xadd8e6, 0.2, -Math.PI / 2.5));
|
|
|
|
// --- UI & CONTROLS ---
|
|
const scaleToggle = document.getElementById('scale-toggle');
|
|
const focusSelect = document.getElementById('focus-select');
|
|
const dateDisplay = document.getElementById('date-container');
|
|
const slowDownButton = document.getElementById('slow-down');
|
|
const pauseToggleButton = document.getElementById('pause-toggle');
|
|
const speedUpButton = document.getElementById('speed-up');
|
|
const speedDisplay = document.getElementById('speed-display');
|
|
|
|
// Populate Focus Dropdown
|
|
celestialBodies.forEach(body => {
|
|
const option = document.createElement('option');
|
|
option.value = body.name;
|
|
option.textContent = body.name;
|
|
focusSelect.appendChild(option);
|
|
});
|
|
|
|
function focusOnObject(name) {
|
|
const body = celestialBodies.find(b => b.name === name);
|
|
if (!body) return;
|
|
|
|
const target = new THREE.Vector3();
|
|
body.object.getWorldPosition(target);
|
|
controls.target.copy(target);
|
|
|
|
const size = body.object.geometry.parameters.radius * body.object.scale.x;
|
|
const offset = size * 5;
|
|
|
|
const cameraPosition = new THREE.Vector3(target.x + offset, target.y + offset / 2, target.z + offset);
|
|
camera.position.copy(cameraPosition);
|
|
controls.update();
|
|
}
|
|
|
|
focusSelect.addEventListener('change', (event) => {
|
|
focusOnObject(event.target.value);
|
|
});
|
|
|
|
scaleToggle.addEventListener('change', (event) => {
|
|
const scale = event.target.checked ? 'realistic' : 'artistic';
|
|
updateScale(scale);
|
|
});
|
|
|
|
// --- TIME CONTROLS ---
|
|
let simulationSpeed = 1; // Represents days per second
|
|
let isPaused = false;
|
|
let currentDate = new Date();
|
|
|
|
function updateSpeedDisplay() {
|
|
speedDisplay.textContent = `Speed: ${simulationSpeed}x`;
|
|
}
|
|
|
|
pauseToggleButton.addEventListener('click', () => {
|
|
isPaused = !isPaused;
|
|
pauseToggleButton.textContent = isPaused ? 'Resume' : 'Pause';
|
|
});
|
|
|
|
slowDownButton.addEventListener('click', () => {
|
|
simulationSpeed /= 2;
|
|
updateSpeedDisplay();
|
|
});
|
|
|
|
speedUpButton.addEventListener('click', () => {
|
|
simulationSpeed *= 2;
|
|
updateSpeedDisplay();
|
|
});
|
|
|
|
|
|
// --- SCALE UPDATE LOGIC ---
|
|
function updateScale(scaleType) {
|
|
currentScale = scaleType;
|
|
const scaleConfig = SCALES[scaleType];
|
|
|
|
sun.scale.setScalar(scaleConfig.sunSize);
|
|
|
|
celestialBodies.forEach(body => {
|
|
if (body.name === 'Sun') return;
|
|
|
|
const { object, pivot, data } = body;
|
|
object.scale.setScalar(scaleConfig.planetScale);
|
|
|
|
if (pivot) {
|
|
if (body.parent) {
|
|
object.position.x = data.distance * scaleConfig.moonOrbitScale;
|
|
} else {
|
|
object.position.x = data.distance * scaleConfig.orbitScale;
|
|
}
|
|
}
|
|
});
|
|
|
|
if (scaleType === 'realistic') {
|
|
controls.maxDistance = 400000;
|
|
controls.minDistance = 0.01;
|
|
} else {
|
|
controls.maxDistance = 1000;
|
|
controls.minDistance = 1;
|
|
}
|
|
focusOnObject(focusSelect.value);
|
|
}
|
|
|
|
// Initial setup
|
|
updateScale('artistic');
|
|
updateSpeedDisplay();
|
|
|
|
// --- LABELS ---
|
|
const labels = celestialBodies.map(body => ({
|
|
object: body.object,
|
|
element: document.getElementById(`label-${body.name.toLowerCase()}`),
|
|
})).filter(label => label.element);
|
|
|
|
// --- ANIMATION LOOP ---
|
|
const clock = new THREE.Clock();
|
|
let simulationTime = 0;
|
|
const ORBIT_SPEED_FACTOR = 0.1; // Slows down overall orbit speed
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
const deltaTime = clock.getDelta();
|
|
|
|
if (!isPaused) {
|
|
const timePassed = deltaTime * simulationSpeed;
|
|
simulationTime += timePassed;
|
|
// Advance current date by days
|
|
currentDate.setDate(currentDate.getDate() + timePassed);
|
|
}
|
|
|
|
// Update Date Display
|
|
dateDisplay.textContent = currentDate.toDateString();
|
|
|
|
sun.rotation.y += 0.0005 * simulationSpeed * deltaTime;
|
|
|
|
celestialBodies.forEach(body => {
|
|
if (!body.pivot || !body.data.period) return;
|
|
|
|
const angle = (simulationTime * ORBIT_SPEED_FACTOR) / body.data.period * (2 * Math.PI);
|
|
body.pivot.rotation.y = angle;
|
|
|
|
body.object.rotation.y += 0.05 * deltaTime * simulationSpeed;
|
|
});
|
|
|
|
// Update labels
|
|
labels.forEach(labelData => {
|
|
const { object, element } = labelData;
|
|
if (!element) return;
|
|
const vector = new THREE.Vector3();
|
|
object.getWorldPosition(vector);
|
|
vector.project(camera);
|
|
|
|
const x = (vector.x * 0.5 + 0.5) * window.innerWidth;
|
|
const y = (vector.y * -0.5 + 0.5) * window.innerHeight;
|
|
|
|
element.style.transform = `translate(-50%, -50%) translate(${x}px, ${y}px)`;
|
|
});
|
|
|
|
controls.update();
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
animate();
|
|
|
|
// Handle window resize
|
|
window.addEventListener('resize', () => {
|
|
camera.aspect = window.innerWidth / window.innerHeight;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
});
|