diff --git a/app.js b/app.js index 6e85528..2083c4e 100644 --- a/app.js +++ b/app.js @@ -119,6 +119,39 @@ document.addEventListener('mousemove', (e) => { mouseY = (e.clientY / window.innerHeight - 0.5) * 2; }); +// === ZOOM TO OBJECT (double-click to fly) === +const raycaster = new THREE.Raycaster(); +raycaster.params.Points.threshold = 3; +const mouse2D = new THREE.Vector2(); + +let zoomTarget = null; +let zoomOrigin = null; +let zoomLookAt = null; +let zoomStartTime = 0; +const ZOOM_DURATION = 1.4; + +function easeInOutQuad(t) { + return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; +} + +renderer.domElement.addEventListener('dblclick', (e) => { + mouse2D.x = (e.clientX / window.innerWidth) * 2 - 1; + mouse2D.y = -(e.clientY / window.innerHeight) * 2 + 1; + + raycaster.setFromCamera(mouse2D, camera); + const intersects = raycaster.intersectObject(stars); + + if (intersects.length > 0) { + const hitPoint = intersects[0].point.clone(); + // Approach from current camera direction, stop 8 units from hit + const approach = camera.position.clone().sub(hitPoint).normalize().multiplyScalar(8); + zoomOrigin = camera.position.clone(); + zoomTarget = hitPoint.clone().add(approach); + zoomLookAt = hitPoint.clone(); + zoomStartTime = clock.getElapsedTime(); + } +}); + // === RESIZE HANDLER === window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; @@ -146,6 +179,17 @@ function animate() { // Subtle pulse on constellation opacity constellationLines.material.opacity = 0.12 + Math.sin(elapsed * 0.5) * 0.06; + // Zoom-to-object animation + if (zoomTarget) { + const t = Math.min((elapsed - zoomStartTime) / ZOOM_DURATION, 1); + const ease = easeInOutQuad(t); + camera.position.lerpVectors(zoomOrigin, zoomTarget, ease); + camera.lookAt(zoomLookAt); + if (t >= 1) { + zoomTarget = null; + } + } + renderer.render(scene, camera); }