[claude] Add photo mode with camera controls and depth of field (#134) #177
79
app.js
79
app.js
@@ -1,4 +1,8 @@
|
||||
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';
|
||||
|
||||
// === COLOR PALETTE ===
|
||||
const NEXUS = {
|
||||
@@ -144,11 +148,75 @@ document.addEventListener('keydown', (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
// === PHOTO MODE ===
|
||||
let photoMode = false;
|
||||
|
||||
// Post-processing composer for depth of field
|
||||
const composer = new EffectComposer(renderer);
|
||||
composer.addPass(new RenderPass(scene, camera));
|
||||
|
||||
const bokehPass = new BokehPass(scene, camera, {
|
||||
focus: 5.0,
|
||||
aperture: 0.0003,
|
||||
maxblur: 0.008,
|
||||
});
|
||||
bokehPass.enabled = false;
|
||||
composer.addPass(bokehPass);
|
||||
|
||||
// Orbit controls for free camera movement in photo mode
|
||||
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');
|
||||
|
||||
/**
|
||||
* Updates the photo mode focus distance display.
|
||||
*/
|
||||
function updateFocusDisplay() {
|
||||
if (photoFocusDisplay) {
|
||||
photoFocusDisplay.textContent = bokehPass.uniforms['focus'].value.toFixed(1);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'p' || e.key === 'P') {
|
||||
photoMode = !photoMode;
|
||||
document.body.classList.toggle('photo-mode', photoMode);
|
||||
bokehPass.enabled = photoMode;
|
||||
orbitControls.enabled = photoMode;
|
||||
if (photoIndicator) {
|
||||
photoIndicator.classList.toggle('visible', photoMode);
|
||||
}
|
||||
if (photoMode) {
|
||||
// Sync orbit target to current look-at
|
||||
orbitControls.target.set(0, 0, 0);
|
||||
orbitControls.update();
|
||||
updateFocusDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust focus with [ ] while in photo mode
|
||||
if (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);
|
||||
});
|
||||
|
||||
// === ANIMATION LOOP ===
|
||||
@@ -168,8 +236,8 @@ function animate() {
|
||||
camera.position.lerpVectors(NORMAL_CAM, OVERVIEW_CAM, overviewT);
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
// Slow auto-rotation — suppressed during overview so the map stays readable
|
||||
const rotationScale = 1 - overviewT;
|
||||
// Slow auto-rotation — suppressed during overview and photo mode
|
||||
const rotationScale = photoMode ? 0 : (1 - overviewT);
|
||||
targetRotX += (mouseY * 0.3 - targetRotX) * 0.02;
|
||||
targetRotY += (mouseX * 0.3 - targetRotY) * 0.02;
|
||||
|
||||
@@ -182,7 +250,12 @@ function animate() {
|
||||
// Subtle pulse on constellation opacity
|
||||
constellationLines.material.opacity = 0.12 + Math.sin(elapsed * 0.5) * 0.06;
|
||||
|
||||
renderer.render(scene, camera);
|
||||
if (photoMode) {
|
||||
orbitControls.update();
|
||||
composer.render();
|
||||
} else {
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
}
|
||||
|
||||
animate();
|
||||
|
||||
@@ -41,6 +41,11 @@
|
||||
<span class="overview-hint">[Tab] to exit</span>
|
||||
</div>
|
||||
|
||||
<div id="photo-indicator">
|
||||
<span>PHOTO MODE</span>
|
||||
<span class="photo-hint">[P] exit | [[] focus- []] focus+ focus: <span id="photo-focus">5.0</span></span>
|
||||
</div>
|
||||
|
||||
<script type="module" src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
44
style.css
44
style.css
@@ -106,3 +106,47 @@ canvas {
|
||||
0%, 100% { opacity: 0.7; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
/* === PHOTO MODE === */
|
||||
body.photo-mode .hud-controls {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.photo-mode #overview-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#photo-indicator {
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: var(--color-primary);
|
||||
font-family: var(--font-body);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
pointer-events: none;
|
||||
z-index: 20;
|
||||
border: 1px solid var(--color-primary);
|
||||
padding: 4px 12px;
|
||||
background: rgba(0, 0, 8, 0.5);
|
||||
white-space: nowrap;
|
||||
animation: overview-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
#photo-indicator.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.photo-hint {
|
||||
margin-left: 12px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
#photo-focus {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user