import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { data } from "./data.js"; import { createCelestialBody, createStarField } from "./constructors.js"; import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js"; import { RenderPass } from "three/addons/postprocessing/RenderPass.js"; import { OutputPass } from "three/addons/postprocessing/OutputPass.js"; const main = function () { // Scene Setup const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100000); camera.position.y = 120; camera.position.z = 150; const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio); renderer.shadowMap.enabled = true; document.body.appendChild(renderer.domElement); const composer = new EffectComposer(renderer); const renderPass = new RenderPass(scene, camera); composer.addPass(renderPass); const outputPass = new OutputPass(); composer.addPass(outputPass); // Basic Controls const cameraDamping = 0.05; const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = cameraDamping; controls.minDistance = 5; controls.maxDistance = 6000; // Responsive Design const onWindowResize = function () { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(window.devicePixelRatio); }; window.addEventListener("resize", onWindowResize, false); // Objects const ambientLight = new THREE.AmbientLight(0x404040); scene.add(ambientLight); const sun = createCelestialBody(data, undefined, 0); let selectedPlanet = sun; let hasFocused = false; scene.add(sun); const starField = createStarField(); scene.add(starField); // Movement const movement = { forward: false, backward: false, left: false, right: false, }; const velocity = 0.5; const onKeyDown = function (event) { switch (event.code) { case "KeyW": movement.forward = true; break; case "KeyS": movement.backward = true; break; case "KeyA": movement.left = true; break; case "KeyD": movement.right = true; break; } }; const onKeyUp = function (event) { switch (event.code) { case "KeyW": movement.forward = false; break; case "KeyS": movement.backward = false; break; case "KeyA": movement.left = false; break; case "KeyD": movement.right = false; break; } }; document.addEventListener("keydown", onKeyDown); document.addEventListener("keyup", onKeyUp); // Selection const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); const updateInfoBox = function () { const infoWindow = document.getElementById("infoWindow"); if (!selectedPlanet || !selectedPlanet.content) { infoWindow.style.display = "none"; return; } const infoTitle = document.getElementById("infoTitle"); const infoContent = document.getElementById("infoContent"); infoTitle.innerText = selectedPlanet.name; infoContent.innerHTML = selectedPlanet.content; infoWindow.style.display = "block"; }; const updateDropdown = function () { const dropdown = document.getElementById("dropdown"); selectedPlanet ? (dropdown.value = selectedPlanet.uuid) : (dropdown.value = ""); }; const onRightClick = function (event) { event.preventDefault(); selectedPlanet = undefined; hasFocused = false; updateDropdown(); updateInfoBox(); }; document.addEventListener("contextmenu", onRightClick); const onDoubleLeftClick = function (event) { mouse.x = (event.clientX / window.innerWidth) * 2 - 1; mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(scene.children, true).filter((intersect) => intersect.object.isCelestialBody); if (intersects.length === 0) return; selectedPlanet = intersects[0].object; hasFocused = false; updateDropdown(); updateInfoBox(); }; document.addEventListener("dblclick", onDoubleLeftClick); const onDropdownChange = function (event) { selectedPlanet = event.target.value ? scene.getObjectByProperty("uuid", event.target.value) : undefined; hasFocused = false; updateInfoBox(); }; document.getElementById("dropdown").addEventListener("change", onDropdownChange); // `Music const music = [ "/music/track1.mp3", "/music/track2.mp3", "/music/track3.mp3", "/music/track4.mp3", "/music/track5.mp3", "/music/track6.mp3", "/music/track7.mp3", ]; let currentMusicIndex = 0; const player = document.getElementById("backgroundMusic"); const playNextTrack = function () { if (currentMusicIndex === music.length - 1) currentMusicIndex = 0; player.src = music[currentMusicIndex]; player.play(); currentMusicIndex++; }; player.addEventListener("ended", playNextTrack); playNextTrack(); // Animation let simulationSpeed = 1; const onSimulationSpeedChange = function (event) { simulationSpeed = parseFloat(event.target.value); document.getElementById("speedValue").textContent = simulationSpeed; }; document.getElementById("speedSlider").addEventListener("input", onSimulationSpeedChange); const updateCelestial = function (celestial) { const time = Date.now() * 0.0001 * simulationSpeed; const angle = time * celestial.orbitSpeed; // Orbit if (celestial.orbitRadius) { celestial.position.x = celestial.orbitRadius * Math.cos(angle); celestial.position.z = celestial.orbitRadius * Math.sin(angle) * Math.cos(celestial.orbitInclination || 0); celestial.position.y = celestial.orbitRadius * Math.sin(angle) * -Math.sin(celestial.orbitInclination || 0); celestial.orbit.rotation.y = -angle; } // Rotation if (celestial.rotationSpeed) celestial.rotation.y += celestial.rotationSpeed; // Recursion if (celestial.children) { celestial.children.forEach((child) => { if (child.isCelestialBody || child.isCelestialParticles) updateCelestial(child); }); } }; const animate = function () { requestAnimationFrame(animate); // Celestial Movement let startingPosition = selectedPlanet ? selectedPlanet.getWorldPosition(new THREE.Vector3()) : undefined; updateCelestial(sun, undefined); let endingPosition = selectedPlanet ? selectedPlanet.getWorldPosition(new THREE.Vector3()) : undefined; // Camera Movement let direction = new THREE.Vector3(); camera.getWorldDirection(direction); let forward = direction.multiplyScalar(velocity); let right = new THREE.Vector3().crossVectors(camera.up, direction).normalize().multiplyScalar(velocity); if (movement.forward) camera.position.add(forward); if (movement.backward) camera.position.sub(forward); if (movement.left) camera.position.sub(right); if (movement.right) camera.position.add(right); // Camera Focus if (selectedPlanet && camera.position.distanceTo(selectedPlanet.getWorldPosition(new THREE.Vector3())) > 10000) { hasFocused = false; } if (selectedPlanet) { const target = selectedPlanet.getWorldPosition(new THREE.Vector3()); let cameraTarget; if (selectedPlanet.name === "Sun") { cameraTarget = new THREE.Vector3(0, 30, 60); } else { const parent = selectedPlanet.parent; const parentPosition = parent.getWorldPosition(new THREE.Vector3()); const planetPosition = selectedPlanet.getWorldPosition(new THREE.Vector3()); let direction; if (parent.name === "Sun") { direction = new THREE.Vector3().subVectors(parentPosition, planetPosition).normalize(); } else { direction = new THREE.Vector3().subVectors(planetPosition, parentPosition).normalize(); } cameraTarget = new THREE.Vector3().copy(planetPosition).add(direction.multiplyScalar(60)).add(new THREE.Vector3(0, 30, 0)); } if (controls.target.distanceTo(target) < 1 && camera.position.distanceTo(cameraTarget) < 100) { hasFocused = true; } if (hasFocused) { const delta = new THREE.Vector3().subVectors(endingPosition, startingPosition); camera.position.add(delta); controls.target.add(delta); } else { if (simulationSpeed < 30) { camera.position.lerp(cameraTarget, cameraDamping * simulationSpeed); controls.target.lerp(target, cameraDamping * simulationSpeed); } else { camera.position.copy(cameraTarget); controls.target.copy(target); hasFocused = true; } } } // Camera Limits if (camera.position.x > 10000) camera.position.x = 10000; if (camera.position.x < -10000) camera.position.x = -10000; if (camera.position.y > 10000) camera.position.y = 10000; if (camera.position.y < -10000) camera.position.y = -10000; if (camera.position.z > 10000) camera.position.z = 10000; if (camera.position.z < -10000) camera.position.z = -10000; // Render controls.update(); composer.render(); }; animate(); }; main();