Three.js From Zero · Article s7-05
S7-05 Touch Gestures
Season 7 · Article 05
Touch Gestures for 3D
Pinch to zoom. Two-finger drag to pan. Twist to rotate. Swipe to orbit. Native Pointer Events track multi-touch — no library needed for most.
1. Pointer Events
Modern web. Unified mouse + touch + pen. Fires pointerdown/move/up/cancel with a pointerId per finger.
const pointers = new Map();
el.addEventListener('pointerdown', e => {
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
el.setPointerCapture(e.pointerId);
});
el.addEventListener('pointermove', e => {
if (!pointers.has(e.pointerId)) return;
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
// Analyze pointers.size:
// 1 → orbit or drag
// 2 → pinch / pan / twist
});
2. Pinch (two-finger scale)
function pinchDistance() {
const ps = [...pointers.values()];
if (ps.length < 2) return null;
const dx = ps[0].x - ps[1].x, dy = ps[0].y - ps[1].y;
return Math.sqrt(dx*dx + dy*dy);
}
let lastDist = null;
if (pointers.size === 2) {
const d = pinchDistance();
if (lastDist) {
const ratio = d / lastDist;
camera.position.multiplyScalar(1 / ratio); // zoom
}
lastDist = d;
}
3. Two-finger twist (rotate)
function twistAngle() {
const ps = [...pointers.values()];
return Math.atan2(ps[0].y - ps[1].y, ps[0].x - ps[1].x);
}
let lastAngle = null;
if (pointers.size === 2) {
const a = twistAngle();
if (lastAngle != null) {
const delta = a - lastAngle;
object.rotation.z -= delta;
}
lastAngle = a;
}
4. Don't forget touch-action
.canvas-container { touch-action: none; }
Otherwise the browser hijacks touches for scroll/pan. Always set on your 3D container.
5. Three.js OrbitControls has it built-in
const controls = new OrbitControls(camera, renderer.domElement);
// Default gestures:
// 1 finger → rotate
// 2 fingers → pan + zoom
controls.touches.ONE = THREE.TOUCH.ROTATE;
controls.touches.TWO = THREE.TOUCH.DOLLY_PAN;
If OrbitControls fits your scene, you're done.
6. Live demo — custom gesture handlers
Rotate a cube by drag. Pinch to scale. Twist two fingers to rotate Z.
–
7. Long press / double tap
let pressTimer, lastTap = 0;
el.addEventListener('pointerdown', e => {
pressTimer = setTimeout(() => onLongPress(), 500);
const now = Date.now();
if (now - lastTap < 300) onDoubleTap();
lastTap = now;
});
el.addEventListener('pointerup', () => clearTimeout(pressTimer));
8. Gesture libraries
- Hammer.js: venerable, has tap/doubletap/pinch/rotate/pan/swipe recognizers.
- @use-gesture/react: React-idiomatic hooks. drag/pinch/wheel.
- Roll your own: 30-50 lines for most needs.
9. Inertia
Users expect flick-to-keep-moving. On pointerup, record velocity, continue applying a decayed velocity each frame.
velocity *= 0.95; // decay
rotation += velocity;
if (Math.abs(velocity) < 0.001) velocity = 0;
10. Takeaways
- Pointer Events are the unified modern API.
- Track all pointers in a Map keyed by pointerId.
- Pinch = distance change. Twist = angle change. Pan = average-of-positions change.
touch-action: noneon the container!- OrbitControls handles the common case out of the box.