// === MOUSE ROTATION + OVERVIEW + ZOOM + PHOTO MODE === import * as THREE from 'three'; 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 { BokehPass } from 'three/addons/postprocessing/BokehPass.js'; import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'; import { scene, camera, renderer } from './scene-setup.js'; import { S } from './state.js'; // === MOUSE-DRIVEN ROTATION === document.addEventListener('mousemove', (e) => { S.mouseX = (e.clientX / window.innerWidth - 0.5) * 2; S.mouseY = (e.clientY / window.innerHeight - 0.5) * 2; }); // === OVERVIEW MODE === export const NORMAL_CAM = new THREE.Vector3(0, 6, 11); export const OVERVIEW_CAM = new THREE.Vector3(0, 200, 0.1); const overviewIndicator = document.getElementById('overview-indicator'); document.addEventListener('keydown', (e) => { if (e.key === 'Tab') { e.preventDefault(); S.overviewMode = !S.overviewMode; if (S.overviewMode) { overviewIndicator.classList.add('visible'); } else { overviewIndicator.classList.remove('visible'); } } }); // === ZOOM-TO-OBJECT === const _zoomRaycaster = new THREE.Raycaster(); const _zoomMouse = new THREE.Vector2(); const zoomIndicator = document.getElementById('zoom-indicator'); const zoomLabelEl = document.getElementById('zoom-label'); function getZoomLabel(obj) { let o = obj; while (o) { if (o.userData && o.userData.zoomLabel) return o.userData.zoomLabel; o = o.parent; } return 'Object'; } export function exitZoom() { S.zoomTargetT = 0; S.zoomActive = false; if (zoomIndicator) zoomIndicator.classList.remove('visible'); } renderer.domElement.addEventListener('dblclick', (e) => { if (S.overviewMode || S.photoMode) return; _zoomMouse.x = (e.clientX / window.innerWidth) * 2 - 1; _zoomMouse.y = -(e.clientY / window.innerHeight) * 2 + 1; _zoomRaycaster.setFromCamera(_zoomMouse, camera); const hits = _zoomRaycaster.intersectObjects(scene.children, true) .filter(h => !(h.object instanceof THREE.Points) && !(h.object instanceof THREE.Line)); if (!hits.length) { exitZoom(); return; } const hit = hits[0]; const label = getZoomLabel(hit.object); const dir = new THREE.Vector3().subVectors(camera.position, hit.point).normalize(); const flyDist = Math.max(1.5, Math.min(5, hit.distance * 0.45)); S._zoomCamTarget.copy(hit.point).addScaledVector(dir, flyDist); S._zoomLookTarget.copy(hit.point); S.zoomT = 0; S.zoomTargetT = 1; S.zoomActive = true; if (zoomLabelEl) zoomLabelEl.textContent = label; if (zoomIndicator) zoomIndicator.classList.add('visible'); }); document.addEventListener('keydown', (e) => { if (e.key === 'Escape') exitZoom(); }); // === PHOTO MODE === // Warp effect state (declared here, used by controls and warp modules) export const WARP_DURATION = 2.2; // Post-processing composer export const composer = new EffectComposer(renderer); composer.addPass(new RenderPass(scene, camera)); export const bokehPass = new BokehPass(scene, camera, { focus: 5.0, aperture: 0.00015, maxblur: 0.004, }); composer.addPass(bokehPass); // Orbit controls for free camera movement in photo mode export const orbitControls = new OrbitControls(camera, renderer.domElement); orbitControls.enableDamping = true; orbitControls.dampingFactor = 0.05; orbitControls.enabled = false; const photoIndicator = document.getElementById('photo-indicator'); const photoFocusDisplay = document.getElementById('photo-focus'); function updateFocusDisplay() { if (photoFocusDisplay) { photoFocusDisplay.textContent = bokehPass.uniforms['focus'].value.toFixed(1); } } document.addEventListener('keydown', (e) => { if (e.key === 'p' || e.key === 'P') { S.photoMode = !S.photoMode; document.body.classList.toggle('photo-mode', S.photoMode); orbitControls.enabled = S.photoMode; if (photoIndicator) { photoIndicator.classList.toggle('visible', S.photoMode); } if (S.photoMode) { bokehPass.uniforms['aperture'].value = 0.0003; bokehPass.uniforms['maxblur'].value = 0.008; orbitControls.target.set(0, 0, 0); orbitControls.update(); updateFocusDisplay(); } else { bokehPass.uniforms['aperture'].value = 0.00015; bokehPass.uniforms['maxblur'].value = 0.004; } } if (S.photoMode) { const focusStep = 0.5; if (e.key === '[') { bokehPass.uniforms['focus'].value = Math.max(0.5, bokehPass.uniforms['focus'].value - focusStep); updateFocusDisplay(); } else if (e.key === ']') { bokehPass.uniforms['focus'].value = Math.min(200, bokehPass.uniforms['focus'].value + focusStep); updateFocusDisplay(); } } }); // === RESIZE HANDLER === window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); composer.setSize(window.innerWidth, window.innerHeight); });