249 lines
7.4 KiB
JavaScript
249 lines
7.4 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 sizes = {
|
|
width: window.innerWidth,
|
|
height: window.innerHeight,
|
|
}
|
|
|
|
|
|
let loadData = null;
|
|
|
|
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');
|
|
|
|
//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}`);
|
|
|
|
loader.load(modelPath, (glb) => {
|
|
currentModel = glb.scene;
|
|
scene.add(glb.scene);
|
|
loaderContainer.style.display = 'none'; // Hide Loading Throbber
|
|
});
|
|
|
|
}
|
|
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');
|
|
|
|
// 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 create a new one with updated options
|
|
variantController.destroy();
|
|
variantController = gui.add(params, 'variant', newVariants).name('Variant');
|
|
|
|
// 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( time ) {
|
|
|
|
controls.update();
|
|
renderer.render( scene, camera );
|
|
}
|
|
|
|
//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);
|