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: none on the container!
  • OrbitControls handles the common case out of the box.