aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/viewer/js/globecontrols.js
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-07 00:14:38 +0200
committergodosa <godosa@godosa.eu>2026-10-07 00:14:38 +0200
commit3443c1c65e9f1753e1e656b35d08416c1fa298f2 (patch)
tree4e43236f460145a4d75d1b4616dcb7aa6ef08f51 /viewer/js/globecontrols.js
downloadworldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.tar.gz
worldmap-viewer-3443c1c65e9f1753e1e656b35d08416c1fa298f2.zip
worldmap-viewer: initial public history
Diffstat (limited to 'viewer/js/globecontrols.js')
-rw-r--r--viewer/js/globecontrols.js88
1 files changed, 88 insertions, 0 deletions
diff --git a/viewer/js/globecontrols.js b/viewer/js/globecontrols.js
new file mode 100644
index 0000000..81a0807
--- /dev/null
+++ b/viewer/js/globecontrols.js
@@ -0,0 +1,88 @@
+// globecontrols.js — mouse input for the globe camera. Left-drag pans (the grabbed ground follows the pointer);
+// the compass heading stays put while panning; right-drag or Shift+drag turns (sideways) and tilts (up = toward the horizon); the wheel zooms toward the target.
+// Touch: one finger pans; two fingers pinch (zoom), twist (turn) and move up/down together (tilt).
+import { rotatePose, keepHeading } from './camera.js';
+import { Touches } from './gestures.js';
+
+export const TURN_DEG_PER_PX = 0.25;
+export const TILT_DEG_PER_PX = 0.2;
+const WHEEL_PER_PX = 0.001;
+
+export class GlobeControls {
+ constructor(view, el) {
+ Object.assign(this, { view, el, enabled: false, drag: null, touches: new Touches() });
+ el.addEventListener('pointerdown', e => this.#down(e));
+ el.addEventListener('pointermove', e => this.#move(e));
+ el.addEventListener('pointerup', e => this.#up(e));
+ el.addEventListener('pointercancel', e => this.#up(e));
+ el.addEventListener('wheel', e => this.#wheel(e), { passive: false });
+ el.addEventListener('contextmenu', e => { if (this.enabled) e.preventDefault(); }); // right-drag turns the globe
+ }
+
+ #at(e) {
+ const r = this.el.getBoundingClientRect();
+ return { x: e.clientX - r.left, y: e.clientY - r.top };
+ }
+
+ #down(e) {
+ if (!this.enabled) return;
+ const at = this.#at(e), turn = e.button === 2 || (e.button === 0 && e.shiftKey);
+ if (e.pointerType === 'touch') {
+ this.touches.down(e.pointerId, at);
+ try { this.el.setPointerCapture?.(e.pointerId); } catch { /* a synthetic or already-ended pointer */ }
+ if (this.touches.size >= 2) { this.drag = null; return; } // a second finger: a gesture, not a pan
+ }
+ if (e.button !== 0 && !turn) return;
+ this.#grab(at, e.pointerId, turn);
+ }
+
+ #grab(at, id, turn = false) {
+ const grab = turn ? null : this.view.pickVec(at.x, at.y);
+ if (!turn && !grab) return; // pressed on space or sky: nothing to hold
+ this.drag = { turn, grab, r: grab && Math.hypot(grab[0], grab[1], grab[2]), x: at.x, y: at.y, id };
+ try { this.el.setPointerCapture?.(id); } catch { /* a synthetic or already-ended pointer */ }
+ }
+
+ #move(e) {
+ if (!this.enabled) return;
+ if (e.pointerType === 'touch') {
+ const g = this.touches.move(e.pointerId, this.#at(e));
+ if (g) return this.#gesture(g);
+ }
+ const d = this.drag;
+ if (!d || e.pointerId !== d.id) return; // one pointer drives
+ const at = this.#at(e), p = this.view.pose;
+ if (d.turn) {
+ this.view.setPose({ ...p, heading: p.heading + (at.x - d.x) * -TURN_DEG_PER_PX, pitch: p.pitch - (at.y - d.y) * TILT_DEG_PER_PX });
+ Object.assign(d, at);
+ return;
+ }
+ for (let i = 0; i < 4; i++) { // a rigid turn would swing north around: keep the heading, re-aim, repeat (converges fast)
+ const cur = this.view.pickVec(at.x, at.y, d.r); // the pointer ray at the grabbed ground's height
+ if (!cur) break;
+ this.view.setPose(keepHeading(p, rotatePose(this.view.pose, cur, d.grab)));
+ }
+ }
+
+ #gesture(g) { // pinch = zoom, twist = turn with the fingers, both up/down = tilt
+ const p = this.view.pose;
+ this.view.setPose({ ...p, range: p.range / g.scale, heading: p.heading - g.rot, pitch: p.pitch - g.tilt * TILT_DEG_PER_PX });
+ }
+
+ #up(e) {
+ try { if (this.drag) this.el.releasePointerCapture?.(this.drag.id); } catch { /* not captured */ }
+ this.drag = null;
+ if (e?.pointerType !== 'touch') return;
+ const wasPair = this.touches.size >= 2;
+ this.touches.up(e.pointerId);
+ const rest = wasPair && this.touches.size === 1 ? this.touches.other(e.pointerId) : null;
+ if (rest && this.enabled) this.#grab(rest.at, rest.id); // one finger left: it pans on from where it is
+ }
+
+ #wheel(e) {
+ if (!this.enabled) return;
+ e.preventDefault();
+ const px = e.deltaY * (e.deltaMode === 1 ? 33 : e.deltaMode === 2 ? 800 : 1), p = this.view.pose;
+ this.view.setPose({ ...p, range: p.range * Math.exp(px * WHEEL_PER_PX) });
+ }
+}