Skip to content

Commit d875811

Browse files
committed
implement touch events for mobile
1 parent 255121a commit d875811

3 files changed

Lines changed: 420 additions & 14 deletions

File tree

webgpu/engine/input.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,40 @@ class InputHandler {
1717
this._isRotating = false;
1818
this._isPanning = false;
1919

20+
// Multi-touch gesture state
21+
this._activeTouches = new Map(); // id -> {x, y}
22+
this._prevTouchDist = null;
23+
this._prevTouchAngle = null;
24+
this._prevTouchCentroid = null;
25+
26+
// Prevent browser scroll/zoom on the canvas
27+
canvas.style.touchAction = 'none';
28+
2029
// Bind handlers for clean removal
2130
this._onPointerDown = this._onPointerDown.bind(this);
2231
this._onPointerUp = this._onPointerUp.bind(this);
2332
this._onPointerMove = this._onPointerMove.bind(this);
2433
this._onWheel = this._onWheel.bind(this);
2534
this._onDblClick = this._onDblClick.bind(this);
2635
this._onContextMenu = (ev) => ev.preventDefault();
36+
this._onTouchStart = this._onTouchStart.bind(this);
37+
this._onTouchMove = this._onTouchMove.bind(this);
38+
this._onTouchEnd = this._onTouchEnd.bind(this);
2739

2840
canvas.addEventListener('pointerdown', this._onPointerDown);
2941
canvas.addEventListener('pointerup', this._onPointerUp);
3042
canvas.addEventListener('pointermove', this._onPointerMove);
3143
canvas.addEventListener('wheel', this._onWheel, { passive: false });
3244
canvas.addEventListener('dblclick', this._onDblClick);
3345
canvas.addEventListener('contextmenu', this._onContextMenu);
46+
canvas.addEventListener('touchstart', this._onTouchStart, { passive: false });
47+
canvas.addEventListener('touchmove', this._onTouchMove, { passive: false });
48+
canvas.addEventListener('touchend', this._onTouchEnd);
49+
canvas.addEventListener('touchcancel', this._onTouchEnd);
3450
}
3551

52+
// --- Pointer events (mouse & single touch fallback) ---
53+
3654
_onPointerDown(ev) {
3755
ev.preventDefault();
3856
if (ev.button === 0 && !ev.shiftKey && !ev.ctrlKey && !ev.altKey) {
@@ -49,6 +67,9 @@ class InputHandler {
4967
}
5068

5169
_onPointerMove(ev) {
70+
// Suppress pointer-based rotation/pan while a multi-touch gesture is active
71+
if (this._activeTouches.size >= 2) return;
72+
5273
const t = this.camera.transform;
5374
if (this._isRotating) {
5475
t.rotate(0.3 * ev.movementY, 0.3 * ev.movementX);
@@ -82,6 +103,111 @@ class InputHandler {
82103
}
83104
}
84105

106+
// --- Multi-touch gesture handling ---
107+
108+
_onTouchStart(ev) {
109+
ev.preventDefault();
110+
for (const touch of ev.changedTouches) {
111+
this._activeTouches.set(touch.identifier, { x: touch.clientX, y: touch.clientY });
112+
}
113+
if (this._activeTouches.size >= 2) {
114+
this._initGestureState();
115+
}
116+
}
117+
118+
_onTouchMove(ev) {
119+
ev.preventDefault();
120+
for (const touch of ev.changedTouches) {
121+
if (this._activeTouches.has(touch.identifier)) {
122+
this._activeTouches.set(touch.identifier, { x: touch.clientX, y: touch.clientY });
123+
}
124+
}
125+
if (this._activeTouches.size >= 2) {
126+
this._handleMultiTouchGesture();
127+
}
128+
}
129+
130+
_onTouchEnd(ev) {
131+
for (const touch of ev.changedTouches) {
132+
this._activeTouches.delete(touch.identifier);
133+
}
134+
// Reset gesture state when fewer than 2 touches remain
135+
if (this._activeTouches.size < 2) {
136+
this._prevTouchDist = null;
137+
this._prevTouchAngle = null;
138+
this._prevTouchCentroid = null;
139+
}
140+
}
141+
142+
/** Compute initial distance, angle, and centroid for a two-finger gesture. */
143+
_initGestureState() {
144+
const [a, b] = this._getTwoTouches();
145+
this._prevTouchDist = this._distance(a, b);
146+
this._prevTouchAngle = this._angle(a, b);
147+
this._prevTouchCentroid = this._centroid(a, b);
148+
}
149+
150+
/** Process ongoing two-finger gesture: pinch-zoom, rotation, and pan. */
151+
_handleMultiTouchGesture() {
152+
const [a, b] = this._getTwoTouches();
153+
const dist = this._distance(a, b);
154+
const angle = this._angle(a, b);
155+
const centroid = this._centroid(a, b);
156+
const t = this.camera.transform;
157+
158+
// Pinch-to-zoom
159+
if (this._prevTouchDist != null && this._prevTouchDist > 0) {
160+
const scaleFactor = dist / this._prevTouchDist;
161+
t.scale(scaleFactor, t._center);
162+
}
163+
164+
// Two-finger rotation
165+
if (this._prevTouchAngle != null) {
166+
let angleDelta = angle - this._prevTouchAngle;
167+
// Normalize to [-PI, PI]
168+
if (angleDelta > Math.PI) angleDelta -= 2 * Math.PI;
169+
if (angleDelta < -Math.PI) angleDelta += 2 * Math.PI;
170+
const degrees = angleDelta * (180 / Math.PI);
171+
t.rotate(0, degrees);
172+
}
173+
174+
// Two-finger pan (centroid movement)
175+
if (this._prevTouchCentroid != null) {
176+
const dx = centroid.x - this._prevTouchCentroid.x;
177+
const dy = centroid.y - this._prevTouchCentroid.y;
178+
t.translate(0.01 * dx, -0.01 * dy);
179+
}
180+
181+
this._prevTouchDist = dist;
182+
this._prevTouchAngle = angle;
183+
this._prevTouchCentroid = centroid;
184+
185+
this.camera._notify();
186+
this.onRender();
187+
}
188+
189+
/** Return the first two active touch positions. */
190+
_getTwoTouches() {
191+
const iter = this._activeTouches.values();
192+
return [iter.next().value, iter.next().value];
193+
}
194+
195+
_distance(a, b) {
196+
const dx = b.x - a.x;
197+
const dy = b.y - a.y;
198+
return Math.sqrt(dx * dx + dy * dy);
199+
}
200+
201+
_angle(a, b) {
202+
return Math.atan2(b.y - a.y, b.x - a.x);
203+
}
204+
205+
_centroid(a, b) {
206+
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
207+
}
208+
209+
// --- Cleanup ---
210+
85211
dispose() {
86212
const c = this.canvas;
87213
c.removeEventListener('pointerdown', this._onPointerDown);
@@ -90,6 +216,10 @@ class InputHandler {
90216
c.removeEventListener('wheel', this._onWheel);
91217
c.removeEventListener('dblclick', this._onDblClick);
92218
c.removeEventListener('contextmenu', this._onContextMenu);
219+
c.removeEventListener('touchstart', this._onTouchStart);
220+
c.removeEventListener('touchmove', this._onTouchMove);
221+
c.removeEventListener('touchend', this._onTouchEnd);
222+
c.removeEventListener('touchcancel', this._onTouchEnd);
93223
}
94224
}
95225

0 commit comments

Comments
 (0)