Axiovora-X / frontend /lib /three-scene.ts
ZAIDX11's picture
Add files using upload-large-folder tool
c4e3b10 verified
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';
// Noise function to generate terrain
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() {
// Scene
this.scene = new THREE.Scene();
this.scene.fog = new THREE.Fog(0x0a0a2a, 30, 150);
this.scene.background = new THREE.Color(0x0a0a2a);
// Camera
this.camera = new THREE.PerspectiveCamera(75, this.container.clientWidth / this.container.clientHeight, 0.1, 1000);
this.camera.position.set(0, 10, 50);
// Renderer
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);
// Post-processing Composer
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); // Default values don't matter much here
// Refined bloom pass for a more cinematic glow
bloomPass.threshold = 0.1; // Only objects brighter than this threshold will bloom
bloomPass.strength = 1.2; // The intensity of the glow
bloomPass.radius = 0.6; // The spread of the glow
this.composer = new EffectComposer(this.renderer);
this.composer.addPass(renderPass);
this.composer.addPass(bloomPass);
// Controls
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; // Prevent camera going below ground
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);
// Central glow from horizon
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({ // Changed from MeshBasicMaterial
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)); // x
vertices.push(THREE.MathUtils.randFloatSpread(2000)); // y
vertices.push(THREE.MathUtils.randFloatSpread(2000)); // z
}
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) {
// 1. Create a flash of light
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();
}
});
// 2. Find a target and create the bolt
const towers = this.mathematicalObjects.filter(o => o.userData.type === 'axiom');
if (towers.length > 0) {
const randomTower = towers[Math.floor(Math.random() * towers.length)];
// Target the top of the tower
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, // Note: this is not supported by all drivers
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, // to white
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);
}
// --- Animations ---
// Portal pulse
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;
// Sky network nodes 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);
});
// Sky network lines animation
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);
});
// Floating debris rotation
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') {
// Aesthetic is now fixed and looks best in dark mode.
// This function can be used for more subtle adjustments if needed.
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 } }, // home
{ pos: { x: 0, y: 7, z: 15 }, lookAt: { x: 0, y: 6, z: 0 } }, // genesis (portal)
{ pos: { x: 25, y: 2, z: 25 }, lookAt: { x: 20, y: 0, z: 10 } }, // quantum (terrain grid)
{ pos: { x: 0, y: 40, z: 10 }, lookAt: { x: 0, y: 35, z: -40 } }, // neuro (sky network)
{ pos: { x: -15, y: 10, z: -15 }, lookAt: { x: -30, y: 15, z: -20 } }, // papers (focus on tower/debris)
{ pos: { x: 0, y: 8, z: 30 }, lookAt: { x: 0, y: 5, z: 0 } }, // explorer
];
const state = cameraStates[sectionIndex];
const overviewState = { pos: { x: 0, y: 35, z: 70 }, lookAt: { x: 0, y: 10, z: 0 } };
// Don't zoom out if just going to explorer from home or vice versa
const shouldZoomOut = !( (oldSectionIndex === 0 && sectionIndex === 5) || (oldSectionIndex === 5 && sectionIndex === 0) );
const tl = gsap.timeline({
onComplete: () => {
this.isTransitioning = false;
this.controls.enabled = isExplorer;
}
});
const onUpdateCallback = () => {
// As the controls.target is animated, we manually update the camera's lookAt
// This is only needed when controls are disabled.
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) {
// Part 1: Zoom out to a neutral, overview position
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);
}
// Part 2: Zoom in to the new section's camera position
// The overlap `>-0.8` creates a smoother "swoop" effect
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,
}, "<"); // "<" ensures this tween starts at the same time as the previous one
}
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();
}
}
});
}
}