diff --git a/index.html b/index.html index 7e248b6..187de6e 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,19 @@ Young Pandas + +
+
+
+
Loading Experience...
+
+
+
+
0%
+
+
+
- + - \ No newline at end of file + diff --git a/src/main.js b/src/main.js index 2572244..a53f816 100644 --- a/src/main.js +++ b/src/main.js @@ -9,6 +9,95 @@ 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 ${modelInfo.type}...`); + 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) => { + // Individual file 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); + } + ); + }); + }); + } +} + +// Initialize loader +const sceneLoader = new SceneLoader(); + +// You can customize the loading message here: +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); @@ -39,20 +128,34 @@ let mixer = null; let nextMixer = null; let autoRotationAngle = 0; +// Store preloaded models +let preloadedModels = {}; + // Bold scene roughness animation state let boldRoughnessAnimation = { - isActive: false, - startTime: 0, - delayDuration: 1.0, // 1 second delay - transitionDuration: 1.0, // 1 second transition - startRoughness: 0.25, - endRoughness: 0.05, - materials: [] // Store references to bold materials + isActive: false, + startTime: 0, + delayDuration: 1.0, // 1 second delay + transitionDuration: 1.0, // 1 second transition + startRoughness: 0.25, + 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); @@ -62,7 +165,6 @@ renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.2; renderer.outputColorSpace = THREE.SRGBColorSpace; renderer.physicallyCorrectLights = true; - document.body.appendChild(renderer.domElement); // Post-processing: Bloom @@ -71,10 +173,10 @@ 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 + new THREE.Vector2(window.innerWidth, window.innerHeight), + 1.0, // strength + 0.45, // radius + 0.85 // threshold ); composer.addPass(bloomPass); @@ -153,640 +255,734 @@ controls.enableZoom = false; // Disable zoom // Material definitions // Bold glass material (starts rough, will transition to clear) const boldGlassMaterial = new THREE.MeshPhysicalMaterial({ - color: 0xFFA500, - metalness: 0.2, - roughness: 0.25, // 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 + color: 0xFFA500, + metalness: 0.2, + roughness: 0.25, // 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 }); -// Clear thick glass for innovation +// 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: 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(0x000000), - transparent: true, - depthWrite: false, - alphaTest: 0 + 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 + 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 + color: 0xff8600, + metalness: 0.05, + roughness: 0.4, + envMapIntensity: 0, + emissive: new THREE.Color(0xffad47), + emissiveMap: videoTexture, + emissiveIntensity: 2.25 }); -const loader = new GLTFLoader(); -const dracoLoader = new DRACOLoader(); -dracoLoader.setDecoderPath('node_modules/three/examples/jsm/libs/draco/'); -loader.setDRACOLoader(dracoLoader); - // Apply materials based on model type function applyMaterials(model, modelType) { - console.log(`=== Material Assignment Debug for ${modelType} ===`); - let meshCount = 0; + console.log(`=== Material Assignment Debug for ${modelType} ===`); + let meshCount = 0; - model.traverse((object) => { - if (object.isMesh) { - meshCount++; - console.log(`Found mesh: "${object.name}"`); + model.traverse((object) => { + if (object.isMesh) { + meshCount++; + console.log(`Found mesh: "${object.name}"`); + const previousMaterial = object.material; + object.castShadow = true; + object.receiveShadow = true; - const previousMaterial = object.material; - object.castShadow = true; - object.receiveShadow = true; - - if (modelType === 'bold') { - // Bold-specific material logic - apply bold glass material to Cube mesh - 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 { - 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; - - // Create inner glass shell - const innerShell = object.clone(); - innerShell.material = innovationGlassMaterial.clone(); - innerShell.material.side = THREE.DoubleSide; - innerShell.material.depthWrite = false; - innerShell.material.thickness = 4; - innerShell.material.transmission = 0.8; - innerShell.renderOrder = 1; - innerShell.scale.multiplyScalar(0.95); - 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(); - } + if (modelType === 'bold') { + // Bold-specific material logic - apply bold glass material to Cube mesh + 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 { + 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']; - console.log(`Total meshes processed: ${meshCount}`); - console.log(`=== End Material Assignment Debug for ${modelType} ===`); + 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); + 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 - targetCamera.position.set(0, 0, fixedCameraDistance); - controls.target.set(0, 0, 0); - - // Set distance limits to lock the camera at this distance - controls.minDistance = fixedCameraDistance; - controls.maxDistance = fixedCameraDistance; - - controls.update(); - } + // 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(); + } } // Setup animations based on model type function setupAnimations(model, gltf, modelType) { - if (gltf.animations && gltf.animations.length > 0) { - const animMixer = new THREE.AnimationMixer(model); + 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(); - } else if (modelType === 'innovation') { - // PingPong loop for innovation - action.loop = THREE.LoopPingPong; - action.play(); - } else if (modelType === 'agility') { - // Regular loop for agility - action.loop = THREE.LoopRepeat; - action.play(); - } else if (modelType === 'storytelling') { - // Play once for storytelling - action.loop = THREE.LoopOnce; - action.clampWhenFinished = true; - action.play(); - } - }); - - if (modelType === 'innovation') { - animMixer.timeScale = 3.0; // Keep existing timeScale for innovation - } - - return animMixer; - } - return null; -} - -// Load model function -function loadModel(filename, modelType, onLoadCallback) { - loader.load(`/${filename}`, (gltf) => { - const model = gltf.scene; - - // Apply materials - applyMaterials(model, modelType); - - // Setup animations - const animMixer = setupAnimations(model, gltf, modelType); - - // Center and frame model - centerAndFrameModel(model); - - if (onLoadCallback) { - onLoadCallback(model, animMixer); - } - }, undefined, (error) => { - console.error(`Error loading ${filename}:`, error); + 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(); + } else if (modelType === 'innovation') { + // PingPong loop for innovation + action.loop = THREE.LoopPingPong; + action.play(); + } else if (modelType === 'agility') { + // Regular loop for agility + action.loop = THREE.LoopRepeat; + action.play(); + } else if (modelType === 'storytelling') { + // Play once for storytelling + action.loop = THREE.LoopOnce; + action.clampWhenFinished = true; + action.play(); + } }); + + if (modelType === 'innovation') { + animMixer.timeScale = 3.0; // Keep existing timeScale for innovation + } + + return animMixer; + } + + return null; } -// Load initial bold model (now the first scene) -loadModel('bold.glb', 'bold', (model, animMixer) => { - currentModel = model; - mixer = animMixer; - scene.add(currentModel); - - // Start the roughness animation for bold scene - boldRoughnessAnimation.isActive = true; - boldRoughnessAnimation.startTime = performance.now(); -}); +// 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 }; + } + + // 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(); + // Clear any previous twist data + delete object.geometry.userData.originalPositions; + delete object.geometry.userData.originalWorldPositions; + delete object.geometry.userData.inverseWorldMatrix; + } + }); + + // Apply materials + applyMaterials(model, modelType); + + // Setup animations + const animMixer = setupAnimations(model, preloadedData.gltf, modelType); + + // Center and frame model + centerAndFrameModel(model); + + return { model, animMixer }; +} + +// Initialize first scene after all models are loaded +function initializeScene() { + const { model, animMixer } = createModelFromPreloaded('bold'); + currentModel = model; + mixer = animMixer; + scene.add(currentModel); + + // Start the roughness animation for bold scene + boldRoughnessAnimation.isActive = true; + boldRoughnessAnimation.startTime = performance.now(); +} + +// Start innovation glass animation +function startInnovationGlassAnimation() { + // 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'); +} // Twist animation function - Updated to twist around world center (0,0,0) function twistMesh(mesh, progress) { - if (!mesh || !mesh.geometry || !mesh.geometry.attributes.position) { - return; - } + if (!mesh || !mesh.geometry || !mesh.geometry.attributes.position) { + return; + } - const positions = mesh.geometry.attributes.position; - - // Store original positions on the first run - if (!mesh.geometry.userData.originalPositions) { - mesh.geometry.userData.originalPositions = new Float32Array(positions.array); - - // Store original world positions for each vertex - mesh.geometry.userData.originalWorldPositions = []; - - // Update world matrix to get accurate world positions - mesh.updateMatrixWorld(true); - - const tempVector = new THREE.Vector3(); - for (let i = 0; i < positions.count; i++) { - tempVector.fromBufferAttribute(positions, i); - tempVector.applyMatrix4(mesh.matrixWorld); - mesh.geometry.userData.originalWorldPositions.push({ - x: tempVector.x, - y: tempVector.y, - z: tempVector.z - }); - } - - // Store the inverse of the current world matrix for transforming back to local space - mesh.geometry.userData.inverseWorldMatrix = mesh.matrixWorld.clone().invert(); - } - - const originalWorldPositions = mesh.geometry.userData.originalWorldPositions; - const inverseWorldMatrix = mesh.geometry.userData.inverseWorldMatrix; + const positions = mesh.geometry.attributes.position; + // Store original positions on the first run + if (!mesh.geometry.userData.originalPositions) { + mesh.geometry.userData.originalPositions = new Float32Array(positions.array); + + // Store original world positions for each vertex + mesh.geometry.userData.originalWorldPositions = []; + + // Update world matrix to get accurate world positions + mesh.updateMatrixWorld(true); + const tempVector = new THREE.Vector3(); + for (let i = 0; i < positions.count; i++) { - const worldPos = originalWorldPositions[i]; - - // Use world Y position for consistent twisting around world Y-axis - const worldY = worldPos.y; - - // Calculate twist angle based on world Y position - // Normalize Y based on a reasonable range (adjust as needed) - const normalizedY = (worldY + 5) / 10; // Assuming meshes are roughly within -5 to +5 world units in Y - const twistAngle = normalizedY * progress * twistStrength * 2 * Math.PI; - - // Apply twist in world coordinates around world Y-axis - const twistedWorldX = worldPos.x * Math.cos(twistAngle) - worldPos.z * Math.sin(twistAngle); - const twistedWorldY = worldPos.y; // Y remains unchanged - const twistedWorldZ = worldPos.x * Math.sin(twistAngle) + worldPos.z * Math.cos(twistAngle); - - // Convert twisted world position back to local coordinates - const twistedWorldVector = new THREE.Vector3(twistedWorldX, twistedWorldY, twistedWorldZ); - twistedWorldVector.applyMatrix4(inverseWorldMatrix); - - positions.setXYZ(i, twistedWorldVector.x, twistedWorldVector.y, twistedWorldVector.z); + tempVector.fromBufferAttribute(positions, i); + tempVector.applyMatrix4(mesh.matrixWorld); + mesh.geometry.userData.originalWorldPositions.push({ + x: tempVector.x, + y: tempVector.y, + z: tempVector.z + }); } + + // Store the inverse of the current world matrix for transforming back to local space + mesh.geometry.userData.inverseWorldMatrix = mesh.matrixWorld.clone().invert(); + } - positions.needsUpdate = true; - mesh.geometry.computeVertexNormals(); + const originalWorldPositions = mesh.geometry.userData.originalWorldPositions; + const inverseWorldMatrix = mesh.geometry.userData.inverseWorldMatrix; + + for (let i = 0; i < positions.count; i++) { + const worldPos = originalWorldPositions[i]; + + // Use world Y position for consistent twisting around world Y-axis + const worldY = worldPos.y; + + // Calculate twist angle based on world Y position + // Normalize Y based on a reasonable range (adjust as needed) + const normalizedY = (worldY + 5) / 10; // Assuming meshes are roughly within -5 to +5 world units in Y + const twistAngle = normalizedY * progress * twistStrength * 2 * Math.PI; + + // Apply twist in world coordinates around world Y-axis + const twistedWorldX = worldPos.x * Math.cos(twistAngle) - worldPos.z * Math.sin(twistAngle); + const twistedWorldY = worldPos.y; // Y remains unchanged + const twistedWorldZ = worldPos.x * Math.sin(twistAngle) + worldPos.z * Math.cos(twistAngle); + + // Convert twisted world position back to local coordinates + const twistedWorldVector = new THREE.Vector3(twistedWorldX, twistedWorldY, twistedWorldZ); + twistedWorldVector.applyMatrix4(inverseWorldMatrix); + + positions.setXYZ(i, twistedWorldVector.x, twistedWorldVector.y, twistedWorldVector.z); + } + + positions.needsUpdate = true; + mesh.geometry.computeVertexNormals(); } // Reset mesh geometry to original state function resetMeshGeometry(mesh) { - if (!mesh || !mesh.geometry || !mesh.geometry.userData.originalPositions) { - return; + 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; } - - 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(); + }); } // 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 + if (isTransitioning) return; - isTransitioning = true; - isTwisting = true; - twistProgress = 0; - transitionStartTime = performance.now(); - transitionDirection = direction; + // 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 - // Determine next model based on direction and current scene - let nextModelFile = ''; - let nextModelType = ''; - - if (direction > 0) { - // Moving forward - if (currentScene === 0) { - nextModelFile = 'innovation.glb'; - nextModelType = 'innovation'; - } else if (currentScene === 1) { - nextModelFile = 'agility.glb'; - nextModelType = 'agility'; - } else if (currentScene === 2) { - nextModelFile = 'storytelling.glb'; - nextModelType = 'storytelling'; - } - } else { - // Moving backward - if (currentScene === 1) { - nextModelFile = 'bold.glb'; - nextModelType = 'bold'; - } else if (currentScene === 2) { - nextModelFile = 'innovation.glb'; - nextModelType = 'innovation'; - } else if (currentScene === 3) { - nextModelFile = 'agility.glb'; - nextModelType = 'agility'; - } + isTransitioning = true; + isTwisting = true; + twistProgress = 0; + 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'; } - - if (nextModelFile) { - loadModel(nextModelFile, nextModelType, (model, animMixer) => { - nextModel = model; - nextMixer = animMixer; - - // Start next model as invisible but in normal position (no vertical offset) - nextModel.position.y = 0; - nextModel.traverse((obj) => { - if (obj.material) { - if (Array.isArray(obj.material)) { - obj.material.forEach(mat => { - mat.transparent = true; - mat.opacity = 0; - }); - } else { - obj.material.transparent = true; - obj.material.opacity = 0; - } - } - }); - - scene.add(nextModel); - }); + } else { + // Moving backward + if (currentScene === 1) { + nextModelType = 'bold'; + } else if (currentScene === 2) { + nextModelType = 'innovation'; + } else if (currentScene === 3) { + nextModelType = 'agility'; } + } + + if (nextModelType) { + const { model, animMixer } = createModelFromPreloaded(nextModelType); + nextModel = model; + nextMixer = animMixer; + + // Start next model as invisible but in normal position (no vertical offset) + nextModel.position.y = 0; + nextModel.traverse((obj) => { + if (obj.material) { + if (Array.isArray(obj.material)) { + obj.material.forEach(mat => { + mat.transparent = true; + mat.opacity = 0; + }); + } else { + obj.material.transparent = true; + obj.material.opacity = 0; + } + } + }); + scene.add(nextModel); + } } // Update transition animation function updateTransition(deltaTime) { - if (!isTransitioning) return; + 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); + 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 up and fade out + // currentModel.position.y = easedProgress * 10; + currentModel.traverse((obj) => { + if (obj.material) { + const targetOpacity = 1 - easedProgress; + if (Array.isArray(obj.material)) { + obj.material.forEach(mat => { + mat.transparent = true; + mat.opacity = targetOpacity; + }); + } else { + obj.material.transparent = true; + obj.material.opacity = targetOpacity; + } + } + }); + } + + if (nextModel) { + // Keep next model in place and just fade in (no vertical movement) + nextModel.position.y = 0; + nextModel.traverse((obj) => { + if (obj.material) { + const targetOpacity = easedProgress; + if (Array.isArray(obj.material)) { + obj.material.forEach(mat => { + mat.transparent = true; + mat.opacity = targetOpacity; + }); + } else { + obj.material.transparent = true; + obj.material.opacity = targetOpacity; + } + } + }); + } + + // Complete transition + if (transitionProgress >= 1) { + // FIXED: Reset geometry before removing the model if (currentModel) { - // Move current model up and fade out - // currentModel.position.y = easedProgress * 10; - - currentModel.traverse((obj) => { - if (obj.material) { - const targetOpacity = 1 - easedProgress; - if (Array.isArray(obj.material)) { - obj.material.forEach(mat => { - mat.transparent = true; - mat.opacity = targetOpacity; - }); - } else { - obj.material.transparent = true; - obj.material.opacity = targetOpacity; - } - } - }); + // 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); } + // Switch to next model if (nextModel) { - // Keep next model in place and just fade in (no vertical movement) - nextModel.position.y = 0; - - nextModel.traverse((obj) => { - if (obj.material) { - const targetOpacity = easedProgress; - if (Array.isArray(obj.material)) { - obj.material.forEach(mat => { - mat.transparent = true; - mat.opacity = targetOpacity; - }); - } else { - obj.material.transparent = true; - obj.material.opacity = targetOpacity; - } + currentModel = nextModel; + mixer = nextMixer; + + // Reset position and opacity + currentModel.position.y = 0; + currentModel.traverse((obj) => { + if (obj.material) { + if (Array.isArray(obj.material)) { + obj.material.forEach(mat => { + mat.opacity = 1; + if (currentScene + transitionDirection === 3) { // Keep transparency for storytelling glass + mat.transparent = mat.transmission > 0; + } else { + mat.transparent = mat.transmission > 0; + } + }); + } else { + obj.material.opacity = 1; + if (currentScene + transitionDirection === 3) { // Keep transparency for storytelling glass + obj.material.transparent = obj.material.transmission > 0; + } else { + obj.material.transparent = obj.material.transmission > 0; } - }); + } + } + }); } - // Complete transition - if (transitionProgress >= 1) { - // Remove current model - if (currentModel) { - scene.remove(currentModel); - - // Clean up geometry user data - currentModel.traverse((obj) => { - if (obj.geometry && obj.geometry.userData.originalPositions) { - delete obj.geometry.userData.originalPositions; - delete obj.geometry.userData.bounds; - } - }); - } - - // Switch to next model - if (nextModel) { - currentModel = nextModel; - mixer = nextMixer; - - // Reset position and opacity - currentModel.position.y = 0; - currentModel.traverse((obj) => { - if (obj.material) { - if (Array.isArray(obj.material)) { - obj.material.forEach(mat => { - mat.opacity = 1; - if (currentScene === 3) { // Keep transparency for storytelling glass - mat.transparent = mat.transmission > 0; - } else { - mat.transparent = mat.transmission > 0; - } - }); - } else { - obj.material.opacity = 1; - if (currentScene === 3) { // Keep transparency for storytelling glass - obj.material.transparent = obj.material.transmission > 0; - } else { - obj.material.transparent = obj.material.transmission > 0; - } - } - } - }); - } - - nextModel = null; - nextMixer = null; - isTransitioning = false; - isTwisting = false; - twistProgress = 0; - currentScene += transitionDirection; // Update scene based on direction - scrollDownCount = 0; - scrollUpCount = 0; - - console.log(`Transition complete. Current scene: ${currentScene}`); + nextModel = null; + nextMixer = null; + isTransitioning = false; + isTwisting = false; + twistProgress = 0; + currentScene += transitionDirection; // Update scene based on direction + scrollDownCount = 0; + scrollUpCount = 0; + + // Start innovation glass animation if we're now in the innovation scene + if (currentScene === 1) { + startInnovationGlassAnimation(); } + + console.log(`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 (isTransitioning) return; - 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 (event.deltaY > 0) { + // Scrolling down - move forward + scrollDownCount++; + scrollUpCount = 0; // Reset up count + console.log(`Scroll down count: ${scrollDownCount}`); - if (scrollUpCount >= scrollThreshold) { - startTransition(-1); // Backward direction - } + 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}`); -// Attach scroll event listener -window.addEventListener('wheel', onMouseScroll, {passive: true}); + if (scrollUpCount >= scrollThreshold) { + startTransition(-1); // Backward direction + } + } +} // Animation loop const clock = new THREE.Clock(); function animate() { - requestAnimationFrame(animate); + requestAnimationFrame(animate); - const delta = clock.getDelta(); + const delta = clock.getDelta(); + + // Update mixers + if (mixer) mixer.update(delta); + if (nextMixer) nextMixer.update(delta); + + // Update transition + if (isTransitioning) { + updateTransition(delta); + + // Apply twist during transition + if (isTwisting && currentModel) { + twistProgress += twistSpeed; + if (twistProgress > 1.0) { + twistProgress = 1.0; + // FIXED: Reset geometry after twist completes + currentModel.traverse((object) => { + if (object.isMesh) { + resetMeshGeometry(object); + } + }); + isTwisting = false; + } else { + // Apply twist to current model + currentModel.traverse((object) => { + if (object.isMesh) { + twistMesh(object, twistProgress); + } + }); + } + } + } + + // Update bold roughness animation + if (boldRoughnessAnimation.isActive) { + const elapsed = (performance.now() - boldRoughnessAnimation.startTime) / 1000; - // Update mixers - if (mixer) mixer.update(delta); - if (nextMixer) nextMixer.update(delta); + if (elapsed >= boldRoughnessAnimation.delayDuration) { + // Delay period is over, start roughness transition + const transitionElapsed = elapsed - boldRoughnessAnimation.delayDuration; + const transitionProgress = Math.min(transitionElapsed / boldRoughnessAnimation.transitionDuration, 1); - // Update transition - if (isTransitioning) { - updateTransition(delta); - - // Apply twist during transition - if (isTwisting && currentModel) { - twistProgress += twistSpeed; - if (twistProgress > 1.0) { - twistProgress = 1.0; - - // Reset geometry after twist completes - // currentModel.traverse((object) => { - // if (object.isMesh) { - // resetMeshGeometry(object); - // } - // }); - - isTwisting = false; - } else { - // Apply twist to current model - currentModel.traverse((object) => { - if (object.isMesh) { - twistMesh(object, twistProgress); - } - }); - } - } + // Smooth easing function (ease-in-out) + const easeInOut = (t) => t * t * (3 - 2 * t); + const easedProgress = easeInOut(transitionProgress); + + // Interpolate roughness from 0.25 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; + } } + } - // 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.9 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; - } - } + // 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'); } + } - // Turntable rotation for current model - // if (currentModel && !isTransitioning) { - // autoRotationAngle += delta * 0.5; - // currentModel.rotation.y = autoRotationAngle; - // } + // Turntable rotation for current model + // if (currentModel && !isTransitioning) { + // autoRotationAngle += delta * 0.5; + // currentModel.rotation.y = autoRotationAngle; + // } - controls.update(); - composer.render(); + controls.update(); + composer.render(); } -animate(); +// Initialize the scene +async function init() { + try { + // Load all models first + preloadedModels = await sceneLoader.loadAllModels(); + + // Initialize the first scene + initializeScene(); + + // Start the animation loop + animate(); + + // Attach scroll event listener + window.addEventListener('wheel', onMouseScroll, {passive: true}); + + } catch (error) { + console.error('Failed to initialize scene:', error); + sceneLoader.setLoadingMessage('Error loading experience. Please refresh.'); + } +} // Handle window resize window.addEventListener('resize', () => { - camera.aspect = window.innerWidth / window.innerHeight; - camera.updateProjectionMatrix(); - renderer.setSize(window.innerWidth, window.innerHeight); - composer.setSize(window.innerWidth, window.innerHeight); -}); \ No newline at end of file + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); + composer.setSize(window.innerWidth, window.innerHeight); +}); + +// Start the application +init(); diff --git a/src/style.css b/src/style.css index d70883b..73a5335 100644 --- a/src/style.css +++ b/src/style.css @@ -1,8 +1,127 @@ -body { +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +* { margin: 0; padding: 0; + box-sizing: border-box; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; overflow: hidden; } -canvas { - display: block; -} \ No newline at end of file + +/* Loading Screen Styles */ +#loading-screen { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(135deg, #000000 0%, #1a1a1a 100%); + display: flex; + justify-content: center; + align-items: center; + z-index: 10000; + transition: opacity 0.8s ease-out, visibility 0.8s ease-out; +} + +#loading-screen.hidden { + opacity: 0; + visibility: hidden; +} + +.loading-container { + text-align: center; + max-width: 400px; + padding: 2rem; +} + +.loading-spinner { + width: 60px; + height: 60px; + margin: 0 auto 2rem; + border: 3px solid rgba(255, 165, 0, 0.1); + border-top: 3px solid #FFA500; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.loading-text { + font-size: 1.5rem; + font-weight: 300; + color: #ffffff; + margin-bottom: 2rem; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.loading-progress { + width: 100%; + height: 2px; + background-color: rgba(255, 165, 0, 0.1); + border-radius: 1px; + overflow: hidden; + margin-bottom: 1rem; +} + +.loading-progress-bar { + height: 100%; + background: linear-gradient(90deg, #FFA500 0%, #ff8600 100%); + width: 0%; + transition: width 0.3s ease; + border-radius: 1px; +} + +.loading-percentage { + font-size: 0.9rem; + color: #FFA500; + font-weight: 500; + letter-spacing: 1px; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .loading-container { + padding: 1rem; + } + + .loading-text { + font-size: 1.2rem; + } + + .loading-spinner { + width: 50px; + height: 50px; + } +} + +#app { + width: 100vw; + height: 100vh; + margin: 0; + padding: 0; +}