159 lines
5.0 KiB
JavaScript
159 lines
5.0 KiB
JavaScript
|
|
// === 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);
|
||
|
|
});
|