diff --git a/src/animationManager.js b/src/animationManager.js new file mode 100644 index 0000000..32e7ef0 --- /dev/null +++ b/src/animationManager.js @@ -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'); + } + } + } + \ No newline at end of file diff --git a/src/main.js b/src/main.js index aa19f52..3a956e0 100644 --- a/src/main.js +++ b/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 diff --git a/src/materialDefinitions.js b/src/materialDefinitions.js new file mode 100644 index 0000000..87daa4c --- /dev/null +++ b/src/materialDefinitions.js @@ -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 +}); diff --git a/src/modelManager.js b/src/modelManager.js new file mode 100644 index 0000000..6ef3118 --- /dev/null +++ b/src/modelManager.js @@ -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 }; +} diff --git a/src/sceneLoader.js b/src/sceneLoader.js new file mode 100644 index 0000000..f713be2 --- /dev/null +++ b/src/sceneLoader.js @@ -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); + } + ); + }); + }); + } +} diff --git a/src/sceneSetup.js b/src/sceneSetup.js new file mode 100644 index 0000000..14722ed --- /dev/null +++ b/src/sceneSetup.js @@ -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; +} diff --git a/src/transitionManager.js b/src/transitionManager.js new file mode 100644 index 0000000..049a5a6 --- /dev/null +++ b/src/transitionManager.js @@ -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 + } + } +}