Files

289 lines
8.8 KiB
JavaScript

import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { UltraHDRLoader } from 'three/addons/loaders/UltraHDRLoader.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
const scene = new THREE.Scene();
const clock = new THREE.Clock(); // Required to calculate delta time
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
}
let loadData = null;
let mixer;
fetch('model-index.json')
.then(response => response.json())
.then(data => {
loadData = data;
startApp();
});
//Add grid
const size = 50;
const divisions = 200;
const centerlinecolor = 0xcccccc;
const linecolor = 0xe0e0e0;
const gridHelper = new THREE.GridHelper(size, divisions, centerlinecolor, linecolor);
scene.add(gridHelper);
//RENDERER
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setSize( sizes.width, sizes.height );
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.5;
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFShadowMap;
document.body.appendChild( renderer.domElement );
//CAMERA
const DEFAULT_CAMERA_POS = new THREE.Vector3(-0.4, 0.5, 1.5); // Camera default position
const DEFAULT_TARGET = new THREE.Vector3(0, 0, 0); // Camera default target
const camera = new THREE.PerspectiveCamera( 65, sizes.width / sizes.height, 0.001, 1000 );
// Apply default position initially
camera.position.copy(DEFAULT_CAMERA_POS);
camera.lookAt(DEFAULT_TARGET);
const controls = new OrbitControls( camera, renderer.domElement );
controls.enableDamping = true;
controls.dampingFactor = 0.05; // Friction
controls.screenSpacePanning = false; // Vertical panning along screen
controls.target.copy(DEFAULT_TARGET);
controls.update();
//RESIZING
function handleResize(){
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
camera.aspect = sizes.width/sizes.height;
camera.updateProjectionMatrix();
renderer.setSize( sizes.width, sizes.height );
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
}
window.addEventListener("resize", handleResize);
//MODEL LOADER
const loader = new GLTFLoader();
let currentModel = null;
// UPDATE NAME FUNCTION
function updateNameText(name) {
const recordtitleElement = document.getElementById('recordnumbertext');
const titleElement = document.getElementById('nametext');
//Record Number
const recnumber = name.slice(0,11);
recordtitleElement.textContent = recnumber;
//Scientific Name
const trimmedstart = name.slice(11);
titleElement.textContent = trimmedstart;
titleElement.style.fontStyle = "italic"; //italicize scientific name
}
const gui = new GUI({ width: 425 }); //New UI
if (window.innerWidth <= 600) {
gui.close(); // Starts collapsed on mobile
}
//Loading elements
const loaderContainer = document.getElementById('loader-container');
const loadingText = document.getElementById('loading-text');
let latestModelPath = null;
//Load Function
function loadModel(modelPath) {
if (currentModel) {
scene.remove(currentModel);
currentModel.traverse((child) => {
if (child.isMesh) {
child.geometry.dispose();
if (Array.isArray(child.material)) {
child.material.forEach(mat => mat.dispose());
} else {
child.material.dispose();
}
}
});
currentModel = null;
}
loaderContainer.style.display = 'flex';
console.log(`Loaded: ${modelPath}`);
setGuiEnabled(gui, false);
loader.load(modelPath, (glb) => {
currentModel = glb.scene;
scene.add(glb.scene);
// Check if the file contains animations
if (glb.animations && glb.animations.length > 0) {
// Create the AnimationMixer and bind it to the model
mixer = new THREE.AnimationMixer(currentModel);
// Get the first animation clip and create a playable action
const animationClip = glb.animations[0];
const action = mixer.clipAction(animationClip);
// Play the animation
action.play();
}
loaderContainer.style.display = 'none'; // Hide Loading Throbber
setGuiEnabled(gui, true);
});
}
function startApp() {
// 1. Create an object to hold the currently selected values
const modelsData = loadData;
const params = {
name: modelsData[0].name,
variant: Object.keys(modelsData[0].variants)[0]
};
// 2. Extract just the names for the first dropdown
const modelNames = modelsData.map(item => item.name);
// 3. Add the Controllers
const nameController = gui.add(params, 'name', modelNames).name('Model Name');
// Initialize the variants dropdown with the first model's options
let variantOptions = Object.keys(modelsData[0].variants);
let variantController = gui.add(params, 'variant', variantOptions).name('Variant');
let gridcontroller = gui.add(gridHelper, 'visible').name('Toggle Grid');
// 4. Handle dynamic updates
nameController.onChange((selectedName) => {
const selectedModelData = modelsData.find(item => item.name === selectedName); // Find the data for the newly selected model
const newVariants = Object.keys(selectedModelData.variants);
params.variant = newVariants[0]; // Update the param state to the first available variant of the new model
// Destroy the old variant dropdown and gridhelper and create a new ones with updated options
variantController.destroy();
gridcontroller.destroy();
variantController = gui.add(params, 'variant', newVariants).name('Variant');
gridcontroller = gui.add(gridHelper, 'visible').name('Toggle Grid');
// Re-attach the onChange listener to the new dropdown
attachVariantListener(variantController, selectedModelData);
//Load Model
loadModel(selectedModelData.variants[params.variant]);
// Reset camera position and target
resetcam();
//Call function to update name text
updateNameText(selectedName);
});
// Helper function to handle variant changes
function attachVariantListener(controller, modelData) {
controller.onChange((selectedVariant) => {
const modelPath = modelData.variants[selectedVariant];
// Load Model
loadModel(modelPath);
});
}
// Attach listener to the very first variant dropdown
attachVariantListener(variantController, modelsData[0]);
const initialModelPath = modelsData[0].variants[params.variant];
// Load default model right away
loadModel(initialModelPath);
updateNameText(modelsData[0].name);
// Reset camera transforms
function resetcam(){
camera.position.copy(DEFAULT_CAMERA_POS);
if (controls) {
controls.target.copy(DEFAULT_TARGET);
controls.update();
} else {
camera.lookAt(DEFAULT_TARGET);
}
}
}
gui.domElement.style.right = '0px';
//ENVIRONMENT
const env_loader = new UltraHDRLoader();
const env_texture = await env_loader.loadAsync( 'assets/studio_small_08_4k.jpg' );
env_texture.mapping = THREE.EquirectangularReflectionMapping;
scene.background = new THREE.Color(0xe6e6e6);
scene.environment = env_texture;
//UPDATE PER FRAME
renderer.setAnimationLoop( animate );
function animate() {
const delta = clock.getDelta();
if (mixer) {
mixer.update(delta);
}
controls.update();
renderer.render( scene, camera );
}
//Toggle GUI
function setGuiEnabled(guiInstance, isEnabled) {
guiInstance.controllersRecursive().forEach(controller => {
isEnabled ? controller.enable() : controller.disable();
});
// Visually dim the GUI when disabled
guiInstance.domElement.style.opacity = isEnabled ? '1' : '0.5';
guiInstance.domElement.style.pointerEvents = isEnabled ? 'auto' : 'none';
}
//Show Info div after 5 seconds after initial load
setTimeout(() => {
const infoElement = document.getElementById('info');
const mabuligheaderElement = document.getElementById('mabuligheader');
infoElement.style.display = 'block';
mabuligheaderElement.style.display = 'block';
infoElement.animate([
{ opacity: 0 },
{ opacity: 1 }
], {
duration: 500, // Duration in milliseconds (500ms = 0.5s)
easing: 'ease-in-out',
fill: 'forwards' // Keeps the final state (opacity: 1) when the animation ends
});
mabuligheaderElement.animate([
{ opacity: 0 },
{ opacity: 1 }
], {
duration: 500,
easing: 'ease-in-out',
fill: 'forwards'
});
}, 750);