# Camera & Navigation

The Alps start tilted and turned. Drag to move; Ctrl+drag or right-drag changes tilt and bearing. Wheel zoom is disabled in these article examples; try it in the [maps workbench](/maps).

## Gestures

```ts
const map = createGlyphMap(host, {
  view, projection,
  controls: { drag: true, wheel: true, tilt: true },
  tilt: 55, bearing: 35,
});
```

All three control flags default to `true`. `drag` controls pan/orbit, `wheel` controls every zoom gesture, and `tilt` controls pitch and bearing independently of dragging.

| Input | Action |
|---|---|
| Drag / one finger | Pan a sheet; orbit a globe |
| Wheel / two-finger pinch | Zoom |
| Ctrl+drag / right-drag | Vertical: tilt; horizontal: bearing |
| Two-finger twist | Bearing |
| Two fingers moving vertically | Tilt |
| Double-tap / two-finger tap | Zoom in / out |
| Double-tap, hold, drag | One-handed zoom; down zooms in |
| Click | `click` event; `lngLat` is `null` off-surface |

Touch pinch and twist combine; a recognised tilt stays exclusive until release. Disable all controls to let touch gestures scroll the article through the map.

## View and limits

```ts
map.setView({ center: [-58.38, -34.6], span: 0.06 });
map.fitBounds({ west: 5.9, east: 10.5, south: 45.8, north: 47.8 });
const view = map.getView();
const maxSpan = map.getMaxSpan();
```

- `center` is `[longitude, latitude]`; `span` is degrees across the grid.
- `minSpan` defaults to `0.001` degrees.
- A sheet's `getMaxSpan()` keeps the viewport covered; its ceiling changes with shape, tilt, bearing, projection, and centre.
- A globe's maximum is its projection domain width.
- Explicit `maxSpan` disables the sheet cover limit and pan clamp, allowing margins.

## Tilt and bearing

```ts
map.setTilt(55);
map.setBearing(137);
const pose = [map.getTilt(), map.getBearing(), map.getMaxTilt()];
```

`tilt` pitches around the surface point at the view centre. Sheets default to `40` degrees; globes to `0`. `getTilt()` reports the applied, clamped value; zooming back in restores a previously limited request.

The pivot uses the drawn ground: the same triangle-sampled, elevation-windowed terrain used to plant features, including exaggeration. A supplied `groundElevation` wins; without ground, the pivot stays at the datum. Terrain arriving later re-anchors before its first render and preserves the geographic centre and shared URL. Surrounding content can shift as the new relief appears. `map.project(map.getView().center)` lands at the centre of the grid at every pitch and span (within a cell with `snapPan`). `unproject()` inverts that same ground surface; a local solve can return `null` where it has no answer.

`getMaxTilt()` is `85` degrees on sheets; globes use a scale-dependent horizon limit capped at `85`. Read it after zooming. `bearing` defaults to `0` (north up); `90` puts east up. `getBearing()` normalizes to `[0, 360)`.

## Flights

```ts
await map.flyTo({ center: [-58.38, -34.6], span: 0.06 }, { durationMs: 1200 });
await map.flyTo({ bounds: { west: 5.9, east: 10.5, south: 45.8, north: 47.8 } });
await map.idle();
const text = map.scene.output.textContent;
```

A target uses centre/span or bounds, never both. `durationMs: 0` jumps immediately; `bow: 1` disables the mid-flight zoom-out. Call `idle()` after a mutation to await tiles, geometry, motion, and transitions before reading output. It has no timeout.

Change projections with [`setProjection`](/maps/projections/#animated-transitions).

## Street-level walk mode

```ts
await map.flyTo({ center: [-58.3816, -34.6037], span: 0.01 });
await map.idle();
map.setWalk({ eyeHeight: 1.7, far: 600 });
const walk = map.getWalk();
map.setWalk(null); // restore the map view
```

Mount [OSM streets and buildings](/maps/openstreetmap/) first. Walk mode requires an orbit projection such as `glyphMapGlobe`; sheets throw `RangeError`. Enter through the handle, not construction options. Entry sets span from `far`; the application should enforce the recommended maximum entry span of `0.05` degrees.

| Option | Default |
|---|---|
| `eyeHeight` | `1.7` m |
| `fov` | `56` degrees horizontal |
| `near` / `far` | `0.5` / `600` m |
| `speed` | `6` m/s |
| `maxPitch` | `84` degrees from horizontal |
| `collision` / `sky` | `true` / `true` |

- WASD/arrows move; Shift runs ×3; hold G to pass through buildings.
- Mouse pointer lock looks; drag is the fallback. Esc releases lock; wheel does nothing.
- Collision uses `fill-extrusion` footprints; fills remain walkable.
- The sky requires `solid` mode; `sky: false` removes it.
- Calling `setWalk` again reconfigures; `setWalk(null)` restores the saved view, tilt, bearing, and camera.

### Whole-cell pans

`createGlyphMap({ ..., snapPan: true })` holds tiny pans on one rendered
cell lattice. Flat maps translate by whole output cells; globes quantize
rotation on a local tangent lattice, so shapes near the limb can still change.
The camera translation is exact on flat maps. The renderer’s screen-anchored
shading dither can still change terrain glyphs during a whole-cell step.
The held centre (`getView()` and shared links) stays exact. Zoom, tilt,
bearing and grid changes start a new lattice without snapping that change.
Walking bypasses snapping. Use `map.setSnapPan(boolean)` to toggle it.
The `/maps` View folder enables **Snap pan** by default and saves the choice
in shared links. Omission in the package keeps continuous pans.

### Experimental compositor pan

`createGlyphMap({ ..., compositorPan: true })` renders a quarter-view margin
of extra cells and translates the text container during mouse or one-finger
pans and inertial glide. Cell size stays fixed; base, detail, and stroke-overlay
text move together. Labels, markers, and other DOM hotspots hide until
settle. Compass and minimap UI remain fixed.

The cache refreshes at 80% of either margin. Globe pans approximate rotation
with the screen displacement of the cached view centre and refresh after 2°
of rotation, so features away from the centre can distort briefly. Refreshes
can change screen-anchored shading dither. Tile arrivals may force earlier
renders; unloaded tiles are not prefetched just for the extra margin.

Release and glide rest restore the normal grid and render the actual view
(with its existing `snapPan` lattice). Zoom, tilt, bearing, projection, resize,
and font changes invalidate the cache. Walking bypasses it. With this option
on, `scene.setInteracting` does not downscale the text.
Use `map.setCompositorPan(boolean)` and `map.getCompositorPan()` at runtime.
The package defaults off; `/maps` defaults **Compositor pan** on in the View
folder and preserves the toggle in shared links.
