js files separated
This commit is contained in:
parent
8390ac15b1
commit
1391d2b5cf
106
src/animationManager.js
Normal file
106
src/animationManager.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
// Bold scene roughness animation state
|
||||
export let boldRoughnessAnimation = {
|
||||
isActive: false,
|
||||
startTime: 0,
|
||||
delayDuration: 1.0, // 1 second delay (will be dynamic)
|
||||
transitionDuration: 1.0, // 1 second transition
|
||||
startRoughness: 0.5,
|
||||
endRoughness: 0.05,
|
||||
materials: [] // Store references to bold materials
|
||||
};
|
||||
|
||||
// Innovation glass animation state
|
||||
export let innovationGlassAnimation = {
|
||||
isActive: false,
|
||||
startTime: 0,
|
||||
transitionDuration: 0.2,
|
||||
startIor: 1.0,
|
||||
endIor: 2.0,
|
||||
startThickness: 1.0,
|
||||
endThickness: 2.0,
|
||||
materials: [] // Store references to innovation glass materials
|
||||
};
|
||||
|
||||
// Start/restart bold roughness animation with optional delay control
|
||||
export function startBoldRoughnessAnimation(withDelay = true) {
|
||||
console.log('Starting/restarting bold roughness animation');
|
||||
// Reset all bold glass materials to starting roughness value
|
||||
boldRoughnessAnimation.materials.forEach(material => {
|
||||
material.roughness = boldRoughnessAnimation.startRoughness;
|
||||
material.needsUpdate = true;
|
||||
});
|
||||
boldRoughnessAnimation.isActive = true;
|
||||
boldRoughnessAnimation.startTime = performance.now();
|
||||
// Set delayDuration based on withDelay parameter
|
||||
boldRoughnessAnimation.delayDuration = withDelay ? 1.0 : 0.0;
|
||||
console.log('Bold roughness animation started with delay:', withDelay);
|
||||
}
|
||||
|
||||
// Start innovation glass animation
|
||||
export function startInnovationGlassAnimation() {
|
||||
console.log('Starting innovation glass animation');
|
||||
// Reset all innovation glass materials to starting values
|
||||
innovationGlassAnimation.materials.forEach(material => {
|
||||
material.ior = innovationGlassAnimation.startIor;
|
||||
material.thickness = innovationGlassAnimation.startThickness;
|
||||
material.needsUpdate = true;
|
||||
});
|
||||
innovationGlassAnimation.isActive = true;
|
||||
innovationGlassAnimation.startTime = performance.now();
|
||||
console.log('Innovation glass animation started');
|
||||
}
|
||||
|
||||
export function updateBoldRoughnessAnimation() {
|
||||
if (boldRoughnessAnimation.isActive) {
|
||||
const elapsed = (performance.now() - boldRoughnessAnimation.startTime) / 1000;
|
||||
if (elapsed >= boldRoughnessAnimation.delayDuration) {
|
||||
// Delay period is over, start roughness transition
|
||||
const transitionElapsed = elapsed - boldRoughnessAnimation.delayDuration;
|
||||
const transitionProgress = Math.min(transitionElapsed / boldRoughnessAnimation.transitionDuration, 1);
|
||||
// Smooth easing function (ease-in-out)
|
||||
const easeInOut = (t) => t * t * (3 - 2 * t);
|
||||
const easedProgress = easeInOut(transitionProgress);
|
||||
// Interpolate roughness from 0.5 to 0.05
|
||||
const currentRoughness = boldRoughnessAnimation.startRoughness +
|
||||
(boldRoughnessAnimation.endRoughness - boldRoughnessAnimation.startRoughness) * easedProgress;
|
||||
// Apply to all bold materials
|
||||
boldRoughnessAnimation.materials.forEach(material => {
|
||||
material.roughness = currentRoughness;
|
||||
material.needsUpdate = true;
|
||||
});
|
||||
// End animation when complete
|
||||
if (transitionProgress >= 1) {
|
||||
boldRoughnessAnimation.isActive = false;
|
||||
console.log('Bold roughness animation completed');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function updateInnovationGlassAnimation() {
|
||||
if (innovationGlassAnimation.isActive) {
|
||||
const elapsed = (performance.now() - innovationGlassAnimation.startTime) / 1000;
|
||||
const transitionProgress = Math.min(elapsed / innovationGlassAnimation.transitionDuration, 1);
|
||||
// Smooth easing function (ease-in-out)
|
||||
const easeInOut = (t) => t * t * (3 - 2 * t);
|
||||
const easedProgress = easeInOut(transitionProgress);
|
||||
// Interpolate IOR from 1.0 to 2.0
|
||||
const currentIor = innovationGlassAnimation.startIor +
|
||||
(innovationGlassAnimation.endIor - innovationGlassAnimation.startIor) * easedProgress;
|
||||
// Interpolate thickness from 1.0 to 2.0
|
||||
const currentThickness = innovationGlassAnimation.startThickness +
|
||||
(innovationGlassAnimation.endThickness - innovationGlassAnimation.startThickness) * easedProgress;
|
||||
// Apply to all innovation glass materials
|
||||
innovationGlassAnimation.materials.forEach(material => {
|
||||
material.ior = currentIor;
|
||||
material.thickness = currentThickness;
|
||||
material.needsUpdate = true;
|
||||
});
|
||||
// End animation when complete
|
||||
if (transitionProgress >= 1) {
|
||||
innovationGlassAnimation.isActive = false;
|
||||
console.log('Innovation glass animation completed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
908
src/main.js
908
src/main.js
|
@ -1,121 +1,34 @@
|
|||
import './style.css'
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
|
||||
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
||||
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
||||
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
||||
|
||||
// Loading Manager
|
||||
class SceneLoader {
|
||||
constructor() {
|
||||
this.loadingScreen = document.getElementById('loading-screen');
|
||||
this.loadingText = document.getElementById('loading-text');
|
||||
this.loadingProgressBar = document.getElementById('loading-progress-bar');
|
||||
this.loadingPercentage = document.getElementById('loading-percentage');
|
||||
this.modelsToLoad = [
|
||||
{ file: 'bold.glb', type: 'bold' },
|
||||
{ file: 'innovation.glb', type: 'innovation' },
|
||||
{ file: 'agility.glb', type: 'agility' },
|
||||
{ file: 'storytelling.glb', type: 'storytelling' }
|
||||
];
|
||||
this.loadedModels = {};
|
||||
this.loadedCount = 0;
|
||||
this.totalModels = this.modelsToLoad.length;
|
||||
}
|
||||
|
||||
setLoadingMessage(message) {
|
||||
this.loadingText.textContent = message;
|
||||
}
|
||||
|
||||
updateProgress(progress) {
|
||||
const percentage = Math.round(progress * 100);
|
||||
this.loadingProgressBar.style.width = `${percentage}%`;
|
||||
this.loadingPercentage.textContent = `${percentage}%`;
|
||||
}
|
||||
|
||||
hideLoadingScreen() {
|
||||
this.loadingScreen.classList.add('hidden');
|
||||
setTimeout(() => {
|
||||
this.loadingScreen.style.display = 'none';
|
||||
}, 800);
|
||||
}
|
||||
|
||||
async loadAllModels() {
|
||||
return new Promise((resolve) => {
|
||||
const loader = new GLTFLoader();
|
||||
const dracoLoader = new DRACOLoader();
|
||||
dracoLoader.setDecoderPath('node_modules/three/examples/jsm/libs/draco/');
|
||||
loader.setDRACOLoader(dracoLoader);
|
||||
this.modelsToLoad.forEach((modelInfo, index) => {
|
||||
this.setLoadingMessage(`Loading experience...`);
|
||||
loader.load(`/${modelInfo.file}`,
|
||||
(gltf) => {
|
||||
this.loadedModels[modelInfo.type] = {
|
||||
scene: gltf.scene,
|
||||
animations: gltf.animations,
|
||||
gltf: gltf
|
||||
};
|
||||
this.loadedCount++;
|
||||
const progress = this.loadedCount / this.totalModels;
|
||||
this.updateProgress(progress);
|
||||
if (this.loadedCount === this.totalModels) {
|
||||
this.setLoadingMessage('Initializing Experience...');
|
||||
setTimeout(() => {
|
||||
this.hideLoadingScreen();
|
||||
resolve(this.loadedModels);
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
(progress) => {
|
||||
const fileProgress = progress.loaded / progress.total;
|
||||
const totalProgress = (this.loadedCount + fileProgress) / this.totalModels;
|
||||
this.updateProgress(totalProgress);
|
||||
},
|
||||
(error) => {
|
||||
console.error(`Error loading ${modelInfo.file}:`, error);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
import { SceneLoader } from './sceneLoader.js';
|
||||
import { createScene, setupLighting, setupControls } from './sceneSetup.js';
|
||||
import { createModelFromPreloaded } from './modelManager.js';
|
||||
import {
|
||||
currentModel,
|
||||
nextModel,
|
||||
mixer,
|
||||
nextMixer,
|
||||
isTransitioning,
|
||||
updateTransition,
|
||||
onMouseScroll,
|
||||
setCurrentModel,
|
||||
setMixer
|
||||
} from './transitionManager.js';
|
||||
import {
|
||||
startBoldRoughnessAnimation,
|
||||
updateBoldRoughnessAnimation,
|
||||
updateInnovationGlassAnimation
|
||||
} from './animationManager.js';
|
||||
|
||||
// Initialize loader
|
||||
const sceneLoader = new SceneLoader();
|
||||
sceneLoader.setLoadingMessage('Preparing Your Experience...');
|
||||
|
||||
// Scene setup
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
camera.setFocalLength(50);
|
||||
const raycaster = new THREE.Raycaster();
|
||||
const mouse = new THREE.Vector2();
|
||||
|
||||
// Transition state management
|
||||
let currentScene = 0; // 0: bold, 1: innovation, 2: agility, 3: storytelling
|
||||
let isTransitioning = false;
|
||||
const fadeSpeed = 1; // Easily adjustable fade speed
|
||||
const transitionDuration = 1; // Easily adjustable transition duration (seconds)
|
||||
let scrollDownCount = 0;
|
||||
let scrollUpCount = 0;
|
||||
const scrollThreshold = 10; // Changed to 10 as requested
|
||||
let transitionStartTime = 0;
|
||||
let transitionDirection = 1; // 1 for forward, -1 for backward
|
||||
|
||||
// Camera-relative transition vectors
|
||||
let transitionUpVector = new THREE.Vector3();
|
||||
let transitionDownVector = new THREE.Vector3();
|
||||
const transitionDistance = 50; // Increased distance for more dramatic transitions
|
||||
|
||||
// Scene objects
|
||||
let currentModel = null;
|
||||
let nextModel = null;
|
||||
let mixer = null;
|
||||
let nextMixer = null;
|
||||
let autoRotationAngle = 0;
|
||||
// Create scene components
|
||||
const { scene, camera, renderer, composer } = createScene();
|
||||
setupLighting(scene, camera);
|
||||
const controls = setupControls(camera, renderer);
|
||||
|
||||
// Turntable animation settings
|
||||
const turntableSpeed = 0.5; // Rotation speed (radians per second)
|
||||
|
@ -123,737 +36,80 @@ const turntableSpeed = 0.5; // Rotation speed (radians per second)
|
|||
// Store preloaded models
|
||||
let preloadedModels = {};
|
||||
|
||||
// Bold scene roughness animation state
|
||||
let boldRoughnessAnimation = {
|
||||
isActive: false,
|
||||
startTime: 0,
|
||||
delayDuration: 1.0, // 1 second delay (will be dynamic)
|
||||
transitionDuration: 1.0, // 1 second transition
|
||||
startRoughness: 0.5,
|
||||
endRoughness: 0.05,
|
||||
materials: [] // Store references to bold materials
|
||||
};
|
||||
|
||||
// Innovation glass animation state
|
||||
let innovationGlassAnimation = {
|
||||
isActive: false,
|
||||
startTime: 0,
|
||||
transitionDuration: 0.2,
|
||||
startIor: 1.0,
|
||||
endIor: 2.0,
|
||||
startThickness: 1.0,
|
||||
endThickness: 2.0,
|
||||
materials: [] // Store references to innovation glass materials
|
||||
};
|
||||
|
||||
// Renderer setup
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
renderer.setClearColor(0x000000);
|
||||
renderer.shadowMap.enabled = true;
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
renderer.toneMappingExposure = 1.2;
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
renderer.physicallyCorrectLights = true;
|
||||
document.body.appendChild(renderer.domElement);
|
||||
|
||||
// Post-processing: Bloom
|
||||
const composer = new EffectComposer(renderer);
|
||||
const renderPass = new RenderPass(scene, camera);
|
||||
composer.addPass(renderPass);
|
||||
const bloomPass = new UnrealBloomPass(
|
||||
new THREE.Vector2(window.innerWidth, window.innerHeight),
|
||||
1.0, // strength
|
||||
0.45, // radius
|
||||
0.85 // threshold
|
||||
);
|
||||
composer.addPass(bloomPass);
|
||||
|
||||
// Video texture for emissive "screen"-like effect on orange material
|
||||
const video = document.createElement('video');
|
||||
video.src = '/shader-flash.webm';
|
||||
video.muted = true;
|
||||
video.loop = true;
|
||||
video.playsInline = true;
|
||||
video.autoplay = true;
|
||||
video.preload = 'auto';
|
||||
const videoTexture = new THREE.VideoTexture(video);
|
||||
videoTexture.colorSpace = THREE.SRGBColorSpace;
|
||||
videoTexture.generateMipmaps = false;
|
||||
videoTexture.minFilter = THREE.LinearFilter;
|
||||
videoTexture.magFilter = THREE.LinearFilter;
|
||||
|
||||
// Ensure autoplay starts (muted autoplay is commonly allowed)
|
||||
video.play().catch(() => { });
|
||||
|
||||
// Local procedural environment for better PBR response (no network)
|
||||
const pmrem = new THREE.PMREMGenerator(renderer);
|
||||
const roomEnv = new RoomEnvironment();
|
||||
scene.environment = pmrem.fromScene(roomEnv).texture;
|
||||
pmrem.dispose();
|
||||
roomEnv.dispose();
|
||||
scene.environment = null; // This will make the renderer's clear color visible again
|
||||
|
||||
// Consistent Lighting Setup
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
||||
scene.add(ambientLight);
|
||||
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x666666, 1.5);
|
||||
hemiLight.position.set(0, 20, 0);
|
||||
scene.add(hemiLight);
|
||||
const fillLight = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||
fillLight.position.set(-12, 6, -8);
|
||||
scene.add(fillLight);
|
||||
const topLight = new THREE.DirectionalLight(0xffffff, 1.5);
|
||||
topLight.position.set(5, 15, 5);
|
||||
scene.add(topLight);
|
||||
const bottomLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||
bottomLight.position.set(-3, -8, 3);
|
||||
scene.add(bottomLight);
|
||||
const leftLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
||||
leftLight.position.set(-12, 2, 5);
|
||||
scene.add(leftLight);
|
||||
const rightLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
||||
rightLight.position.set(12, 2, -5);
|
||||
scene.add(rightLight);
|
||||
const frontLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||
frontLight.position.set(8, 4, 12);
|
||||
scene.add(frontLight);
|
||||
const backLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||
backLight.position.set(-8, 4, -12);
|
||||
scene.add(backLight);
|
||||
const cameraLight = new THREE.PointLight(0xffffff, 0.8, 0, 2);
|
||||
camera.add(cameraLight);
|
||||
scene.add(camera);
|
||||
|
||||
// Controls with zoom disabled and camera constraints
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.25;
|
||||
controls.enableZoom = false; // Disable zoom
|
||||
|
||||
// Add camera constraints to prevent extreme angles
|
||||
controls.maxPolarAngle = Math.PI * 0.8; // Prevent looking too far up
|
||||
controls.minPolarAngle = Math.PI * 0.2; // Prevent looking too far down
|
||||
console.log('Orbit controls initialized with camera constraints');
|
||||
|
||||
// Material definitions
|
||||
|
||||
// Bold glass material (starts rough, will transition to clear)
|
||||
const boldGlassMaterial = new THREE.MeshPhysicalMaterial({
|
||||
color: 0xffffff,
|
||||
metalness: 0.2,
|
||||
roughness: 0.5, // Start with rough glass
|
||||
transmission: 1,
|
||||
ior: 2,
|
||||
thickness: 2,
|
||||
clearcoat: 1.0,
|
||||
clearcoatRoughness: 0.1,
|
||||
attenuationColor: new THREE.Color(0xffffff),
|
||||
attenuationDistance: 0.8,
|
||||
envMapIntensity: 0,
|
||||
specularIntensity: 1.0,
|
||||
specularColor: new THREE.Color(0xffffff),
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
alphaTest: 0
|
||||
});
|
||||
|
||||
// Orange wireframe material for bold Cubewire mesh
|
||||
const boldWireframeMaterial = new THREE.MeshStandardMaterial({
|
||||
color: 0xff8600,
|
||||
metalness: 0.05,
|
||||
roughness: 0.5
|
||||
});
|
||||
|
||||
// Clear thick glass for innovation (starts with animated values)
|
||||
const innovationGlassMaterial = new THREE.MeshPhysicalMaterial({
|
||||
color: 0xffffff,
|
||||
metalness: 0.2,
|
||||
roughness: 0.05,
|
||||
transmission: 1,
|
||||
ior: 1.0, // Will animate from 1 to 2
|
||||
thickness: 1.0, // Will animate from 1 to 2
|
||||
clearcoat: 1.0,
|
||||
clearcoatRoughness: 0.1,
|
||||
attenuationColor: new THREE.Color(0xffffff),
|
||||
attenuationDistance: 0.8,
|
||||
envMapIntensity: 0,
|
||||
specularIntensity: 1.0,
|
||||
specularColor: new THREE.Color(0x000000),
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
alphaTest: 0
|
||||
});
|
||||
|
||||
// Slightly frosted glass for agility and storytelling
|
||||
const frostedGlassMaterial = new THREE.MeshPhysicalMaterial({
|
||||
color: 0xffffff,
|
||||
metalness: 0.0,
|
||||
roughness: 0.25,
|
||||
transmission: 1.0,
|
||||
ior: 1.5,
|
||||
thickness: 2.0,
|
||||
clearcoat: 0.75,
|
||||
clearcoatRoughness: 0.25,
|
||||
attenuationColor: new THREE.Color(0xffffff),
|
||||
attenuationDistance: 1.5,
|
||||
envMapIntensity: 1.25,
|
||||
specularIntensity: 1.0,
|
||||
specularColor: new THREE.Color(0xffffff),
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
// Orange material with video shader for innovation
|
||||
const lightOrangeMaterial = new THREE.MeshStandardMaterial({
|
||||
color: 0xff8600,
|
||||
metalness: 0.05,
|
||||
roughness: 0.4,
|
||||
envMapIntensity: 0,
|
||||
emissive: new THREE.Color(0xffad47),
|
||||
emissiveMap: videoTexture,
|
||||
emissiveIntensity: 2.25
|
||||
});
|
||||
|
||||
// Calculate camera-relative transition vectors for diagonal movement
|
||||
function calculateTransitionVectors() {
|
||||
// Get camera's world direction
|
||||
const cameraDirection = new THREE.Vector3();
|
||||
camera.getWorldDirection(cameraDirection);
|
||||
// Get world up vector
|
||||
const worldUp = new THREE.Vector3(0, 1, 0);
|
||||
// Calculate camera's left vector - BACK TO ORIGINAL (this gave correct left direction)
|
||||
const cameraLeft = new THREE.Vector3();
|
||||
cameraLeft.crossVectors(worldUp, cameraDirection).normalize();
|
||||
// Calculate camera's local up vector
|
||||
const cameraUp = new THREE.Vector3();
|
||||
cameraUp.crossVectors(cameraLeft, cameraDirection).normalize();
|
||||
// Blend camera up with world up - BUT NEGATE to flip up/down direction
|
||||
const blendedUp = new THREE.Vector3();
|
||||
blendedUp.addVectors(
|
||||
cameraUp.clone().multiplyScalar(0.5),
|
||||
worldUp.clone().multiplyScalar(0.5)
|
||||
).normalize().negate(); // ADD .negate() here to flip up to down
|
||||
// Create diagonal vector (up-left)
|
||||
const diagonalUpLeft = new THREE.Vector3();
|
||||
diagonalUpLeft.addVectors(
|
||||
blendedUp.clone().multiplyScalar(0.5),
|
||||
cameraLeft.clone().multiplyScalar(0.5)
|
||||
).normalize();
|
||||
// Set transition vectors
|
||||
transitionUpVector = diagonalUpLeft.clone().multiplyScalar(transitionDistance);
|
||||
transitionDownVector = diagonalUpLeft.clone().multiplyScalar(-transitionDistance);
|
||||
console.log('Diagonal transition vectors calculated with distance:', transitionDistance);
|
||||
}
|
||||
|
||||
// Apply materials based on model type
|
||||
function applyMaterials(model, modelType) {
|
||||
console.log(`=== Material Assignment Debug for ${modelType} ===`);
|
||||
let meshCount = 0;
|
||||
model.traverse((object) => {
|
||||
if (object.isMesh) {
|
||||
meshCount++;
|
||||
console.log(`Found mesh: "${object.name}"`);
|
||||
const previousMaterial = object.material;
|
||||
object.castShadow = true;
|
||||
object.receiveShadow = true;
|
||||
if (modelType === 'bold') {
|
||||
// Bold-specific material logic
|
||||
if (object.name === 'Cube') {
|
||||
console.log(` → Applying bold glass material to "${object.name}"`);
|
||||
object.material = boldGlassMaterial.clone();
|
||||
object.material.side = THREE.DoubleSide;
|
||||
object.material.depthWrite = false;
|
||||
object.renderOrder = 2;
|
||||
// Store material reference for roughness animation
|
||||
boldRoughnessAnimation.materials.push(object.material);
|
||||
} else if (object.name === 'Cubewire') {
|
||||
console.log(` → Applying wireframe material to "${object.name}"`);
|
||||
object.material = boldWireframeMaterial.clone();
|
||||
object.renderOrder = 1;
|
||||
} else {
|
||||
console.log(` → Applying bold glass material (fallback) to "${object.name}"`);
|
||||
object.material = boldGlassMaterial.clone();
|
||||
// Store material reference for roughness animation
|
||||
boldRoughnessAnimation.materials.push(object.material);
|
||||
}
|
||||
} else if (modelType === 'innovation') {
|
||||
// Innovation-specific material logic
|
||||
const orangeMeshes = ['dblsc', 'ec', 'gemini', 'infinity', 'star', 'dpd'];
|
||||
const targetGlassNames = ['Cube.alt90.df'];
|
||||
const sanitize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
const nameMatches = (name, targets) => {
|
||||
const clean = sanitize(name);
|
||||
return targets.some((t) => {
|
||||
const ct = sanitize(t);
|
||||
return clean === ct || clean.includes(ct) || ct.includes(clean);
|
||||
});
|
||||
};
|
||||
if (nameMatches(object.name, targetGlassNames)) {
|
||||
// Create outer glass shell with innovation-specific material
|
||||
object.material = innovationGlassMaterial.clone();
|
||||
object.material.side = THREE.DoubleSide;
|
||||
object.material.depthWrite = false;
|
||||
object.renderOrder = 2;
|
||||
// Store material reference for animation
|
||||
innovationGlassAnimation.materials.push(object.material);
|
||||
// Create inner glass shell
|
||||
const innerShell = object.clone();
|
||||
innerShell.material = innovationGlassMaterial.clone();
|
||||
innerShell.material.side = THREE.DoubleSide;
|
||||
innerShell.material.depthWrite = false;
|
||||
innerShell.material.transmission = 0.8;
|
||||
innerShell.renderOrder = 1;
|
||||
innerShell.scale.multiplyScalar(0.95);
|
||||
// Store inner shell material reference for animation too
|
||||
innovationGlassAnimation.materials.push(innerShell.material);
|
||||
object.parent.add(innerShell);
|
||||
} else if (nameMatches(object.name, orangeMeshes)) {
|
||||
object.material = lightOrangeMaterial.clone();
|
||||
object.renderOrder = 0;
|
||||
}
|
||||
} else {
|
||||
// Agility and Storytelling use frosted glass material for all meshes
|
||||
if (object.name.startsWith('base')) {
|
||||
console.log(` → Applying frosted glass material to "${object.name}"`);
|
||||
object.material = frostedGlassMaterial.clone();
|
||||
} else {
|
||||
console.log(` → Applying frosted glass material (fallback) to "${object.name}"`);
|
||||
object.material = frostedGlassMaterial.clone();
|
||||
}
|
||||
}
|
||||
object.material.needsUpdate = true;
|
||||
// Cleanup previous materials
|
||||
if (Array.isArray(previousMaterial)) {
|
||||
previousMaterial.forEach((mat) => mat && mat.dispose && mat.dispose());
|
||||
} else if (previousMaterial && previousMaterial.dispose) {
|
||||
previousMaterial.dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log(`Total meshes processed: ${meshCount}`);
|
||||
console.log(`=== End Material Assignment Debug for ${modelType} ===`);
|
||||
}
|
||||
|
||||
// Center and frame model with camera
|
||||
function centerAndFrameModel(model, targetCamera = camera) {
|
||||
const box = new THREE.Box3().setFromObject(model);
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
model.position.sub(center);
|
||||
model.updateMatrixWorld(true);
|
||||
// Only set camera position if it's not already positioned (avoid reset during transitions)
|
||||
// Use fixed camera distance that's further away from the origin
|
||||
if (!isTransitioning) {
|
||||
const fixedCameraDistance = 50; // Fixed distance, much further than before
|
||||
// Calculate isometric-like position with 35-degree angles
|
||||
const angle = 35 * Math.PI / 180; // Convert 35 degrees to radians
|
||||
const cosAngle = Math.cos(angle);
|
||||
const x = fixedCameraDistance * cosAngle;
|
||||
const y = fixedCameraDistance * cosAngle;
|
||||
const z = fixedCameraDistance * cosAngle;
|
||||
targetCamera.position.set(x, y, z);
|
||||
controls.target.set(0, 0, 0);
|
||||
// Set distance limits to lock the camera at this distance
|
||||
controls.minDistance = fixedCameraDistance;
|
||||
controls.maxDistance = fixedCameraDistance;
|
||||
controls.update();
|
||||
console.log(`Camera positioned at: x=${x}, y=${y}, z=${z}, distance=${fixedCameraDistance}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Setup animations based on model type
|
||||
function setupAnimations(model, gltf, modelType) {
|
||||
if (gltf.animations && gltf.animations.length > 0) {
|
||||
const animMixer = new THREE.AnimationMixer(model);
|
||||
gltf.animations.forEach((clip) => {
|
||||
const action = animMixer.clipAction(clip);
|
||||
if (modelType === 'bold') {
|
||||
// Play once for bold
|
||||
action.loop = THREE.LoopOnce;
|
||||
action.clampWhenFinished = true;
|
||||
action.play();
|
||||
console.log(`Bold animation started: ${clip.name}`);
|
||||
} else if (modelType === 'innovation') {
|
||||
// PingPong loop for innovation
|
||||
action.loop = THREE.LoopPingPong;
|
||||
action.play();
|
||||
console.log(`Innovation animation started: ${clip.name} (PingPong)`);
|
||||
} else if (modelType === 'agility') {
|
||||
// Regular loop for agility
|
||||
action.loop = THREE.LoopRepeat;
|
||||
action.play();
|
||||
console.log(`Agility animation started: ${clip.name} (Loop)`);
|
||||
} else if (modelType === 'storytelling') {
|
||||
// Play once for storytelling
|
||||
action.loop = THREE.LoopOnce;
|
||||
action.clampWhenFinished = true;
|
||||
action.play();
|
||||
console.log(`Storytelling animation started: ${clip.name}`);
|
||||
}
|
||||
});
|
||||
if (modelType === 'innovation') {
|
||||
animMixer.timeScale = 3.0; // Keep existing timeScale for innovation
|
||||
console.log('Innovation animation timeScale set to 3.0');
|
||||
}
|
||||
return animMixer;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create model from preloaded data - FIXED: Always create fresh geometry
|
||||
function createModelFromPreloaded(modelType) {
|
||||
const preloadedData = preloadedModels[modelType];
|
||||
if (!preloadedData) {
|
||||
console.error(`Preloaded model not found: ${modelType}`);
|
||||
return { model: null, animMixer: null };
|
||||
}
|
||||
console.log(`Creating model from preloaded data: ${modelType}`);
|
||||
// Clear animation materials arrays when creating new models
|
||||
if (modelType === 'bold') {
|
||||
boldRoughnessAnimation.materials = [];
|
||||
} else if (modelType === 'innovation') {
|
||||
innovationGlassAnimation.materials = [];
|
||||
}
|
||||
// Clone the scene deeply to ensure fresh geometry
|
||||
const model = preloadedData.scene.clone(true);
|
||||
// IMPORTANT: Clone all geometries to ensure they're independent
|
||||
model.traverse((object) => {
|
||||
if (object.isMesh && object.geometry) {
|
||||
object.geometry = object.geometry.clone();
|
||||
}
|
||||
});
|
||||
// Apply materials
|
||||
applyMaterials(model, modelType);
|
||||
// Setup animations
|
||||
const animMixer = setupAnimations(model, preloadedData.gltf, modelType);
|
||||
// Center and frame model
|
||||
centerAndFrameModel(model);
|
||||
console.log(`Model created successfully: ${modelType}`);
|
||||
return { model, animMixer };
|
||||
}
|
||||
|
||||
// Start/restart bold roughness animation with optional delay control
|
||||
function startBoldRoughnessAnimation(withDelay = true) {
|
||||
console.log('Starting/restarting bold roughness animation');
|
||||
// Reset all bold glass materials to starting roughness value
|
||||
boldRoughnessAnimation.materials.forEach(material => {
|
||||
material.roughness = boldRoughnessAnimation.startRoughness;
|
||||
material.needsUpdate = true;
|
||||
});
|
||||
boldRoughnessAnimation.isActive = true;
|
||||
boldRoughnessAnimation.startTime = performance.now();
|
||||
// Set delayDuration based on withDelay parameter
|
||||
boldRoughnessAnimation.delayDuration = withDelay ? 1.0 : 0.0;
|
||||
console.log('Bold roughness animation started with delay:', withDelay);
|
||||
}
|
||||
|
||||
// Initialize first scene after all models are loaded
|
||||
function initializeScene() {
|
||||
console.log('Initializing first scene (bold)');
|
||||
const { model, animMixer } = createModelFromPreloaded('bold');
|
||||
currentModel = model;
|
||||
mixer = animMixer;
|
||||
scene.add(currentModel);
|
||||
// Start the roughness animation for bold scene with delay
|
||||
startBoldRoughnessAnimation(true);
|
||||
console.log('Bold scene initialized');
|
||||
}
|
||||
|
||||
// Start innovation glass animation
|
||||
function startInnovationGlassAnimation() {
|
||||
console.log('Starting innovation glass animation');
|
||||
// Reset all innovation glass materials to starting values
|
||||
innovationGlassAnimation.materials.forEach(material => {
|
||||
material.ior = innovationGlassAnimation.startIor;
|
||||
material.thickness = innovationGlassAnimation.startThickness;
|
||||
material.needsUpdate = true;
|
||||
});
|
||||
innovationGlassAnimation.isActive = true;
|
||||
innovationGlassAnimation.startTime = performance.now();
|
||||
console.log('Innovation glass animation started');
|
||||
}
|
||||
|
||||
// Reset mesh geometry to original state
|
||||
function resetMeshGeometry(mesh) {
|
||||
if (!mesh || !mesh.geometry || !mesh.geometry.userData.originalPositions) {
|
||||
return;
|
||||
}
|
||||
const positions = mesh.geometry.attributes.position;
|
||||
const original = mesh.geometry.userData.originalPositions;
|
||||
for (let i = 0; i < positions.count; i++) {
|
||||
positions.setXYZ(i, original[i * 3], original[i * 3 + 1], original[i * 3 + 2]);
|
||||
}
|
||||
positions.needsUpdate = true;
|
||||
mesh.geometry.computeVertexNormals();
|
||||
}
|
||||
|
||||
// FIXED: Clean up geometry data completely
|
||||
function cleanupGeometryData(model) {
|
||||
if (!model) return;
|
||||
model.traverse((object) => {
|
||||
if (object.isMesh && object.geometry && object.geometry.userData) {
|
||||
delete object.geometry.userData.originalPositions;
|
||||
delete object.geometry.userData.originalWorldPositions;
|
||||
delete object.geometry.userData.inverseWorldMatrix;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Start transition to next or previous scene
|
||||
function startTransition(direction = 1) {
|
||||
if (isTransitioning) return;
|
||||
// Check bounds - now 4 scenes (0-3)
|
||||
if (direction > 0 && currentScene >= 3) return; // Can't go forward from storytelling
|
||||
if (direction < 0 && currentScene <= 0) return; // Can't go backward from bold
|
||||
console.log(`Starting diagonal transition: direction=${direction}, currentScene=${currentScene}`);
|
||||
// Calculate camera-relative diagonal transition vectors
|
||||
calculateTransitionVectors();
|
||||
isTransitioning = true;
|
||||
transitionStartTime = performance.now();
|
||||
transitionDirection = direction;
|
||||
// Determine next model based on direction and current scene
|
||||
let nextModelType = '';
|
||||
if (direction > 0) {
|
||||
// Moving forward
|
||||
if (currentScene === 0) {
|
||||
nextModelType = 'innovation';
|
||||
} else if (currentScene === 1) {
|
||||
nextModelType = 'agility';
|
||||
} else if (currentScene === 2) {
|
||||
nextModelType = 'storytelling';
|
||||
}
|
||||
} else {
|
||||
// Moving backward
|
||||
if (currentScene === 1) {
|
||||
nextModelType = 'bold';
|
||||
} else if (currentScene === 2) {
|
||||
nextModelType = 'innovation';
|
||||
} else if (currentScene === 3) {
|
||||
nextModelType = 'agility';
|
||||
}
|
||||
}
|
||||
console.log(`Next model type: ${nextModelType}`);
|
||||
if (nextModelType) {
|
||||
const { model, animMixer } = createModelFromPreloaded(nextModelType);
|
||||
nextModel = model;
|
||||
nextMixer = animMixer;
|
||||
// Position next model based on transition direction
|
||||
if (transitionDirection === 1) {
|
||||
// Forward: next model starts from diagonal down position (bottom-right)
|
||||
nextModel.position.copy(transitionDownVector);
|
||||
console.log(`Next model positioned at diagonal down vector (bottom-right): x=${nextModel.position.x}, y=${nextModel.position.y}, z=${nextModel.position.z}`);
|
||||
} else {
|
||||
// Backward: next model starts from diagonal up position (top-left)
|
||||
nextModel.position.copy(transitionUpVector);
|
||||
console.log(`Next model positioned at diagonal up vector (top-left): x=${nextModel.position.x}, y=${nextModel.position.y}, z=${nextModel.position.z}`);
|
||||
}
|
||||
// Add next model to scene without opacity changes - it will appear instantly when it enters the camera view
|
||||
scene.add(nextModel);
|
||||
}
|
||||
}
|
||||
|
||||
// Update transition animation
|
||||
function updateTransition(deltaTime) {
|
||||
if (!isTransitioning) return;
|
||||
const elapsed = (performance.now() - transitionStartTime) / 1000;
|
||||
const transitionProgress = Math.min(elapsed / transitionDuration, 1);
|
||||
// Smooth easing function (ease-in-out)
|
||||
const easeInOut = (t) => t * t * (3 - 2 * t);
|
||||
const easedProgress = easeInOut(transitionProgress);
|
||||
if (currentModel) {
|
||||
// Move current model along diagonal vector based on transition direction
|
||||
let moveVector;
|
||||
if (transitionDirection === 1) {
|
||||
// Forward: current model moves top-left
|
||||
moveVector = transitionUpVector.clone().multiplyScalar(easedProgress);
|
||||
console.log('Current model moving top-left (forward transition)');
|
||||
} else {
|
||||
// Backward: current model moves bottom-right
|
||||
moveVector = transitionDownVector.clone().multiplyScalar(easedProgress);
|
||||
console.log('Current model moving bottom-right (backward transition)');
|
||||
}
|
||||
currentModel.position.copy(moveVector);
|
||||
}
|
||||
if (nextModel) {
|
||||
// Move next model from diagonal vector to center based on transition direction
|
||||
let moveVector;
|
||||
if (transitionDirection === 1) {
|
||||
// Forward: next model moves from bottom-right to center
|
||||
moveVector = transitionDownVector.clone().multiplyScalar(1 - easedProgress);
|
||||
console.log('Next model moving from bottom-right to center (forward transition)');
|
||||
} else {
|
||||
// Backward: next model moves from top-left to center
|
||||
moveVector = transitionUpVector.clone().multiplyScalar(1 - easedProgress);
|
||||
console.log('Next model moving from top-left to center (backward transition)');
|
||||
}
|
||||
nextModel.position.copy(moveVector);
|
||||
}
|
||||
// Complete transition
|
||||
if (transitionProgress >= 1) {
|
||||
console.log('Diagonal transition animation complete');
|
||||
// FIXED: Reset geometry before removing the model
|
||||
if (currentModel) {
|
||||
// Reset all geometry to original state before removal
|
||||
currentModel.traverse((object) => {
|
||||
if (object.isMesh) {
|
||||
resetMeshGeometry(object);
|
||||
}
|
||||
});
|
||||
// Clean up geometry user data completely
|
||||
cleanupGeometryData(currentModel);
|
||||
scene.remove(currentModel);
|
||||
console.log('Previous model removed from scene');
|
||||
}
|
||||
// Switch to next model
|
||||
if (nextModel) {
|
||||
currentModel = nextModel;
|
||||
mixer = nextMixer;
|
||||
// Reset position to center
|
||||
currentModel.position.set(0, 0, 0);
|
||||
}
|
||||
nextModel = null;
|
||||
nextMixer = null;
|
||||
isTransitioning = false;
|
||||
currentScene += transitionDirection; // Update scene based on direction
|
||||
scrollDownCount = 0;
|
||||
scrollUpCount = 0;
|
||||
// Start animations based on current scene
|
||||
if (currentScene === 0) {
|
||||
// Restart bold roughness animation when returning to bold section WITHOUT delay
|
||||
startBoldRoughnessAnimation(false);
|
||||
} else if (currentScene === 1) {
|
||||
startInnovationGlassAnimation();
|
||||
}
|
||||
console.log(`Diagonal transition complete. Current scene: ${currentScene}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll event handler
|
||||
function onMouseScroll(event) {
|
||||
if (isTransitioning) return;
|
||||
if (event.deltaY > 0) {
|
||||
// Scrolling down - move forward
|
||||
scrollDownCount++;
|
||||
scrollUpCount = 0; // Reset up count
|
||||
console.log(`Scroll down count: ${scrollDownCount}`);
|
||||
if (scrollDownCount >= scrollThreshold) {
|
||||
startTransition(1); // Forward direction
|
||||
}
|
||||
} else if (event.deltaY < 0) {
|
||||
// Scrolling up - move backward
|
||||
scrollUpCount++;
|
||||
scrollDownCount = 0; // Reset down count
|
||||
console.log(`Scroll up count: ${scrollUpCount}`);
|
||||
if (scrollUpCount >= scrollThreshold) {
|
||||
startTransition(-1); // Backward direction
|
||||
}
|
||||
}
|
||||
console.log('Initializing first scene (bold)');
|
||||
const { model, animMixer } = createModelFromPreloaded('bold', preloadedModels, camera, controls);
|
||||
// Use setter functions instead of direct assignment
|
||||
setCurrentModel(model);
|
||||
setMixer(animMixer);
|
||||
scene.add(currentModel);
|
||||
// Start the roughness animation for bold scene with delay
|
||||
startBoldRoughnessAnimation(true);
|
||||
console.log('Bold scene initialized');
|
||||
}
|
||||
|
||||
// Animation loop
|
||||
const clock = new THREE.Clock();
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
const delta = clock.getDelta();
|
||||
// Update mixers
|
||||
if (mixer) mixer.update(delta);
|
||||
if (nextMixer) nextMixer.update(delta);
|
||||
// Update transition
|
||||
if (isTransitioning) {
|
||||
updateTransition(delta);
|
||||
}
|
||||
// Turntable rotation animation
|
||||
if (currentModel) {
|
||||
currentModel.rotation.y += turntableSpeed * delta;
|
||||
}
|
||||
if (nextModel) {
|
||||
nextModel.rotation.y += turntableSpeed * delta;
|
||||
}
|
||||
// Update bold roughness animation
|
||||
if (boldRoughnessAnimation.isActive) {
|
||||
const elapsed = (performance.now() - boldRoughnessAnimation.startTime) / 1000;
|
||||
if (elapsed >= boldRoughnessAnimation.delayDuration) {
|
||||
// Delay period is over, start roughness transition
|
||||
const transitionElapsed = elapsed - boldRoughnessAnimation.delayDuration;
|
||||
const transitionProgress = Math.min(transitionElapsed / boldRoughnessAnimation.transitionDuration, 1);
|
||||
// Smooth easing function (ease-in-out)
|
||||
const easeInOut = (t) => t * t * (3 - 2 * t);
|
||||
const easedProgress = easeInOut(transitionProgress);
|
||||
// Interpolate roughness from 0.5 to 0.05
|
||||
const currentRoughness = boldRoughnessAnimation.startRoughness +
|
||||
(boldRoughnessAnimation.endRoughness - boldRoughnessAnimation.startRoughness) * easedProgress;
|
||||
// Apply to all bold materials
|
||||
boldRoughnessAnimation.materials.forEach(material => {
|
||||
material.roughness = currentRoughness;
|
||||
material.needsUpdate = true;
|
||||
});
|
||||
// End animation when complete
|
||||
if (transitionProgress >= 1) {
|
||||
boldRoughnessAnimation.isActive = false;
|
||||
console.log('Bold roughness animation completed');
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update innovation glass animation
|
||||
if (innovationGlassAnimation.isActive) {
|
||||
const elapsed = (performance.now() - innovationGlassAnimation.startTime) / 1000;
|
||||
const transitionProgress = Math.min(elapsed / innovationGlassAnimation.transitionDuration, 1);
|
||||
// Smooth easing function (ease-in-out)
|
||||
const easeInOut = (t) => t * t * (3 - 2 * t);
|
||||
const easedProgress = easeInOut(transitionProgress);
|
||||
// Interpolate IOR from 1.0 to 2.0
|
||||
const currentIor = innovationGlassAnimation.startIor +
|
||||
(innovationGlassAnimation.endIor - innovationGlassAnimation.startIor) * easedProgress;
|
||||
// Interpolate thickness from 1.0 to 2.0
|
||||
const currentThickness = innovationGlassAnimation.startThickness +
|
||||
(innovationGlassAnimation.endThickness - innovationGlassAnimation.startThickness) * easedProgress;
|
||||
// Apply to all innovation glass materials
|
||||
innovationGlassAnimation.materials.forEach(material => {
|
||||
material.ior = currentIor;
|
||||
material.thickness = currentThickness;
|
||||
material.needsUpdate = true;
|
||||
});
|
||||
// End animation when complete
|
||||
if (transitionProgress >= 1) {
|
||||
innovationGlassAnimation.isActive = false;
|
||||
console.log('Innovation glass animation completed');
|
||||
}
|
||||
}
|
||||
controls.update();
|
||||
composer.render();
|
||||
requestAnimationFrame(animate);
|
||||
const delta = clock.getDelta();
|
||||
|
||||
// Update mixers
|
||||
if (mixer) mixer.update(delta);
|
||||
if (nextMixer) nextMixer.update(delta);
|
||||
|
||||
// Update transition
|
||||
if (isTransitioning) {
|
||||
updateTransition(delta, scene);
|
||||
}
|
||||
|
||||
// Turntable rotation animation
|
||||
if (currentModel) {
|
||||
currentModel.rotation.y += turntableSpeed * delta;
|
||||
}
|
||||
if (nextModel) {
|
||||
nextModel.rotation.y += turntableSpeed * delta;
|
||||
}
|
||||
|
||||
// Update animations
|
||||
updateBoldRoughnessAnimation();
|
||||
updateInnovationGlassAnimation();
|
||||
|
||||
controls.update();
|
||||
composer.render();
|
||||
}
|
||||
|
||||
// Initialize the scene
|
||||
async function init() {
|
||||
try {
|
||||
console.log('Starting application initialization');
|
||||
// Load all models first
|
||||
preloadedModels = await sceneLoader.loadAllModels();
|
||||
console.log('All models loaded successfully');
|
||||
// Initialize the first scene
|
||||
initializeScene();
|
||||
// Start the animation loop
|
||||
animate();
|
||||
console.log('Animation loop started');
|
||||
// Attach scroll event listener
|
||||
window.addEventListener('wheel', onMouseScroll, { passive: true });
|
||||
console.log('Scroll event listener attached');
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize scene:', error);
|
||||
sceneLoader.setLoadingMessage('Error loading experience. Please refresh.');
|
||||
}
|
||||
try {
|
||||
console.log('Starting application initialization');
|
||||
// Load all models first
|
||||
preloadedModels = await sceneLoader.loadAllModels();
|
||||
console.log('All models loaded successfully');
|
||||
// Initialize the first scene
|
||||
initializeScene();
|
||||
// Start the animation loop
|
||||
animate();
|
||||
console.log('Animation loop started');
|
||||
// Attach scroll event listener
|
||||
window.addEventListener('wheel', (event) => {
|
||||
onMouseScroll(event, preloadedModels, scene, camera, controls);
|
||||
}, { passive: true });
|
||||
console.log('Scroll event listener attached');
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize scene:', error);
|
||||
sceneLoader.setLoadingMessage('Error loading experience. Please refresh.');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener('resize', () => {
|
||||
console.log('Window resized');
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
composer.setSize(window.innerWidth, window.innerHeight);
|
||||
console.log('Window resized');
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
composer.setSize(window.innerWidth, window.innerHeight);
|
||||
});
|
||||
|
||||
// Start the application
|
||||
|
|
97
src/materialDefinitions.js
Normal file
97
src/materialDefinitions.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
import * as THREE from 'three';
|
||||
|
||||
// Video texture for emissive "screen"-like effect on orange material
|
||||
export const video = document.createElement('video');
|
||||
video.src = '/shader-flash.webm';
|
||||
video.muted = true;
|
||||
video.loop = true;
|
||||
video.playsInline = true;
|
||||
video.autoplay = true;
|
||||
video.preload = 'auto';
|
||||
|
||||
export const videoTexture = new THREE.VideoTexture(video);
|
||||
videoTexture.colorSpace = THREE.SRGBColorSpace;
|
||||
videoTexture.generateMipmaps = false;
|
||||
videoTexture.minFilter = THREE.LinearFilter;
|
||||
videoTexture.magFilter = THREE.LinearFilter;
|
||||
|
||||
// Ensure autoplay starts (muted autoplay is commonly allowed)
|
||||
video.play().catch(() => { });
|
||||
|
||||
// Bold glass material (starts rough, will transition to clear)
|
||||
export const boldGlassMaterial = new THREE.MeshPhysicalMaterial({
|
||||
color: 0xffffff,
|
||||
metalness: 0.2,
|
||||
roughness: 0.5,
|
||||
transmission: 1,
|
||||
ior: 2,
|
||||
thickness: 2,
|
||||
clearcoat: 1.0,
|
||||
clearcoatRoughness: 0.1,
|
||||
attenuationColor: new THREE.Color(0xffffff),
|
||||
attenuationDistance: 0.8,
|
||||
envMapIntensity: 0,
|
||||
specularIntensity: 1.0,
|
||||
specularColor: new THREE.Color(0xffffff),
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
alphaTest: 0
|
||||
});
|
||||
|
||||
// Orange wireframe material for bold Cubewire mesh
|
||||
export const boldWireframeMaterial = new THREE.MeshStandardMaterial({
|
||||
color: 0xff8600,
|
||||
metalness: 0.05,
|
||||
roughness: 0.5
|
||||
});
|
||||
|
||||
// Clear thick glass for innovation (starts with animated values)
|
||||
export const innovationGlassMaterial = new THREE.MeshPhysicalMaterial({
|
||||
color: 0xffffff,
|
||||
metalness: 0.2,
|
||||
roughness: 0.05,
|
||||
transmission: 1,
|
||||
ior: 1.0,
|
||||
thickness: 1.0,
|
||||
clearcoat: 1.0,
|
||||
clearcoatRoughness: 0.1,
|
||||
attenuationColor: new THREE.Color(0xffffff),
|
||||
attenuationDistance: 0.8,
|
||||
envMapIntensity: 0,
|
||||
specularIntensity: 1.0,
|
||||
specularColor: new THREE.Color(0x000000),
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
alphaTest: 0
|
||||
});
|
||||
|
||||
// Slightly frosted glass for agility and storytelling
|
||||
export const frostedGlassMaterial = new THREE.MeshPhysicalMaterial({
|
||||
color: 0xffffff,
|
||||
metalness: 0.0,
|
||||
roughness: 0.25,
|
||||
transmission: 1.0,
|
||||
ior: 1.5,
|
||||
thickness: 2.0,
|
||||
clearcoat: 0.75,
|
||||
clearcoatRoughness: 0.25,
|
||||
attenuationColor: new THREE.Color(0xffffff),
|
||||
attenuationDistance: 1.5,
|
||||
envMapIntensity: 1.25,
|
||||
specularIntensity: 1.0,
|
||||
specularColor: new THREE.Color(0xffffff),
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
// Orange material with video shader for innovation
|
||||
export const lightOrangeMaterial = new THREE.MeshStandardMaterial({
|
||||
color: 0xff8600,
|
||||
metalness: 0.05,
|
||||
roughness: 0.4,
|
||||
envMapIntensity: 0,
|
||||
emissive: new THREE.Color(0xffad47),
|
||||
emissiveMap: videoTexture,
|
||||
emissiveIntensity: 2.25
|
||||
});
|
229
src/modelManager.js
Normal file
229
src/modelManager.js
Normal file
|
@ -0,0 +1,229 @@
|
|||
import * as THREE from 'three';
|
||||
import {
|
||||
boldGlassMaterial,
|
||||
boldWireframeMaterial,
|
||||
innovationGlassMaterial,
|
||||
frostedGlassMaterial,
|
||||
lightOrangeMaterial
|
||||
} from './materialDefinitions.js';
|
||||
import { boldRoughnessAnimation, innovationGlassAnimation } from './animationManager.js';
|
||||
|
||||
// Apply materials based on model type
|
||||
export function applyMaterials(model, modelType) {
|
||||
console.log(`=== Material Assignment Debug for ${modelType} ===`);
|
||||
let meshCount = 0;
|
||||
model.traverse((object) => {
|
||||
if (object.isMesh) {
|
||||
meshCount++;
|
||||
console.log(`Found mesh: "${object.name}"`);
|
||||
const previousMaterial = object.material;
|
||||
object.castShadow = true;
|
||||
object.receiveShadow = true;
|
||||
|
||||
if (modelType === 'bold') {
|
||||
// Bold-specific material logic
|
||||
if (object.name === 'Cube') {
|
||||
console.log(` → Applying bold glass material to "${object.name}"`);
|
||||
object.material = boldGlassMaterial.clone();
|
||||
object.material.side = THREE.DoubleSide;
|
||||
object.material.depthWrite = false;
|
||||
object.renderOrder = 2;
|
||||
// Store material reference for roughness animation
|
||||
boldRoughnessAnimation.materials.push(object.material);
|
||||
} else if (object.name === 'Cubewire') {
|
||||
console.log(` → Applying wireframe material to "${object.name}"`);
|
||||
object.material = boldWireframeMaterial.clone();
|
||||
object.renderOrder = 1;
|
||||
} else {
|
||||
console.log(` → Applying bold glass material (fallback) to "${object.name}"`);
|
||||
object.material = boldGlassMaterial.clone();
|
||||
// Store material reference for roughness animation
|
||||
boldRoughnessAnimation.materials.push(object.material);
|
||||
}
|
||||
} else if (modelType === 'innovation') {
|
||||
// Innovation-specific material logic
|
||||
const orangeMeshes = ['dblsc', 'ec', 'gemini', 'infinity', 'star', 'dpd'];
|
||||
const targetGlassNames = ['Cube.alt90.df'];
|
||||
const sanitize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
const nameMatches = (name, targets) => {
|
||||
const clean = sanitize(name);
|
||||
return targets.some((t) => {
|
||||
const ct = sanitize(t);
|
||||
return clean === ct || clean.includes(ct) || ct.includes(clean);
|
||||
});
|
||||
};
|
||||
|
||||
if (nameMatches(object.name, targetGlassNames)) {
|
||||
// Create outer glass shell with innovation-specific material
|
||||
object.material = innovationGlassMaterial.clone();
|
||||
object.material.side = THREE.DoubleSide;
|
||||
object.material.depthWrite = false;
|
||||
object.renderOrder = 2;
|
||||
// Store material reference for animation
|
||||
innovationGlassAnimation.materials.push(object.material);
|
||||
// Create inner glass shell
|
||||
const innerShell = object.clone();
|
||||
innerShell.material = innovationGlassMaterial.clone();
|
||||
innerShell.material.side = THREE.DoubleSide;
|
||||
innerShell.material.depthWrite = false;
|
||||
innerShell.material.transmission = 0.8;
|
||||
innerShell.renderOrder = 1;
|
||||
innerShell.scale.multiplyScalar(0.95);
|
||||
// Store inner shell material reference for animation too
|
||||
innovationGlassAnimation.materials.push(innerShell.material);
|
||||
object.parent.add(innerShell);
|
||||
} else if (nameMatches(object.name, orangeMeshes)) {
|
||||
object.material = lightOrangeMaterial.clone();
|
||||
object.renderOrder = 0;
|
||||
}
|
||||
} else {
|
||||
// Agility and Storytelling use frosted glass material for all meshes
|
||||
if (object.name.startsWith('base')) {
|
||||
console.log(` → Applying frosted glass material to "${object.name}"`);
|
||||
object.material = frostedGlassMaterial.clone();
|
||||
} else {
|
||||
console.log(` → Applying frosted glass material (fallback) to "${object.name}"`);
|
||||
object.material = frostedGlassMaterial.clone();
|
||||
}
|
||||
}
|
||||
|
||||
object.material.needsUpdate = true;
|
||||
// Cleanup previous materials
|
||||
if (Array.isArray(previousMaterial)) {
|
||||
previousMaterial.forEach((mat) => mat && mat.dispose && mat.dispose());
|
||||
} else if (previousMaterial && previousMaterial.dispose) {
|
||||
previousMaterial.dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log(`Total meshes processed: ${meshCount}`);
|
||||
console.log(`=== End Material Assignment Debug for ${modelType} ===`);
|
||||
}
|
||||
|
||||
// Center and frame model with camera
|
||||
export function centerAndFrameModel(model, targetCamera, controls) {
|
||||
const box = new THREE.Box3().setFromObject(model);
|
||||
const center = box.getCenter(new THREE.Vector3());
|
||||
model.position.sub(center);
|
||||
model.updateMatrixWorld(true);
|
||||
|
||||
// Only set camera position if it's not already positioned (avoid reset during transitions)
|
||||
// Use fixed camera distance that's further away from the origin
|
||||
const fixedCameraDistance = 50; // Fixed distance, much further than before
|
||||
// Calculate isometric-like position with 35-degree angles
|
||||
const angle = 35 * Math.PI / 180; // Convert 35 degrees to radians
|
||||
const cosAngle = Math.cos(angle);
|
||||
const x = fixedCameraDistance * cosAngle;
|
||||
const y = fixedCameraDistance * cosAngle;
|
||||
const z = fixedCameraDistance * cosAngle;
|
||||
|
||||
targetCamera.position.set(x, y, z);
|
||||
controls.target.set(0, 0, 0);
|
||||
// Set distance limits to lock the camera at this distance
|
||||
controls.minDistance = fixedCameraDistance;
|
||||
controls.maxDistance = fixedCameraDistance;
|
||||
controls.update();
|
||||
|
||||
console.log(`Camera positioned at: x=${x}, y=${y}, z=${z}, distance=${fixedCameraDistance}`);
|
||||
}
|
||||
|
||||
// Setup animations based on model type
|
||||
export function setupAnimations(model, gltf, modelType) {
|
||||
if (gltf.animations && gltf.animations.length > 0) {
|
||||
const animMixer = new THREE.AnimationMixer(model);
|
||||
gltf.animations.forEach((clip) => {
|
||||
const action = animMixer.clipAction(clip);
|
||||
if (modelType === 'bold') {
|
||||
// Play once for bold
|
||||
action.loop = THREE.LoopOnce;
|
||||
action.clampWhenFinished = true;
|
||||
action.play();
|
||||
console.log(`Bold animation started: ${clip.name}`);
|
||||
} else if (modelType === 'innovation') {
|
||||
// PingPong loop for innovation
|
||||
action.loop = THREE.LoopPingPong;
|
||||
action.play();
|
||||
console.log(`Innovation animation started: ${clip.name} (PingPong)`);
|
||||
} else if (modelType === 'agility') {
|
||||
// Regular loop for agility
|
||||
action.loop = THREE.LoopRepeat;
|
||||
action.play();
|
||||
console.log(`Agility animation started: ${clip.name} (Loop)`);
|
||||
} else if (modelType === 'storytelling') {
|
||||
// Play once for storytelling
|
||||
action.loop = THREE.LoopOnce;
|
||||
action.clampWhenFinished = true;
|
||||
action.play();
|
||||
console.log(`Storytelling animation started: ${clip.name}`);
|
||||
}
|
||||
});
|
||||
if (modelType === 'innovation') {
|
||||
animMixer.timeScale = 3.0; // Keep existing timeScale for innovation
|
||||
console.log('Innovation animation timeScale set to 3.0');
|
||||
}
|
||||
return animMixer;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Reset mesh geometry to original state
|
||||
export function resetMeshGeometry(mesh) {
|
||||
if (!mesh || !mesh.geometry || !mesh.geometry.userData.originalPositions) {
|
||||
return;
|
||||
}
|
||||
const positions = mesh.geometry.attributes.position;
|
||||
const original = mesh.geometry.userData.originalPositions;
|
||||
for (let i = 0; i < positions.count; i++) {
|
||||
positions.setXYZ(i, original[i * 3], original[i * 3 + 1], original[i * 3 + 2]);
|
||||
}
|
||||
positions.needsUpdate = true;
|
||||
mesh.geometry.computeVertexNormals();
|
||||
}
|
||||
|
||||
// FIXED: Clean up geometry data completely
|
||||
export function cleanupGeometryData(model) {
|
||||
if (!model) return;
|
||||
model.traverse((object) => {
|
||||
if (object.isMesh && object.geometry && object.geometry.userData) {
|
||||
delete object.geometry.userData.originalPositions;
|
||||
delete object.geometry.userData.originalWorldPositions;
|
||||
delete object.geometry.userData.inverseWorldMatrix;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Create model from preloaded data - FIXED: Always create fresh geometry
|
||||
export function createModelFromPreloaded(modelType, preloadedModels, camera, controls) {
|
||||
const preloadedData = preloadedModels[modelType];
|
||||
if (!preloadedData) {
|
||||
console.error(`Preloaded model not found: ${modelType}`);
|
||||
return { model: null, animMixer: null };
|
||||
}
|
||||
|
||||
console.log(`Creating model from preloaded data: ${modelType}`);
|
||||
// Clear animation materials arrays when creating new models
|
||||
if (modelType === 'bold') {
|
||||
boldRoughnessAnimation.materials = [];
|
||||
} else if (modelType === 'innovation') {
|
||||
innovationGlassAnimation.materials = [];
|
||||
}
|
||||
|
||||
// Clone the scene deeply to ensure fresh geometry
|
||||
const model = preloadedData.scene.clone(true);
|
||||
// IMPORTANT: Clone all geometries to ensure they're independent
|
||||
model.traverse((object) => {
|
||||
if (object.isMesh && object.geometry) {
|
||||
object.geometry = object.geometry.clone();
|
||||
}
|
||||
});
|
||||
|
||||
// Apply materials
|
||||
applyMaterials(model, modelType);
|
||||
// Setup animations
|
||||
const animMixer = setupAnimations(model, preloadedData.gltf, modelType);
|
||||
// Center and frame model
|
||||
centerAndFrameModel(model, camera, controls);
|
||||
|
||||
console.log(`Model created successfully: ${modelType}`);
|
||||
return { model, animMixer };
|
||||
}
|
78
src/sceneLoader.js
Normal file
78
src/sceneLoader.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
import * as THREE from 'three';
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
|
||||
|
||||
export class SceneLoader {
|
||||
constructor() {
|
||||
this.loadingScreen = document.getElementById('loading-screen');
|
||||
this.loadingText = document.getElementById('loading-text');
|
||||
this.loadingProgressBar = document.getElementById('loading-progress-bar');
|
||||
this.loadingPercentage = document.getElementById('loading-percentage');
|
||||
this.modelsToLoad = [
|
||||
{ file: 'bold.glb', type: 'bold' },
|
||||
{ file: 'innovation.glb', type: 'innovation' },
|
||||
{ file: 'agility.glb', type: 'agility' },
|
||||
{ file: 'storytelling.glb', type: 'storytelling' }
|
||||
];
|
||||
this.loadedModels = {};
|
||||
this.loadedCount = 0;
|
||||
this.totalModels = this.modelsToLoad.length;
|
||||
}
|
||||
|
||||
setLoadingMessage(message) {
|
||||
this.loadingText.textContent = message;
|
||||
}
|
||||
|
||||
updateProgress(progress) {
|
||||
const percentage = Math.round(progress * 100);
|
||||
this.loadingProgressBar.style.width = `${percentage}%`;
|
||||
this.loadingPercentage.textContent = `${percentage}%`;
|
||||
}
|
||||
|
||||
hideLoadingScreen() {
|
||||
this.loadingScreen.classList.add('hidden');
|
||||
setTimeout(() => {
|
||||
this.loadingScreen.style.display = 'none';
|
||||
}, 800);
|
||||
}
|
||||
|
||||
async loadAllModels() {
|
||||
return new Promise((resolve) => {
|
||||
const loader = new GLTFLoader();
|
||||
const dracoLoader = new DRACOLoader();
|
||||
dracoLoader.setDecoderPath('node_modules/three/examples/jsm/libs/draco/');
|
||||
loader.setDRACOLoader(dracoLoader);
|
||||
|
||||
this.modelsToLoad.forEach((modelInfo, index) => {
|
||||
this.setLoadingMessage(`Loading experience...`);
|
||||
loader.load(`/${modelInfo.file}`,
|
||||
(gltf) => {
|
||||
this.loadedModels[modelInfo.type] = {
|
||||
scene: gltf.scene,
|
||||
animations: gltf.animations,
|
||||
gltf: gltf
|
||||
};
|
||||
this.loadedCount++;
|
||||
const progress = this.loadedCount / this.totalModels;
|
||||
this.updateProgress(progress);
|
||||
if (this.loadedCount === this.totalModels) {
|
||||
this.setLoadingMessage('Initializing Experience...');
|
||||
setTimeout(() => {
|
||||
this.hideLoadingScreen();
|
||||
resolve(this.loadedModels);
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
(progress) => {
|
||||
const fileProgress = progress.loaded / progress.total;
|
||||
const totalProgress = (this.loadedCount + fileProgress) / this.totalModels;
|
||||
this.updateProgress(totalProgress);
|
||||
},
|
||||
(error) => {
|
||||
console.error(`Error loading ${modelInfo.file}:`, error);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
108
src/sceneSetup.js
Normal file
108
src/sceneSetup.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
|
||||
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
||||
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
||||
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
||||
|
||||
export function createScene() {
|
||||
// Scene setup
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
camera.setFocalLength(50);
|
||||
const raycaster = new THREE.Raycaster();
|
||||
const mouse = new THREE.Vector2();
|
||||
|
||||
// Renderer setup
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
renderer.setClearColor(0x000000);
|
||||
renderer.shadowMap.enabled = true;
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
renderer.toneMappingExposure = 1.2;
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
renderer.physicallyCorrectLights = true;
|
||||
document.body.appendChild(renderer.domElement);
|
||||
|
||||
// Post-processing: Bloom
|
||||
const composer = new EffectComposer(renderer);
|
||||
const renderPass = new RenderPass(scene, camera);
|
||||
composer.addPass(renderPass);
|
||||
|
||||
const bloomPass = new UnrealBloomPass(
|
||||
new THREE.Vector2(window.innerWidth, window.innerHeight),
|
||||
1.0, // strength
|
||||
0.45, // radius
|
||||
0.85 // threshold
|
||||
);
|
||||
composer.addPass(bloomPass);
|
||||
|
||||
// Local procedural environment for better PBR response (no network)
|
||||
const pmrem = new THREE.PMREMGenerator(renderer);
|
||||
const roomEnv = new RoomEnvironment();
|
||||
scene.environment = pmrem.fromScene(roomEnv).texture;
|
||||
pmrem.dispose();
|
||||
roomEnv.dispose();
|
||||
scene.environment = null; // This will make the renderer's clear color visible again
|
||||
|
||||
return { scene, camera, renderer, composer, raycaster, mouse };
|
||||
}
|
||||
|
||||
export function setupLighting(scene, camera) {
|
||||
// Consistent Lighting Setup
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
||||
scene.add(ambientLight);
|
||||
|
||||
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x666666, 1.5);
|
||||
hemiLight.position.set(0, 20, 0);
|
||||
scene.add(hemiLight);
|
||||
|
||||
const fillLight = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||
fillLight.position.set(-12, 6, -8);
|
||||
scene.add(fillLight);
|
||||
|
||||
const topLight = new THREE.DirectionalLight(0xffffff, 1.5);
|
||||
topLight.position.set(5, 15, 5);
|
||||
scene.add(topLight);
|
||||
|
||||
const bottomLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||
bottomLight.position.set(-3, -8, 3);
|
||||
scene.add(bottomLight);
|
||||
|
||||
const leftLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
||||
leftLight.position.set(-12, 2, 5);
|
||||
scene.add(leftLight);
|
||||
|
||||
const rightLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
||||
rightLight.position.set(12, 2, -5);
|
||||
scene.add(rightLight);
|
||||
|
||||
const frontLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||
frontLight.position.set(8, 4, 12);
|
||||
scene.add(frontLight);
|
||||
|
||||
const backLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||
backLight.position.set(-8, 4, -12);
|
||||
scene.add(backLight);
|
||||
|
||||
const cameraLight = new THREE.PointLight(0xffffff, 0.8, 0, 2);
|
||||
camera.add(cameraLight);
|
||||
scene.add(camera);
|
||||
}
|
||||
|
||||
export function setupControls(camera, renderer) {
|
||||
// Controls with zoom disabled and camera constraints
|
||||
const controls = new OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.25;
|
||||
controls.enableZoom = false; // Disable zoom
|
||||
|
||||
// Add camera constraints to prevent extreme angles
|
||||
controls.maxPolarAngle = Math.PI * 0.8; // Prevent looking too far up
|
||||
controls.minPolarAngle = Math.PI * 0.2; // Prevent looking too far down
|
||||
|
||||
console.log('Orbit controls initialized with camera constraints');
|
||||
return controls;
|
||||
}
|
239
src/transitionManager.js
Normal file
239
src/transitionManager.js
Normal file
|
@ -0,0 +1,239 @@
|
|||
import * as THREE from 'three';
|
||||
import { createModelFromPreloaded, resetMeshGeometry, cleanupGeometryData } from './modelManager.js';
|
||||
import { startBoldRoughnessAnimation, startInnovationGlassAnimation } from './animationManager.js';
|
||||
|
||||
// Transition state management
|
||||
export let currentScene = 0; // 0: bold, 1: innovation, 2: agility, 3: storytelling
|
||||
export let isTransitioning = false;
|
||||
export const fadeSpeed = 1; // Easily adjustable fade speed
|
||||
export const transitionDuration = 1; // Easily adjustable transition duration (seconds)
|
||||
export let scrollDownCount = 0;
|
||||
export let scrollUpCount = 0;
|
||||
export const scrollThreshold = 10; // Changed to 10 as requested
|
||||
export let transitionStartTime = 0;
|
||||
export let transitionDirection = 1; // 1 for forward, -1 for backward
|
||||
|
||||
// Camera-relative transition vectors
|
||||
export let transitionUpVector = new THREE.Vector3();
|
||||
export let transitionDownVector = new THREE.Vector3();
|
||||
export const transitionDistance = 50; // Increased distance for more dramatic transitions
|
||||
|
||||
// Scene objects
|
||||
export let currentModel = null;
|
||||
export let nextModel = null;
|
||||
export let mixer = null;
|
||||
export let nextMixer = null;
|
||||
export let autoRotationAngle = 0;
|
||||
|
||||
// Setter functions to modify exported variables safely
|
||||
export function setCurrentModel(model) {
|
||||
currentModel = model;
|
||||
}
|
||||
|
||||
export function setMixer(animMixer) {
|
||||
mixer = animMixer;
|
||||
}
|
||||
|
||||
export function setNextModel(model) {
|
||||
nextModel = model;
|
||||
}
|
||||
|
||||
export function setNextMixer(animMixer) {
|
||||
nextMixer = animMixer;
|
||||
}
|
||||
|
||||
// Calculate camera-relative transition vectors for diagonal movement
|
||||
export function calculateTransitionVectors(camera) {
|
||||
// Get camera's world direction
|
||||
const cameraDirection = new THREE.Vector3();
|
||||
camera.getWorldDirection(cameraDirection);
|
||||
// Get world up vector
|
||||
const worldUp = new THREE.Vector3(0, 1, 0);
|
||||
// Calculate camera's left vector - BACK TO ORIGINAL (this gave correct left direction)
|
||||
const cameraLeft = new THREE.Vector3();
|
||||
cameraLeft.crossVectors(worldUp, cameraDirection).normalize();
|
||||
// Calculate camera's local up vector
|
||||
const cameraUp = new THREE.Vector3();
|
||||
cameraUp.crossVectors(cameraLeft, cameraDirection).normalize();
|
||||
// Blend camera up with world up - BUT NEGATE to flip up/down direction
|
||||
const blendedUp = new THREE.Vector3();
|
||||
blendedUp.addVectors(
|
||||
cameraUp.clone().multiplyScalar(0.5),
|
||||
worldUp.clone().multiplyScalar(0.5)
|
||||
).normalize().negate(); // ADD .negate() here to flip up to down
|
||||
// Create diagonal vector (up-left)
|
||||
const diagonalUpLeft = new THREE.Vector3();
|
||||
diagonalUpLeft.addVectors(
|
||||
blendedUp.clone().multiplyScalar(0.5),
|
||||
cameraLeft.clone().multiplyScalar(0.5)
|
||||
).normalize();
|
||||
// Set transition vectors
|
||||
transitionUpVector = diagonalUpLeft.clone().multiplyScalar(transitionDistance);
|
||||
transitionDownVector = diagonalUpLeft.clone().multiplyScalar(-transitionDistance);
|
||||
console.log('Diagonal transition vectors calculated with distance:', transitionDistance);
|
||||
}
|
||||
|
||||
// Start transition to next or previous scene
|
||||
export function startTransition(direction = 1, preloadedModels, scene, camera, controls) {
|
||||
if (isTransitioning) return;
|
||||
// Check bounds - now 4 scenes (0-3)
|
||||
if (direction > 0 && currentScene >= 3) return; // Can't go forward from storytelling
|
||||
if (direction < 0 && currentScene <= 0) return; // Can't go backward from bold
|
||||
|
||||
console.log(`Starting diagonal transition: direction=${direction}, currentScene=${currentScene}`);
|
||||
// Calculate camera-relative diagonal transition vectors
|
||||
calculateTransitionVectors(camera);
|
||||
|
||||
isTransitioning = true;
|
||||
transitionStartTime = performance.now();
|
||||
transitionDirection = direction;
|
||||
|
||||
// Determine next model based on direction and current scene
|
||||
let nextModelType = '';
|
||||
if (direction > 0) {
|
||||
// Moving forward
|
||||
if (currentScene === 0) {
|
||||
nextModelType = 'innovation';
|
||||
} else if (currentScene === 1) {
|
||||
nextModelType = 'agility';
|
||||
} else if (currentScene === 2) {
|
||||
nextModelType = 'storytelling';
|
||||
}
|
||||
} else {
|
||||
// Moving backward
|
||||
if (currentScene === 1) {
|
||||
nextModelType = 'bold';
|
||||
} else if (currentScene === 2) {
|
||||
nextModelType = 'innovation';
|
||||
} else if (currentScene === 3) {
|
||||
nextModelType = 'agility';
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Next model type: ${nextModelType}`);
|
||||
if (nextModelType) {
|
||||
const { model, animMixer } = createModelFromPreloaded(nextModelType, preloadedModels, camera, controls);
|
||||
nextModel = model;
|
||||
nextMixer = animMixer;
|
||||
|
||||
// Position next model based on transition direction
|
||||
if (transitionDirection === 1) {
|
||||
// Forward: next model starts from diagonal down position (bottom-right)
|
||||
nextModel.position.copy(transitionDownVector);
|
||||
console.log(`Next model positioned at diagonal down vector (bottom-right): x=${nextModel.position.x}, y=${nextModel.position.y}, z=${nextModel.position.z}`);
|
||||
} else {
|
||||
// Backward: next model starts from diagonal up position (top-left)
|
||||
nextModel.position.copy(transitionUpVector);
|
||||
console.log(`Next model positioned at diagonal up vector (top-left): x=${nextModel.position.x}, y=${nextModel.position.y}, z=${nextModel.position.z}`);
|
||||
}
|
||||
// Add next model to scene without opacity changes - it will appear instantly when it enters the camera view
|
||||
scene.add(nextModel);
|
||||
}
|
||||
}
|
||||
|
||||
// Update transition animation
|
||||
export function updateTransition(deltaTime, scene) {
|
||||
if (!isTransitioning) return;
|
||||
|
||||
const elapsed = (performance.now() - transitionStartTime) / 1000;
|
||||
const transitionProgress = Math.min(elapsed / transitionDuration, 1);
|
||||
// Smooth easing function (ease-in-out)
|
||||
const easeInOut = (t) => t * t * (3 - 2 * t);
|
||||
const easedProgress = easeInOut(transitionProgress);
|
||||
|
||||
if (currentModel) {
|
||||
// Move current model along diagonal vector based on transition direction
|
||||
let moveVector;
|
||||
if (transitionDirection === 1) {
|
||||
// Forward: current model moves top-left
|
||||
moveVector = transitionUpVector.clone().multiplyScalar(easedProgress);
|
||||
console.log('Current model moving top-left (forward transition)');
|
||||
} else {
|
||||
// Backward: current model moves bottom-right
|
||||
moveVector = transitionDownVector.clone().multiplyScalar(easedProgress);
|
||||
console.log('Current model moving bottom-right (backward transition)');
|
||||
}
|
||||
currentModel.position.copy(moveVector);
|
||||
}
|
||||
|
||||
if (nextModel) {
|
||||
// Move next model from diagonal vector to center based on transition direction
|
||||
let moveVector;
|
||||
if (transitionDirection === 1) {
|
||||
// Forward: next model moves from bottom-right to center
|
||||
moveVector = transitionDownVector.clone().multiplyScalar(1 - easedProgress);
|
||||
console.log('Next model moving from bottom-right to center (forward transition)');
|
||||
} else {
|
||||
// Backward: next model moves from top-left to center
|
||||
moveVector = transitionUpVector.clone().multiplyScalar(1 - easedProgress);
|
||||
console.log('Next model moving from top-left to center (backward transition)');
|
||||
}
|
||||
nextModel.position.copy(moveVector);
|
||||
}
|
||||
|
||||
// Complete transition
|
||||
if (transitionProgress >= 1) {
|
||||
console.log('Diagonal transition animation complete');
|
||||
// FIXED: Reset geometry before removing the model
|
||||
if (currentModel) {
|
||||
// Reset all geometry to original state before removal
|
||||
currentModel.traverse((object) => {
|
||||
if (object.isMesh) {
|
||||
resetMeshGeometry(object);
|
||||
}
|
||||
});
|
||||
// Clean up geometry user data completely
|
||||
cleanupGeometryData(currentModel);
|
||||
scene.remove(currentModel);
|
||||
console.log('Previous model removed from scene');
|
||||
}
|
||||
|
||||
// Switch to next model
|
||||
if (nextModel) {
|
||||
currentModel = nextModel;
|
||||
mixer = nextMixer;
|
||||
// Reset position to center
|
||||
currentModel.position.set(0, 0, 0);
|
||||
}
|
||||
|
||||
nextModel = null;
|
||||
nextMixer = null;
|
||||
isTransitioning = false;
|
||||
currentScene += transitionDirection; // Update scene based on direction
|
||||
scrollDownCount = 0;
|
||||
scrollUpCount = 0;
|
||||
|
||||
// Start animations based on current scene
|
||||
if (currentScene === 0) {
|
||||
// Restart bold roughness animation when returning to bold section WITHOUT delay
|
||||
startBoldRoughnessAnimation(false);
|
||||
} else if (currentScene === 1) {
|
||||
startInnovationGlassAnimation();
|
||||
}
|
||||
|
||||
console.log(`Diagonal transition complete. Current scene: ${currentScene}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll event handler
|
||||
export function onMouseScroll(event, preloadedModels, scene, camera, controls) {
|
||||
if (isTransitioning) return;
|
||||
|
||||
if (event.deltaY > 0) {
|
||||
// Scrolling down - move forward
|
||||
scrollDownCount++;
|
||||
scrollUpCount = 0; // Reset up count
|
||||
console.log(`Scroll down count: ${scrollDownCount}`);
|
||||
if (scrollDownCount >= scrollThreshold) {
|
||||
startTransition(1, preloadedModels, scene, camera, controls); // Forward direction
|
||||
}
|
||||
} else if (event.deltaY < 0) {
|
||||
// Scrolling up - move backward
|
||||
scrollUpCount++;
|
||||
scrollDownCount = 0; // Reset down count
|
||||
console.log(`Scroll up count: ${scrollUpCount}`);
|
||||
if (scrollUpCount >= scrollThreshold) {
|
||||
startTransition(-1, preloadedModels, scene, camera, controls); // Backward direction
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue