Skip to content

feat(maps): pinch, twist, two-finger tilt and tap zoom on touch - #42

Merged
apresmoi merged 2 commits into
mainfrom
feat/maps-touch-gestures
Sep 10, 2026
Merged

apresmoi merged 2 commits into
mainfrom
feat/maps-touch-gestures

Conversation

@apresmoi

Copy link
Copy Markdown
Owner

Multi-touch on the map: pinch to zoom, twist to turn, two fingers to pitch, and both tap zooms.

The gestures. A pinch scales 1:1 with the finger separation, anchored on their midpoint so the ground under them stays put. A twist turns the bearing 1:1. Two fingers dragging together pitch the camera at GLYPH_MAP_TILT_DRAG_DEG_PER_PX — the same 0.5 °/px constant the mouse orient stroke already uses. Double-tap zooms in a level about the tapped point, two-finger tap zooms out, and double-tap-hold-drag zooms continuously. One-finger pan is untouched, and lifting one finger from a pinch continues the pan with no seam.

Taps zoom outright rather than easing, for the reason applyWheel already gives about the wheel: an eased span makes getView().span disagree with the gesture and feeds tile LOD a span nobody asked for.

Pan, pinch and twist compose — they are the three components of one similarity transform of the finger pair, and each has its own activation threshold, so a pure gesture moves only its own quantity. Those thresholds are a ratio (7.2% of separation) and an arc (25 px along the finger circle), not pixel counts, so a wide grip and a narrow one are the same gesture. Pitch is exclusive and latched once: recognising it needs the fingers side by side, both travelled, both more vertical than horizontal and both the same way, with a 100 ms grace for one finger leading — the clause without which a slightly uneven drag zooms before it pitches.

Sourcing. Google publishes the gesture vocabulary and not one threshold, rate or arbitration rule, so the feel comes from MapLibre — the same library this widget's 0.5 °/px pitch and 0.8 °/px bearing rates already came from — cross-checked against MapLibre Native. One documented disagreement: Google's Android docs say two fingers down increases tilt, while MapLibre GL JS, MapLibre Native and the Maps app all raise pitch on an upward drag. This follows the products, and the existing Ctrl+drag.

One thing that is ours rather than MapLibre's. MapLibre reads TouchEvent.touches, so every event carries both fingers; a Pointer Event carries one, so a pair assembled mid-recognition holds one live position and one stale one — and the zoom then magnifies that error. Measured, a 5 px reference error grew to 5.38 / 6.15 / 6.92 px through 1.08× / 1.23× / 1.38×, exactly 5 × magnification. Every event that arrives undecided now applies nothing and re-bases, the resolving one included, so each increment is a true similarity between two real configurations.

No new controls flag: the three that exist are capabilities rather than devices, so wheel covers pinch and both tap zooms, tilt covers the two-finger pitch and the twist, and drag covers pan. The host takes touch-action: none, restored on destroy().

Everything routes through the existing state setters and the single motion loop, so the cover clamp, getMaxTilt(), bearing normalisation, the URL write and interactiveDownscale all still apply.

Also fixed: /maps held its drag-density press as a boolean, so lifting one finger from a pinch settled the map back to full detail mid-gesture and never went coarse again for the rest of the stroke.

Verified in a Pixel 7 context with real multi-touch: double-tap halves the span exactly, two-finger tap doubles it exactly, drag-zoom lands on 144.08 / 2^0.9375, and a two-finger drag at Zurich takes the tilt to its live getMaxTilt() ceiling with span and bearing bit-identical.

@apresmoi
apresmoi merged commit f25d715 into main Sep 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant