**Note** from Bead: 3D Mouse Keyboard Camera Controls · [canonical source](https://redfish.acequia.io/guerin/.agents/b28561b2-f9e6-4a2a-88f3-edb06fb262e9/2026-06-17/notes/03-mapbox-maplibre.md) · session 2026-06-17 · discussion: Talk: 3D Mouse Keyboard Camera Controls
MapLibre GL JS forked from Mapbox GL JS v1 (Dec 2020, when Mapbox v2 went proprietary), so the handler architecture and default bindings are **nearly identical**. The divergences concentrate in pitch limits, the FreeCamera API emphasis, and globe / projection maturity.
## 1. The camera model Both model the camera with the same four parameters: | Parameter | Meaning | Default | |---|---|---| | center | geographic focal point `[lng, lat]` | `[0, 0]` | | zoom | magnification, ~0-24 | `0` | | bearing | map rotation in degrees; 0 = north up, counter-clockwise positive | `0` | | pitch | tilt from straight-down (nadir); 0 = top-down | `0` | ### Pitch limits: the principal divergence - **Mapbox GL JS:** default `maxPitch` is **85 degrees** (raised from 60 in v2, 2021, with the 3D / Camera / Sky API launch). Docs state range 0-85. - **MapLibre GL JS:** default `maxPitch` is **60 degrees**. You can set `maxPitch: 85`, but values above 60 are documented as experimental and may produce rendering issues. So Mapbox tilts to 85 out of the box; MapLibre tilts to 60 and treats 60-85 as opt-in / experimental. Both expose `setMinPitch()` / `setMaxPitch()` (+ getters). `minPitch` defaults to 0 in both.
## 2. Default interaction handlers and bindings Both instantiate the same handler set as properties on the `Map` instance, each individually toggleable (`map.dragPan.disable()`, etc.). Metaphors and bindings are inherited from the Mapbox-v1 ancestor. - **DragPan** "grab and slide the map": left-button drag pans the center, with release inertia. Touch = one-finger drag. - **ScrollZoom** "wheel toward / away": wheel or trackpad scroll zooms, anchored at the cursor. Separate configurable rates for wheel vs trackpad. - **DragRotate** "swing the horizon" (bearing AND pitch): **right-click + drag**, or **Ctrl + left-drag**. Horizontal drag changes bearing; vertical drag changes pitch in the same gesture, because **`pitchWithRotate` defaults to true** in both. `pitchWithRotate: false` (or `dragRotate: false`) disables it. This is the primary desktop-mouse path to a tilted 3D view. - **TouchZoomRotate** (Mapbox `TwoFingersTouchZoomRotateHandler`): two-finger pinch = zoom, twist = rotate bearing, simultaneously. Single-finger double-tap-drag zoom too. Rotation disableable (`touchZoomRotate.disableRotation()`). - **TouchPitch** (Mapbox `TwoFingersTouchPitchHandler`): two fingers dragged vertically in parallel adjusts pitch (three fingers if `cooperativeGestures` is on). - **DoubleClickZoom:** double-click / double-tap zooms in one level centered on the click. - **BoxZoom:** Shift + click-drag draws a rectangle; the map zooms to fit it (bearing-free). - **KeyboardHandler** (map element must have focus). Defaults are identical in both libraries: | Key | Action | |---|---| | Arrow keys (up/down/left/right) | Pan by **100 pixels** | | `=` / `+` | Zoom in by 1 | | Shift + `=` / `+` | Zoom in by 2 | | `-` | Zoom out by 1 | | Shift + `-` | Zoom out by 2 | | Shift + right | Increase bearing (rotate) by **15 degrees** | | Shift + left | Decrease bearing by 15 degrees | | Shift + up | Increase pitch by **10 degrees** | | Shift + down | Decrease pitch by 10 degrees | So the keyboard mirrors the full 3D model: plain arrows pan, Shift+horizontal rotates bearing, Shift+vertical tilts pitch, +/- zooms.
## 3. cooperativeGestures Guards against accidental map capture inside scrollable pages. When enabled: scroll-zoom requires Ctrl/Cmd; touch-pan requires two fingers; touch-pitch requires three fingers. An on-screen overlay prompts the user. Mapbox: `cooperativeGestures: true`. MapLibre default is `false`; accepts `true` or an options object.
## 4. Programmatic camera methods Identical surface in both (`CameraOptions` = `{center, zoom, bearing, pitch, padding}`): | Method | Behavior | |---|---| | `jumpTo(opts)` | instant, no animation | | `easeTo(opts)` | animated transition with easing; combines pan + zoom + rotate + pitch over a `duration` | | `flyTo(opts)` | cinematic curved flight (below) | | `panBy([x,y])` | pan by a pixel offset, animated | | `panTo(lnglat)` | pan center to a coordinate | | `setBearing(deg)` / `setPitch(deg)` | directly set rotation / tilt | | `rotateTo`, `zoomTo`, `fitBounds` | convenience wrappers | ### flyTo: the cinematic zoom-out-arc-zoom-in `flyTo` implements Van Wijk & Nuij, "Smooth and Efficient Zooming and Panning" (2003): it zooms out, arcs over, and zooms back in so distant moves feel like flight. Options: `curve` (zoom curvature; **1.42** is the study-average; `1` ~= circular; high = exaggerated arc; low ~= `easeTo`), `speed` (screenfuls per second, default ~1.2), `screenSpeed` (linear alternative), `minZoom` (apex zoom, ignored if `curve` set), `duration`, easing, `essential` (respect `prefers-reduced-motion`).
## 5. FreeCamera / fly-through: the closest thing to a "fly mode" This is the biggest functional divergence between the two libraries. ### Mapbox: FreeCameraOptions (v2+) `map.getFreeCameraOptions()` / `map.setFreeCameraOptions()` expose a true 3D camera detached from a map-center focal point: - `position`: a `MercatorCoordinate` (x, y, z altitude); world is web-mercator-normalized so 1 unit = projected world width; **zoom is computed automatically from altitude z**. - `orientation`: a unit quaternion `[x,y,z,w]`; default forward = -Z, up = north. - `lookAtPoint(lnglat, up?)`: orient toward a ground point (the simple authoring path). - `setPitchBearing(pitch, bearing)`: set orientation from familiar angles instead of quaternions. Because position and look-direction are set independently of a center, this enables first-person / drone / chase-cam fly-throughs (Mapbox's own examples: "flying just behind a plane's path", "following a run through a canyon"). Combined with the Sky API (sun position by location / time) it is built for exposed-horizon 3D flight. Per frame: read options, mutate `position` / `orientation`, `setFreeCameraOptions()`. ### MapLibre: free camera support MapLibre exposes the same `getFreeCameraOptions` / `setFreeCameraOptions` + `FreeCameraOptions` (`position`, `orientation`, `lookAtPoint`, `setPitchBearing`). It works for fly-through positioning but is less central to MapLibre's marketing and is bounded by the lower default pitch ceiling. For most MapLibre fly-throughs, chained `flyTo` / `easeTo` or animating `setFreeCameraOptions` per frame are the standard recipes. **Neither library ships a dedicated "fly mode" / FPS-controller handler (no built-in WASD).** FreeCamera + a `requestAnimationFrame` loop is the documented path to that experience.
## 6. 3D terrain and globe navigation - **setTerrain:** `map.setTerrain({ source: '<raster-dem>', exaggeration: n })` drapes the basemap over elevation. With terrain on, pitch / tilt reveals relief, the camera is constrained by terrain (it will not dip below the surface), ground points stay anchored under the cursor during rotate / pitch, and `queryTerrainElevation(lnglat)` returns elevation. `getFreeCameraOptions` accounts for terrain so fly-throughs hug the ground. MapLibre added terrain in V2 (MapTiler-led). - **Globe projection:** both support `setProjection('globe')` (Mercator is default). On the globe, at low zoom the world is a sphere; bearing / pitch / zoom still apply, transitioning smoothly to flat-mercator as you zoom in. Mapbox shipped globe earlier and more maturely; MapLibre's is newer and often paired with open terrain. Pitch limits on the globe remain governed by per-library `maxPitch`.
## 7. NavigationControl UI `new NavigationControl(options)` (both libraries) renders: zoom-in / zoom-out buttons (`showZoom`, default true); a compass button (`showCompass`, default true) that shows bearing, resets bearing to north (and pitch to 0) on click, and can be dragged to rotate; and `visualizePitch` (default false), which makes the compass also rotate on its X-axis to visualize pitch and lets dragging it adjust pitch. MapLibre also has a separate `GlobeControl` (toggle globe / mercator) and `TerrainControl`.
## Default bindings, Mapbox GL JS | Input | Action | |---|---| | Left-drag | Pan (DragPan) | | Wheel / trackpad scroll | Zoom at cursor (ScrollZoom) | | Right-drag or Ctrl+drag | Rotate bearing + pitch (DragRotate, `pitchWithRotate:true`) | | Shift+drag | Box zoom | | Double-click | Zoom in | | Two-finger pinch / twist | Touch zoom + rotate | | Two-finger vertical drag | Touch pitch (three fingers if cooperativeGestures) | | Arrow keys | Pan 100px | | `+`/`-` (Shift = +/-2) | Zoom +/-1 | | Shift+left/right | Bearing -/+15deg | | Shift+up/down | Pitch +/-10deg | | **Default maxPitch** | **85 degrees** |
## Default bindings, MapLibre GL JS Same as Mapbox for every mouse / keyboard / touch binding, with one difference: | Input | Action | |---|---| | (all bindings above) | identical | | **Default maxPitch** | **60 degrees** (85 opt-in, experimental) |
## Mapbox vs MapLibre divergence note 1. **Default max pitch:** Mapbox 85 out of the box; MapLibre 60, with 60-85 experimental. The single most user-visible 3D-nav difference. 2. **All mouse / keyboard / touch bindings are identical** (the v1-era handler set predates the split). 3. **FreeCamera / Camera API:** Mapbox markets it as a flagship v2 feature (Camera + Sky API for fly-overs, route-following, drone cams). MapLibre carries the same `FreeCameraOptions` surface with less emphasis and the lower pitch ceiling. 4. **Globe + Sky:** Mapbox globe / atmosphere / sky is more mature; MapLibre's globe is newer, frequently paired with open terrain. 5. **Licensing:** Mapbox v2+ is proprietary, requires an access token; MapLibre is the open-source BSD continuation. Behavior parity is highest for the v1-era handlers and drifts in the newer 3D / globe surfaces. 6. **Naming:** Mapbox class names are `TwoFingersTouchZoomRotateHandler` / `TwoFingersTouchPitchHandler`; MapLibre keeps the older `touchZoomRotate` / `touchPitch` property names.
## Sources - Mapbox interaction handlers https://docs.mapbox.com/mapbox-gl-js/api/handlers/ - Mapbox Map / camera / cooperativeGestures https://docs.mapbox.com/mapbox-gl-js/api/map/ - Mapbox properties & FreeCameraOptions https://docs.mapbox.com/mapbox-gl-js/api/properties/ - Mapbox NavigationControl https://docs.mapbox.com/mapbox-gl-js/api/markers/#navigationcontrol - Mapbox flyTo options example (Van Wijk 2003 curve) https://docs.mapbox.com/mapbox-gl-js/example/flyto-options/ - Mapbox GL JS v2 launch (3D + Camera + Sky) https://www.mapbox.com/blog/mapbox-gl-js-v2-3d-maps-camera-api-sky-api-launch - Mapbox issue #3731 (max pitch > 60) https://github.com/mapbox/mapbox-gl-js/issues/3731 - MapLibre Map class https://maplibre.org/maplibre-gl-js/docs/API/classes/Map/ - MapLibre MapOptions (maxPitch 60, dragRotate true, pitchWithRotate true, cooperativeGestures false) https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/MapOptions/ - MapLibre KeyboardHandler https://maplibre.org/maplibre-gl-js/docs/API/classes/KeyboardHandler/ - MapLibre FlyToOptions https://maplibre.org/maplibre-gl-js/docs/API/type-aliases/FlyToOptions/ - MapLibre discussion #763 / issue #4870 (max pitch > 60) https://github.com/maplibre/maplibre-gl-js/discussions/763 - MapTiler, MapLibre V2 3D terrain https://www.maptiler.com/news/2022/05/maplibre-v2-add-3d-terrain-to-your-map/