|
|
|
|
|
|
| import * as THREE from 'three'; |
| import { gsap } from 'gsap'; |
| import { OrbitControls } from 'three/addons/controls/OrbitControls.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'; |
| import { ObjectInfo } from '../types'; |
|
|
| |
| function getNoise(x: number, y: number) { |
| return (Math.sin(x / 15) * Math.cos(y / 15) + Math.sin(y / 10) * Math.cos(x / 20)) * 5; |
| } |
|
|
| export class ThreeScene { |
| private container: HTMLElement; |
| private scene!: THREE.Scene; |
| private camera!: THREE.PerspectiveCamera; |
| private renderer!: THREE.WebGLRenderer; |
| private composer!: EffectComposer; |
| private controls!: OrbitControls; |
| private mathematicalObjects: THREE.Object3D[] = []; |
| private clock = new THREE.Clock(); |
| private mouse = new THREE.Vector2(); |
| private raycaster = new THREE.Raycaster(); |
| private currentSectionIndex = 0; |
| private isTransitioning = false; |
| private hoveredObject: THREE.Object3D | null = null; |
| private portalSprite!: THREE.Sprite; |
| private skyNetworkNodes: THREE.Mesh[] = []; |
| private skyNetworkLines: THREE.Line[] = []; |
| private lightningConductor!: THREE.Object3D; |
|
|
| private animationFrameId!: number; |
|
|
| constructor(container: HTMLElement, private onObjectSelect: (info: ObjectInfo) => void) { |
| this.container = container; |
| } |
|
|
| public init() { |
| |
| this.scene = new THREE.Scene(); |
| this.scene.fog = new THREE.Fog(0x0a0a2a, 30, 150); |
| this.scene.background = new THREE.Color(0x0a0a2a); |
|
|
| |
| this.camera = new THREE.PerspectiveCamera(75, this.container.clientWidth / this.container.clientHeight, 0.1, 1000); |
| this.camera.position.set(0, 10, 50); |
|
|
| |
| this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); |
| this.renderer.setSize(this.container.clientWidth, this.container.clientHeight); |
| this.renderer.setPixelRatio(window.devicePixelRatio); |
| this.renderer.toneMapping = THREE.ACESFilmicToneMapping; |
| this.renderer.outputColorSpace = THREE.SRGBColorSpace; |
| this.container.appendChild(this.renderer.domElement); |
|
|
| |
| const renderPass = new RenderPass(this.scene, this.camera); |
| const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85); |
| |
| bloomPass.threshold = 0.1; |
| bloomPass.strength = 1.2; |
| bloomPass.radius = 0.6; |
|
|
|
|
| this.composer = new EffectComposer(this.renderer); |
| this.composer.addPass(renderPass); |
| this.composer.addPass(bloomPass); |
|
|
| |
| this.controls = new OrbitControls(this.camera, this.renderer.domElement); |
| this.controls.enableDamping = true; |
| this.controls.dampingFactor = 0.05; |
| this.controls.screenSpacePanning = false; |
| this.controls.minDistance = 5; |
| this.controls.maxDistance = 100; |
| this.controls.maxPolarAngle = Math.PI / 2 - 0.05; |
| this.controls.enabled = false; |
|
|
| this.setupLighting(); |
| this.createMathematicalCosmos(); |
| this.addEventListeners(); |
| this.animate(); |
| } |
|
|
| private setupLighting() { |
| const ambientLight = new THREE.AmbientLight(0x5a2a8a, 0.5); |
| this.scene.add(ambientLight); |
|
|
| |
| const sunLight = new THREE.PointLight(0xffcc66, 3.5, 200); |
| sunLight.position.set(0, 10, -50); |
| this.scene.add(sunLight); |
|
|
| const fillLight = new THREE.DirectionalLight(0xaa44ff, 0.8); |
| fillLight.position.set(-1, 1, 1); |
| this.scene.add(fillLight); |
| } |
| |
| private createMathematicalCosmos() { |
| this.createLandscape(); |
| this.createCrystalTowers(); |
| this.createCentralPortal(); |
| this.createSkyNetwork(); |
| this.createFloatingDebris(); |
| this.createStars(); |
| this.createLightningConductor(); |
| } |
|
|
| private createLandscape() { |
| const size = 200; |
| const segments = 100; |
| const geometry = new THREE.PlaneGeometry(size, size, segments, segments); |
| |
| const positions = geometry.attributes.position.array as Float32Array; |
| for (let i = 0; i < positions.length; i += 3) { |
| const x = positions[i]; |
| const y = positions[i+1]; |
| const z = getNoise(x, y); |
| positions[i+2] = z; |
| } |
| geometry.computeVertexNormals(); |
|
|
| const terrainMaterial = new THREE.MeshStandardMaterial({ |
| color: 0x4b0082, |
| emissive: 0x1a0033, |
| emissiveIntensity: 1.2, |
| roughness: 0.8, |
| metalness: 0.2, |
| }); |
|
|
| const terrainMesh = new THREE.Mesh(geometry, terrainMaterial); |
| terrainMesh.rotation.x = -Math.PI / 2; |
| |
| const wireframeMaterial = new THREE.MeshBasicMaterial({ |
| color: 0xffdd00, |
| wireframe: true, |
| transparent: true, |
| opacity: 0.25, |
| }); |
| const wireframeMesh = new THREE.Mesh(geometry.clone(), wireframeMaterial); |
| wireframeMesh.rotation.x = -Math.PI / 2; |
| wireframeMesh.position.y = 0.01; |
|
|
| const gridHelper = new THREE.GridHelper(size, segments, 0x00ffff, 0x8800ff); |
| gridHelper.position.y = 0.05; |
| (gridHelper.material as THREE.Material).transparent = true; |
| (gridHelper.material as THREE.Material).opacity = 0.5; |
|
|
| const landscapeGroup = new THREE.Group(); |
| landscapeGroup.add(terrainMesh, wireframeMesh, gridHelper); |
| this.scene.add(landscapeGroup); |
|
|
| landscapeGroup.userData = { |
| type: 'quantum_field', |
| isInteractive: true, |
| info: { |
| title: 'Quantum Probability Field', |
| description: 'This dynamic grid represents the fluctuating probabilities of mathematical states in superposition. Its form shifts and evolves, visualizing the uncertainty inherent in the quantum layer.' |
| } |
| }; |
| this.mathematicalObjects.push(landscapeGroup); |
| } |
|
|
| private createCrystalTowers() { |
| const towerPositions = [ |
| { x: -30, z: -20 }, { x: 35, z: -10 }, |
| { x: -50, z: 10 }, { x: 40, z: 30 }, |
| { x: -20, z: 40 }, { x: 10, z: -40 }, |
| { x: 60, z: 0 }, { x: -65, z: -30 }, |
| ]; |
|
|
| for (const pos of towerPositions) { |
| const height = Math.random() * 20 + 20; |
| const radius = Math.random() * 1.5 + 1.5; |
| const geometry = new THREE.CylinderGeometry(radius * 0.2, radius, height, 8); |
| |
| const material = new THREE.MeshPhysicalMaterial({ |
| color: 0xffffff, |
| transmission: 0.9, |
| roughness: 0.05, |
| ior: 2.3, |
| thickness: 2.0, |
| emissive: 0xeeeeff, |
| emissiveIntensity: 0.5 |
| }); |
|
|
| const tower = new THREE.Mesh(geometry, material); |
| const y = getNoise(pos.x, -pos.z) + height / 2; |
| tower.position.set(pos.x, y, pos.z); |
| |
| tower.userData = { |
| type: 'axiom', |
| isInteractive: true, |
| info: { |
| title: 'Crystalline Spire', |
| description: 'A monument of pure logic, channeling mathematical energies from the cosmos. These towers stabilize the foundational axioms of this universe.' |
| }, |
| originalEmissiveIntensity: material.emissiveIntensity, |
| }; |
|
|
| this.scene.add(tower); |
| this.mathematicalObjects.push(tower); |
| } |
| } |
|
|
| private createCentralPortal() { |
| const portalGroup = new THREE.Group(); |
| portalGroup.position.set(0, 6, 0); |
|
|
| const frameMaterial = new THREE.MeshStandardMaterial({ |
| color: 0x111122, |
| roughness: 0.4, |
| metalness: 0.8, |
| emissive: 0x00ffff, |
| emissiveIntensity: 0.8 |
| }); |
| |
| const side1 = new THREE.Mesh(new THREE.BoxGeometry(1.5, 12, 1.5), frameMaterial); |
| side1.position.x = -5; |
| const side2 = new THREE.Mesh(new THREE.BoxGeometry(1.5, 12, 1.5), frameMaterial); |
| side2.position.x = 5; |
| const top = new THREE.Mesh(new THREE.BoxGeometry(11.5, 1.5, 1.5), frameMaterial); |
| top.position.y = 6; |
| portalGroup.add(side1, side2, top); |
|
|
| const canvas = document.createElement('canvas'); |
| const context = canvas.getContext('2d')!; |
| canvas.width = 256; canvas.height = 512; |
| context.fillStyle = '#00ffff'; |
| context.shadowColor = '#00ffff'; |
| context.shadowBlur = 40; |
| context.font = 'bold 400px "Times New Roman"'; |
| context.textAlign = 'center'; context.textBaseline = 'middle'; |
| context.fillText('∫', 128, 256); |
| const texture = new THREE.CanvasTexture(canvas); |
| const material = new THREE.SpriteMaterial({ map: texture, transparent: true, blending: THREE.AdditiveBlending, depthTest: false, depthWrite: false }); |
| this.portalSprite = new THREE.Sprite(material); |
| this.portalSprite.scale.set(8, 16, 1); |
| portalGroup.add(this.portalSprite); |
|
|
| const reflectionSprite = this.portalSprite.clone(); |
| reflectionSprite.scale.y = -16; |
| reflectionSprite.position.y = -12.2; |
| (reflectionSprite.material as THREE.SpriteMaterial).opacity = 0.2; |
| portalGroup.add(reflectionSprite); |
|
|
| portalGroup.userData = { |
| type: 'genesis', |
| isInteractive: true, |
| info: { title: 'Genesis Portal', description: 'The central gateway through which new mathematical concepts are born into this universe. It represents the fundamental operation of integration.' }, |
| originalEmissiveIntensity: frameMaterial.emissiveIntensity, |
| }; |
|
|
| this.scene.add(portalGroup); |
| this.mathematicalObjects.push(portalGroup); |
| } |
| |
| private createSkyNetwork() { |
| const networkGroup = new THREE.Group(); |
| networkGroup.position.set(0, 35, -40); |
|
|
| const nodes = []; |
| for (let i = 0; i < 40; i++) { |
| const nodeGeometry = new THREE.SphereGeometry(0.3, 16, 16); |
| const nodeMaterial = new THREE.MeshStandardMaterial({ |
| color: 0xffdd00, |
| emissive: 0xffdd00, |
| emissiveIntensity: 1.5 |
| }); |
| const node = new THREE.Mesh(nodeGeometry, nodeMaterial); |
| node.position.set((Math.random() - 0.5) * 50, (Math.random() - 0.5) * 15, (Math.random() - 0.5) * 25); |
| |
| node.userData = { |
| type: 'neuro', |
| isInteractive: true, |
| info: { title: 'Cognitive Node', description: 'A point of calculation in the Neuro-Symbolic Core, forming a constellation of pure thought.' }, |
| originalEmissiveIntensity: nodeMaterial.emissiveIntensity, |
| }; |
| networkGroup.add(node); |
| nodes.push(node); |
| this.mathematicalObjects.push(node); |
| this.skyNetworkNodes.push(node); |
| } |
|
|
| for (let i = 0; i < nodes.length; i++) { |
| for (let j = i + 1; j < nodes.length; j++) { |
| if (Math.random() > 0.8) { |
| const geometry = new THREE.BufferGeometry().setFromPoints([nodes[i].position, nodes[j].position]); |
| const material = new THREE.LineBasicMaterial({ color: 0xffdd00, transparent: true, opacity: 0.5 }); |
| const line = new THREE.Line(geometry, material); |
| line.userData = { type: 'connection', originalOpacity: 0.5 }; |
| networkGroup.add(line); |
| this.skyNetworkLines.push(line); |
| } |
| } |
| } |
| this.scene.add(networkGroup); |
| } |
|
|
| private createFloatingDebris() { |
| const debrisGeometries = [ |
| new THREE.BoxGeometry(0.5, 0.5, 0.5), |
| new THREE.TetrahedronGeometry(0.4), |
| new THREE.OctahedronGeometry(0.4) |
| ]; |
| for (let i = 0; i < 150; i++) { |
| const material = new THREE.MeshStandardMaterial({ |
| color: new THREE.Color().setHSL(Math.random(), 0.7, 0.7), |
| transparent: true, opacity: 0.8, emissive: 0x222222, emissiveIntensity: 1.0 |
| }); |
| const debris = new THREE.Mesh(debrisGeometries[Math.floor(Math.random() * debrisGeometries.length)], material); |
| debris.position.set((Math.random() - 0.5) * 120, Math.random() * 40 + 2, (Math.random() - 0.5) * 120); |
| debris.userData = { |
| type: 'papers', |
| isInteractive: true, |
| rotationSpeed: new THREE.Vector3((Math.random() - 0.5) * 0.01, (Math.random() - 0.5) * 0.01, (Math.random() - 0.5) * 0.01), |
| info: { title: 'Data Fragment', description: 'A remnant of a complex calculation or a piece of a forgotten theorem, drifting through the cosmos.' }, |
| originalEmissive: material.emissive.clone(), |
| originalEmissiveIntensity: material.emissiveIntensity, |
| }; |
| this.scene.add(debris); |
| this.mathematicalObjects.push(debris); |
| } |
| } |
|
|
| private createStars() { |
| const vertices = []; |
| for (let i = 0; i < 10000; i++) { |
| vertices.push(THREE.MathUtils.randFloatSpread(2000)); |
| vertices.push(THREE.MathUtils.randFloatSpread(2000)); |
| vertices.push(THREE.MathUtils.randFloatSpread(2000)); |
| } |
| const geometry = new THREE.BufferGeometry(); |
| geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); |
| const material = new THREE.PointsMaterial({ color: 0xffffff, size: 0.7, transparent: true, opacity: 0.8 }); |
| const stars = new THREE.Points(geometry, material); |
| this.scene.add(stars); |
| } |
| |
| private createLightningConductor() { |
| const geometry = new THREE.OctahedronGeometry(1.5); |
| const material = new THREE.MeshStandardMaterial({ |
| color: 0x00ffff, |
| emissive: 0x00ffff, |
| emissiveIntensity: 1.5, |
| metalness: 0.8, |
| roughness: 0.2, |
| }); |
| |
| const conductor = new THREE.Mesh(geometry, material); |
| conductor.position.set(10, 12, 15); |
| conductor.userData = { |
| type: 'conductor', |
| isInteractive: true, |
| info: { |
| title: 'Storm Weaver Crystal', |
| description: 'Click this crystal to channel raw cosmic energy, discharging it as a bolt of lightning across the mathematical plane.' |
| }, |
| originalEmissiveIntensity: material.emissiveIntensity, |
| }; |
| this.scene.add(conductor); |
| this.mathematicalObjects.push(conductor); |
| this.lightningConductor = conductor; |
| } |
|
|
| private handleResize = () => { |
| this.camera.aspect = this.container.clientWidth / this.container.clientHeight; |
| this.camera.updateProjectionMatrix(); |
| this.renderer.setSize(this.container.clientWidth, this.container.clientHeight); |
| this.composer.setSize(this.container.clientWidth, this.container.clientHeight); |
| } |
| |
| private handleMouseMove = (event: MouseEvent) => { |
| this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1; |
| this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; |
| } |
|
|
| private handleClick = (event: MouseEvent) => { |
| if (this.currentSectionIndex !== 5) return; |
|
|
| this.mouse.x = (event.clientX / this.container.clientWidth) * 2 - 1; |
| this.mouse.y = -(event.clientY / this.container.clientHeight) * 2 + 1; |
|
|
| this.raycaster.setFromCamera(this.mouse, this.camera); |
| const intersects = this.raycaster.intersectObjects(this.mathematicalObjects.filter(o => o.userData.info), true); |
|
|
| if (intersects.length > 0) { |
| const rootObject = this.findRootObject(intersects[0].object); |
| if (rootObject && rootObject.userData.info) { |
| gsap.fromTo(rootObject.scale, |
| { x: 1, y: 1, z: 1 }, |
| { x: 1.2, y: 1.2, z: 1.2, duration: 0.4, yoyo: true, repeat: 1, ease: 'power2.inOut' } |
| ); |
|
|
| this.onObjectSelect(rootObject.userData.info); |
| |
| gsap.to(this.controls.target, { |
| x: rootObject.position.x, |
| y: rootObject.position.y, |
| z: rootObject.position.z, |
| duration: 1, |
| ease: 'power3.inOut' |
| }); |
|
|
| if (rootObject === this.lightningConductor) { |
| this.triggerLightningEffect(rootObject.position); |
| } |
| } |
| } |
| } |
|
|
| private triggerLightningEffect(startPosition: THREE.Vector3) { |
| |
| const flash = new THREE.PointLight(0x88aaff, 30, 150, 2); |
| flash.position.copy(startPosition); |
| this.scene.add(flash); |
| gsap.to(flash, { |
| intensity: 0, |
| duration: 0.6, |
| ease: 'power2.out', |
| onComplete: () => { |
| this.scene.remove(flash); |
| flash.dispose(); |
| } |
| }); |
| |
| |
| const towers = this.mathematicalObjects.filter(o => o.userData.type === 'axiom'); |
| if (towers.length > 0) { |
| const randomTower = towers[Math.floor(Math.random() * towers.length)]; |
| |
| const endPosition = randomTower.position.clone(); |
| if (randomTower instanceof THREE.Mesh && randomTower.geometry instanceof THREE.CylinderGeometry) { |
| endPosition.y += randomTower.geometry.parameters.height / 2; |
| } |
| |
| this.createLightningBolt(startPosition, endPosition); |
| } |
| } |
| |
| private createLightningBolt(start: THREE.Vector3, end: THREE.Vector3) { |
| const points = [start.clone()]; |
| const direction = end.clone().sub(start); |
| const numSegments = 15; |
| const randomness = 1.5; |
| |
| for (let i = 1; i < numSegments; i++) { |
| const pos = start.clone().add(direction.clone().multiplyScalar(i / numSegments)); |
| pos.x += (Math.random() - 0.5) * randomness; |
| pos.y += (Math.random() - 0.5) * randomness; |
| pos.z += (Math.random() - 0.5) * randomness; |
| points.push(pos); |
| } |
| points.push(end.clone()); |
| |
| const geometry = new THREE.BufferGeometry().setFromPoints(points); |
| const material = new THREE.LineBasicMaterial({ |
| color: 0x00ffff, |
| linewidth: 2, |
| transparent: true, |
| opacity: 0.9, |
| blending: THREE.AdditiveBlending, |
| depthTest: false, |
| }); |
| |
| const bolt = new THREE.Line(geometry, material); |
| this.scene.add(bolt); |
| |
| gsap.to(material, { |
| opacity: 0, |
| duration: 0.7, |
| ease: 'power2.out', |
| delay: 0.1, |
| onComplete: () => { |
| this.scene.remove(bolt); |
| geometry.dispose(); |
| material.dispose(); |
| } |
| }); |
| } |
|
|
| private findRootObject(object: THREE.Object3D): THREE.Object3D | null { |
| let current = object; |
| while(current.parent && current.parent !== this.scene) { |
| if(current.userData.info) return current; |
| current = current.parent; |
| } |
| return current.userData.info ? current : null; |
| } |
|
|
|
|
| private addEventListeners() { |
| window.addEventListener('resize', this.handleResize); |
| window.addEventListener('mousemove', this.handleMouseMove); |
| this.container.addEventListener('click', this.handleClick); |
| } |
|
|
| private updateHover() { |
| if (this.currentSectionIndex !== 5) { |
| if (this.hoveredObject) this.unhighlight(this.hoveredObject); |
| this.hoveredObject = null; |
| return; |
| }; |
|
|
| this.raycaster.setFromCamera(this.mouse, this.camera); |
| const intersects = this.raycaster.intersectObjects(this.mathematicalObjects.filter(o => o.userData.isInteractive), true); |
| |
| const newHoveredObject = (intersects.length > 0) ? this.findRootObject(intersects[0].object) : null; |
|
|
| if (this.hoveredObject && this.hoveredObject !== newHoveredObject) { |
| this.unhighlight(this.hoveredObject); |
| } |
|
|
| if (newHoveredObject && newHoveredObject !== this.hoveredObject) { |
| this.highlight(newHoveredObject); |
| } |
| |
| this.hoveredObject = newHoveredObject; |
| } |
|
|
| private highlight(obj: THREE.Object3D) { |
| gsap.to(obj.scale, { |
| x: 1.1, y: 1.1, z: 1.1, |
| duration: 0.3, ease: 'power2.out', |
| overwrite: true, |
| }); |
|
|
| obj.traverse(child => { |
| if (child instanceof THREE.Mesh && child.material) { |
| const mat = child.material as THREE.MeshStandardMaterial | THREE.MeshBasicMaterial; |
| if ('emissive' in mat && 'emissiveIntensity' in mat) { |
| gsap.to(mat, { |
| emissiveIntensity: (obj.userData.originalEmissiveIntensity || 0.5) * 5, |
| duration: 0.8, |
| yoyo: true, |
| repeat: -1, |
| ease: 'sine.inOut', |
| overwrite: true, |
| }); |
| } else if ('color' in mat) { |
| gsap.to((mat as THREE.MeshBasicMaterial).color, { |
| r: 1, g: 1, b: 1, |
| duration: 0.8, |
| yoyo: true, |
| repeat: -1, |
| ease: 'sine.inOut', |
| overwrite: true, |
| }); |
| } |
| } |
| }); |
| } |
|
|
| private unhighlight(obj: THREE.Object3D) { |
| gsap.to(obj.scale, { |
| x: 1, y: 1, z: 1, |
| duration: 0.3, ease: 'power2.out', |
| overwrite: true, |
| }); |
|
|
| obj.traverse(child => { |
| if (child instanceof THREE.Mesh && child.material) { |
| const mat = child.material as THREE.MeshStandardMaterial | THREE.MeshBasicMaterial; |
| if ('emissive' in mat && 'emissiveIntensity' in mat) { |
| gsap.to(mat, { |
| emissiveIntensity: obj.userData.originalEmissiveIntensity || 0.5, |
| duration: 0.3, |
| overwrite: true, |
| }); |
| } else if ('color' in mat && obj.userData.originalColor) { |
| const originalColor = obj.userData.originalColor as THREE.Color; |
| gsap.to((mat as THREE.MeshBasicMaterial).color, { |
| r: originalColor.r, g: originalColor.g, b: originalColor.b, |
| duration: 0.3, |
| overwrite: true, |
| }); |
| } |
| } |
| }); |
| } |
|
|
| private animate = () => { |
| this.animationFrameId = requestAnimationFrame(this.animate); |
| const elapsedTime = this.clock.getElapsedTime(); |
|
|
| if (this.controls.enabled) { |
| this.controls.update(); |
| } else if (!this.isTransitioning) { |
| const target = this.controls.target; |
| this.camera.position.x += (this.mouse.x * 2 - (this.camera.position.x - target.x)) * 0.01; |
| this.camera.position.y += (this.mouse.y * 2 - (this.camera.position.y - target.y)) * 0.01; |
| this.camera.lookAt(target); |
| } |
|
|
| |
| |
| const pulse = Math.sin(elapsedTime * 1.5) * 0.1 + 0.95; |
| this.portalSprite.scale.set(8 * pulse, 16 * pulse, 1); |
| (this.portalSprite.material as THREE.SpriteMaterial).opacity = pulse; |
| |
| |
| this.skyNetworkNodes.forEach((node, index) => { |
| const mat = node.material as THREE.MeshStandardMaterial; |
| const baseIntensity = node.userData.originalEmissiveIntensity || 1.5; |
| mat.emissiveIntensity = baseIntensity * (1.0 + Math.sin(elapsedTime * 3 + index * 0.5) * 0.5); |
| }); |
|
|
| |
| this.skyNetworkLines.forEach((line, index) => { |
| const material = line.material as THREE.LineBasicMaterial; |
| material.opacity = line.userData.originalOpacity * (0.6 + Math.sin(elapsedTime * 1.5 + index * 0.5) * 0.4); |
| }); |
|
|
| |
| this.mathematicalObjects.forEach((obj) => { |
| if (obj.userData.type === 'papers' && obj.userData.rotationSpeed) { |
| obj.rotation.x += obj.userData.rotationSpeed.x; |
| obj.rotation.y += obj.userData.rotationSpeed.y; |
| } |
| }); |
| |
| this.updateHover(); |
|
|
| this.composer.render(); |
| } |
|
|
| public updateTheme(theme: 'dark' | 'light') { |
| |
| |
| if (theme === 'dark') { |
| this.scene.fog?.color.setHex(0x0a0a2a); |
| this.scene.background = new THREE.Color(0x0a0a2a); |
| } else { |
| this.scene.fog?.color.setHex(0x87ceeb); |
| this.scene.background = new THREE.Color(0x87ceeb); |
| } |
| } |
| |
| public updateForSection(sectionIndex: number) { |
| if (this.currentSectionIndex === sectionIndex || this.isTransitioning) return; |
| |
| const oldSectionIndex = this.currentSectionIndex; |
| this.currentSectionIndex = sectionIndex; |
| this.isTransitioning = true; |
| const isExplorer = sectionIndex === 5; |
|
|
| gsap.killTweensOf(this.camera.position); |
| gsap.killTweensOf(this.controls.target); |
|
|
| const cameraStates = [ |
| { pos: { x: 0, y: 10, z: 50 }, lookAt: { x: 0, y: 5, z: 0 } }, |
| { pos: { x: 0, y: 7, z: 15 }, lookAt: { x: 0, y: 6, z: 0 } }, |
| { pos: { x: 25, y: 2, z: 25 }, lookAt: { x: 20, y: 0, z: 10 } }, |
| { pos: { x: 0, y: 40, z: 10 }, lookAt: { x: 0, y: 35, z: -40 } }, |
| { pos: { x: -15, y: 10, z: -15 }, lookAt: { x: -30, y: 15, z: -20 } }, |
| { pos: { x: 0, y: 8, z: 30 }, lookAt: { x: 0, y: 5, z: 0 } }, |
| ]; |
| |
| const state = cameraStates[sectionIndex]; |
| const overviewState = { pos: { x: 0, y: 35, z: 70 }, lookAt: { x: 0, y: 10, z: 0 } }; |
| |
| |
| const shouldZoomOut = !( (oldSectionIndex === 0 && sectionIndex === 5) || (oldSectionIndex === 5 && sectionIndex === 0) ); |
|
|
| const tl = gsap.timeline({ |
| onComplete: () => { |
| this.isTransitioning = false; |
| this.controls.enabled = isExplorer; |
| } |
| }); |
|
|
| const onUpdateCallback = () => { |
| |
| |
| if (!this.controls.enabled) { |
| this.camera.lookAt(this.controls.target); |
| } |
| }; |
|
|
| const zoomOutDuration = 2.2; |
| const zoomInDuration = shouldZoomOut ? 2.2 : 2.8; |
| const ease = 'power2.inOut'; |
|
|
| if (shouldZoomOut) { |
| |
| tl.to(this.camera.position, { |
| ...overviewState.pos, |
| duration: zoomOutDuration, |
| ease: ease, |
| }, 0); |
| tl.to(this.controls.target, { |
| ...overviewState.lookAt, |
| duration: zoomOutDuration, |
| ease: ease, |
| onUpdate: onUpdateCallback, |
| }, 0); |
| } |
|
|
| |
| |
| const zoomInStartTime = shouldZoomOut ? ">-0.8" : 0; |
| |
| tl.to(this.camera.position, { |
| ...state.pos, |
| duration: zoomInDuration, |
| ease: ease, |
| }, zoomInStartTime); |
|
|
| tl.to(this.controls.target, { |
| ...state.lookAt, |
| duration: zoomInDuration, |
| ease: ease, |
| onUpdate: onUpdateCallback, |
| }, "<"); |
| } |
|
|
|
|
| public cleanUp() { |
| window.removeEventListener('resize', this.handleResize); |
| window.removeEventListener('mousemove', this.handleMouseMove); |
| this.container.removeEventListener('click', this.handleClick); |
| this.controls.dispose(); |
| cancelAnimationFrame(this.animationFrameId); |
| if (this.renderer.domElement.parentNode === this.container) { |
| this.container.removeChild(this.renderer.domElement); |
| } |
|
|
| this.scene.traverse(object => { |
| if (object instanceof THREE.Mesh) { |
| object.geometry.dispose(); |
| if(Array.isArray(object.material)) { |
| object.material.forEach(material => material.dispose()); |
| } else if (object.material) { |
| object.material.dispose(); |
| } |
| } |
| }); |
| } |
| } |