From a00d8c52d7fee854333c818b3e136158dd09d4b2 Mon Sep 17 00:00:00 2001 From: David Allemang Date: Sat, 4 Jul 2026 11:20:03 -0400 Subject: [PATCH] less intrusive camera controls --- camera.js | 150 +++++++++++++++++++++++++++++++++++++++-------------- index.html | 4 +- 2 files changed, 112 insertions(+), 42 deletions(-) diff --git a/camera.js b/camera.js index 7d5b2adc..96bbe3eb 100644 --- a/camera.js +++ b/camera.js @@ -16,23 +16,65 @@ export class Camera { this.viewProjMatrix = new Float32Array(16); this.isDragging = false; - this.dragButton = 0; // 0: Left, 1: Middle, 2: Right + this.dragButton = 0; + this.touchMode = ''; this.lastMouse = {x: 0, y: 0}; + this.lastPinchDist = 0; this.attachEvents(); } + // --- INTERACTION MATH --- + + orbit(dx, dy) { + this.theta -= dx * 0.4; + this.phi += dy * 0.4; + this.phi = Math.max(-90, Math.min(90, this.phi)); + } + + pan(dx, dy) { + const t = this.theta * Math.PI / 180; + const p = this.phi * Math.PI / 180; + + const rightX = Math.cos(t); + const rightZ = -Math.sin(t); + + const upX = -Math.sin(p) * Math.sin(t); + const upY = Math.cos(p); + const upZ = -Math.sin(p) * Math.cos(t); + + const panSpeed = this.radius * 0.0025; + + this.target[0] += (-rightX * dx + upX * dy) * panSpeed; + this.target[1] += (upY * dy) * panSpeed; + this.target[2] += (-rightZ * dx + upZ * dy) * panSpeed; + } + + zoom(delta) { + this.radius += delta * 0.05; + this.radius = Math.max(1, Math.min(50, this.radius)); + } + + // --- EVENT LISTENERS --- + attachEvents() { - // Prevent the browser context menu on right-click this.element.addEventListener('contextmenu', e => e.preventDefault()); + // --- MOUSE EVENTS --- this.element.addEventListener('mousedown', (e) => { e.preventDefault(); this.isDragging = true; - this.dragButton = e.button; this.lastMouse = {x: e.clientX, y: e.clientY}; - // Visual feedback based on action + // Base button + this.dragButton = e.button; + + // Modifier Overrides (Accessibility) + if (this.dragButton === 0) { + if (e.ctrlKey || e.metaKey) this.dragButton = 2; // Ctrl = Zoom + else if (e.shiftKey) this.dragButton = 1; // Shift = Pan + } + if (this.dragButton === 1) this.element.style.cursor = 'move'; else if (this.dragButton === 2) this.element.style.cursor = 'ns-resize'; else this.element.style.cursor = 'grabbing'; @@ -49,49 +91,79 @@ export class Camera { const dy = e.clientY - this.lastMouse.y; this.lastMouse = {x: e.clientX, y: e.clientY}; - if (this.dragButton === 0) { - // LEFT CLICK: Orbit - this.theta -= dx * 0.4; - this.phi += dy * 0.4; - this.phi = Math.max(-90, Math.min(90, this.phi)); - } else if (this.dragButton === 1) { - // MIDDLE CLICK: Pan - const t = this.theta * Math.PI / 180; - const p = this.phi * Math.PI / 180; + if (this.dragButton === 0) this.orbit(dx, dy); + else if (this.dragButton === 1) this.pan(dx, dy); + else if (this.dragButton === 2) this.zoom(dy); - // Camera's local Right vector mapped to world space - const rightX = Math.cos(t); - const rightZ = -Math.sin(t); + this.onChange(); + }); - // Camera's local Up vector mapped to world space - const upX = -Math.sin(p) * Math.sin(t); - const upY = Math.cos(p); - const upZ = -Math.sin(p) * Math.cos(t); + // --- TOUCH EVENTS --- + this.element.addEventListener('touchstart', (e) => { + e.preventDefault(); // Prevents mobile browser pull-to-refresh / page scrolling + this.isDragging = true; - // Scale pan speed based on zoom radius so it feels consistent - const panSpeed = this.radius * 0.0025; - - this.target[0] += (-rightX * dx + upX * dy) * panSpeed; - this.target[1] += (upY * dy) * panSpeed; - this.target[2] += (-rightZ * dx + upZ * dy) * panSpeed; - } else if (this.dragButton === 2) { - // RIGHT CLICK: Zoom - this.radius += dy * 0.05; - this.radius = Math.max(1, Math.min(50, this.radius)); + if (e.touches.length === 1) { + this.touchMode = 'orbit'; + this.lastMouse = {x: e.touches[0].clientX, y: e.touches[0].clientY}; + } else if (e.touches.length >= 2) { + this.touchMode = 'pan-zoom'; + const t1 = e.touches[0], t2 = e.touches[1]; + this.lastMouse = { + x: (t1.clientX + t2.clientX) / 2, + y: (t1.clientY + t2.clientY) / 2 + }; + this.lastPinchDist = Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY); } + }, { passive: false }); - this.onChange(); - }); - - this.element.addEventListener('wheel', (e) => { + this.element.addEventListener('touchmove', (e) => { + if (!this.isDragging) return; e.preventDefault(); - this.radius += e.deltaY * 0.01; - this.radius = Math.max(1, Math.min(50, this.radius)); - this.onChange(); - }); + + if (this.touchMode === 'orbit' && e.touches.length === 1) { + const dx = e.touches[0].clientX - this.lastMouse.x; + const dy = e.touches[0].clientY - this.lastMouse.y; + this.lastMouse = {x: e.touches[0].clientX, y: e.touches[0].clientY}; + + this.orbit(dx, dy); + this.onChange(); + } + else if (this.touchMode === 'pan-zoom' && e.touches.length >= 2) { + const t1 = e.touches[0], t2 = e.touches[1]; + const cx = (t1.clientX + t2.clientX) / 2; + const cy = (t1.clientY + t2.clientY) / 2; + const dist = Math.hypot(t1.clientX - t2.clientX, t1.clientY - t2.clientY); + + const dx = cx - this.lastMouse.x; + const dy = cy - this.lastMouse.y; + const dDist = dist - this.lastPinchDist; // Positive if spreading fingers + + this.lastMouse = {x: cx, y: cy}; + this.lastPinchDist = dist; + + this.pan(dx, dy); + this.zoom(-dDist); // Pinching out zooms in + this.onChange(); + } + }, { passive: false }); + + const onTouchEnd = (e) => { + if (e.touches.length === 0) { + this.isDragging = false; + } else if (e.touches.length === 1) { + // Seamlessly fall back to orbit if one finger is lifted + this.touchMode = 'orbit'; + this.lastMouse = {x: e.touches[0].clientX, y: e.touches[0].clientY}; + } + }; + + this.element.addEventListener('touchend', onTouchEnd); + this.element.addEventListener('touchcancel', onTouchEnd); } updateMatrices(aspectRatio) { + // ... (Keep existing updateMatrices code exactly as it is) ... const t = this.theta * Math.PI / 180; const p = this.phi * Math.PI / 180; @@ -99,7 +171,6 @@ export class Camera { const eyeY = this.target[1] + this.radius * Math.sin(p); const eyeZ = this.target[2] + this.radius * Math.cos(p) * Math.cos(t); - // Calculate dynamic Up vector to prevent gimbal lock at exactly +/- 90 degrees const upX = -Math.sin(p) * Math.sin(t); const upY = Math.cos(p); const upZ = -Math.sin(p) * Math.cos(t); @@ -107,7 +178,6 @@ export class Camera { const size = this.radius * 0.5; mat4Ortho(this.projMatrix, -size * aspectRatio, size * aspectRatio, -size, size, -50, 50); - // Pass the dynamic Up vector to the LookAt matrix mat4LookAt(this.viewMatrix, eyeX, eyeY, eyeZ, this.target[0], this.target[1], this.target[2], upX, upY, upZ); mat4Multiply(this.viewProjMatrix, this.projMatrix, this.viewMatrix); diff --git a/index.html b/index.html index 3f7db7f0..1ac5cb97 100644 --- a/index.html +++ b/index.html @@ -45,7 +45,7 @@ dis parturient montes, nascetur ridiculus mus.

-
+