[claude] Add zoom-to-object on double-click (#139) #235
84
app.js
84
app.js
@@ -231,6 +231,9 @@ voidLight.position.set(0, -3.5, 0);
|
||||
glassPlatformGroup.add(voidLight);
|
||||
|
||||
scene.add(glassPlatformGroup);
|
||||
glassPlatformGroup.traverse(obj => {
|
||||
if (obj.isMesh) obj.userData.zoomLabel = 'Glass Platform';
|
||||
});
|
||||
|
||||
// === COMMIT HEATMAP ===
|
||||
// Canvas-texture overlay on the floor. Each agent occupies a polar sector;
|
||||
@@ -269,6 +272,7 @@ const heatmapMesh = new THREE.Mesh(
|
||||
);
|
||||
heatmapMesh.rotation.x = -Math.PI / 2;
|
||||
heatmapMesh.position.y = 0.005;
|
||||
heatmapMesh.userData.zoomLabel = 'Activity Heatmap';
|
||||
scene.add(heatmapMesh);
|
||||
|
||||
// Per-zone intensity [0..1], updated by updateHeatmap()
|
||||
@@ -408,6 +412,66 @@ document.addEventListener('keydown', (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
// === ZOOM-TO-OBJECT ===
|
||||
const _zoomRaycaster = new THREE.Raycaster();
|
||||
const _zoomMouse = new THREE.Vector2();
|
||||
const _zoomCamTarget = new THREE.Vector3();
|
||||
const _zoomLookTarget = new THREE.Vector3();
|
||||
let zoomT = 0;
|
||||
let zoomTargetT = 0;
|
||||
let zoomActive = false;
|
||||
|
||||
const zoomIndicator = document.getElementById('zoom-indicator');
|
||||
const zoomLabelEl = document.getElementById('zoom-label');
|
||||
|
||||
function getZoomLabel(/** @type {THREE.Object3D} */ obj) {
|
||||
let o = /** @type {THREE.Object3D|null} */ (obj);
|
||||
while (o) {
|
||||
if (o.userData && o.userData.zoomLabel) return o.userData.zoomLabel;
|
||||
o = o.parent;
|
||||
}
|
||||
return 'Object';
|
||||
}
|
||||
|
||||
function exitZoom() {
|
||||
zoomTargetT = 0;
|
||||
zoomActive = false;
|
||||
if (zoomIndicator) zoomIndicator.classList.remove('visible');
|
||||
}
|
||||
|
||||
renderer.domElement.addEventListener('dblclick', (/** @type {MouseEvent} */ e) => {
|
||||
if (overviewMode || 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));
|
||||
_zoomCamTarget.copy(hit.point).addScaledVector(dir, flyDist);
|
||||
_zoomLookTarget.copy(hit.point);
|
||||
zoomT = 0;
|
||||
zoomTargetT = 1;
|
||||
zoomActive = true;
|
||||
|
||||
if (zoomLabelEl) zoomLabelEl.textContent = label;
|
||||
if (zoomIndicator) zoomIndicator.classList.add('visible');
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') exitZoom();
|
||||
});
|
||||
|
||||
// === PHOTO MODE ===
|
||||
let photoMode = false;
|
||||
|
||||
@@ -550,6 +614,9 @@ meterSprite.scale.set(3.2, 1.6, 1);
|
||||
sovereigntyGroup.add(meterSprite);
|
||||
|
||||
scene.add(sovereigntyGroup);
|
||||
sovereigntyGroup.traverse(obj => {
|
||||
if (obj.isMesh || obj.isSprite) obj.userData.zoomLabel = 'Sovereignty Meter';
|
||||
});
|
||||
|
||||
async function loadSovereigntyStatus() {
|
||||
try {
|
||||
@@ -590,8 +657,19 @@ function animate() {
|
||||
// Smooth camera transition for overview mode
|
||||
const targetT = overviewMode ? 1 : 0;
|
||||
overviewT += (targetT - overviewT) * 0.04;
|
||||
camera.position.lerpVectors(NORMAL_CAM, OVERVIEW_CAM, overviewT);
|
||||
camera.lookAt(0, 0, 0);
|
||||
const _basePos = new THREE.Vector3().lerpVectors(NORMAL_CAM, OVERVIEW_CAM, overviewT);
|
||||
|
||||
// Zoom-to-object interpolation
|
||||
if (!photoMode) {
|
||||
zoomT += (zoomTargetT - zoomT) * 0.07;
|
||||
}
|
||||
if (zoomT > 0.001 && !photoMode && !overviewMode) {
|
||||
camera.position.lerpVectors(_basePos, _zoomCamTarget, zoomT);
|
||||
camera.lookAt(new THREE.Vector3(0, 0, 0).lerp(_zoomLookTarget, zoomT));
|
||||
} else {
|
||||
camera.position.copy(_basePos);
|
||||
camera.lookAt(0, 0, 0);
|
||||
}
|
||||
|
||||
// Slow auto-rotation — suppressed during overview and photo mode
|
||||
const rotationScale = photoMode ? 0 : (1 - overviewT);
|
||||
@@ -931,6 +1009,7 @@ async function initCommitBanners() {
|
||||
startDelay: i * 2.5,
|
||||
lifetime: 12 + i * 1.5,
|
||||
spawnTime: /** @type {number|null} */ (null),
|
||||
zoomLabel: `Commit: ${commit.hash}`,
|
||||
};
|
||||
scene.add(sprite);
|
||||
commitBanners.push(sprite);
|
||||
@@ -1076,6 +1155,7 @@ function rebuildAgentPanels(statusData) {
|
||||
baseY: BOARD_Y,
|
||||
floatPhase: (i / n) * Math.PI * 2,
|
||||
floatSpeed: 0.18 + i * 0.04,
|
||||
zoomLabel: `Agent: ${agent.name}`,
|
||||
};
|
||||
agentBoardGroup.add(sprite);
|
||||
agentPanelSprites.push(sprite);
|
||||
|
||||
@@ -48,6 +48,11 @@
|
||||
|
||||
<div id="sovereignty-msg">⚡ SOVEREIGNTY ⚡</div>
|
||||
|
||||
<div id="zoom-indicator">
|
||||
<span>ZOOMED: <span id="zoom-label">Object</span></span>
|
||||
<span class="zoom-hint">[Esc] or double-click to exit</span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/sw.js').catch(() => {});
|
||||
|
||||
31
style.css
31
style.css
@@ -151,6 +151,37 @@ body.photo-mode #overview-indicator {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* === ZOOM-TO-OBJECT INDICATOR === */
|
||||
#zoom-indicator {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: var(--color-accent);
|
||||
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-accent);
|
||||
padding: 4px 12px;
|
||||
background: rgba(0, 0, 8, 0.6);
|
||||
white-space: nowrap;
|
||||
animation: overview-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
#zoom-indicator.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.zoom-hint {
|
||||
margin-left: 12px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* === SOVEREIGNTY EASTER EGG === */
|
||||
#sovereignty-msg {
|
||||
display: none;
|
||||
|
||||
Reference in New Issue
Block a user