This repository has been archived on 2025-07-31. You can view files and clone it, but cannot push or open issues or pull requests.
Files
studenthack2024-project/js/main.js
Nikolaos Karaolidis cbdd838693 Deploy APOD Button
Co-authored-by: Panos Triantafyllidis <panostriantafyllidis@users.noreply.github.com>
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2024-04-14 11:58:00 +01:00

335 lines
10 KiB
JavaScript

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();
// APOD
document.getElementById("apodButton").addEventListener("click", function () {
window.open("https://apod.nasa.gov/apod/astropix.html", "_blank");
});
document.getElementById("downloadApodButton").addEventListener("click", async function () {
const apiKey = process.env.NASA_API_KEY;
const apiUrl = `https://api.nasa.gov/planetary/apod?api_key=${apiKey}`;
try {
const response = await fetch(apiUrl);
const data = await response.json();
if (data.media_type === "image") {
const a = document.createElement("a");
a.href = data.hdurl || data.url;
a.download = data.title.replace(/[^a-zA-Z0-9]/g, "_") + ".jpg";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
} else {
alert("The APOD for today is not an image.");
}
} catch (error) {
console.error("Failed to fetch APOD data:", error);
alert("Failed to download the APOD image. Please try again later.");
}
});
// 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();