Skip to main content

Camera

The camera namespace moves the viewport. It is the largest surface in the protocol, because the difference between a map that feels considered and one that does not is mostly camera work.

Examples use a demonstration campus. Every snippet on this site addresses ucba, a publicly available campus with the building muntz-hall and the room 170. Substitute your own campus slug once you have one. Slugs for your campus are listed in your Waylocate console, and the addressing rules are in the reference grammar.

Reading and moving

const position = await map.camera.get();
// { center: { lat, lng }, zoom, bearing, pitch }

await map.camera.jumpTo({ zoom: 18 });
await map.camera.flyTo({ zoom: 18, bearing: 45, duration: 800 });
await map.camera.fitTo("muntz-hall");
await map.camera.cancel();
MethodArgumentEffect
get()noneCurrent position, or null while the map is not rendering
jumpTo(position)CameraPositionMoves instantly
flyTo(position)CameraPosition & CameraAnimationMoves with animation
fitTo(ref)referenceFrames whatever the reference names
cancel()noneStops any animation or path playback in progress

Position

interface CameraPosition {
center?: { lat: number; lng: number };
zoom?: number; // 0 to 22
bearing?: number; // degrees, 0 is north
pitch?: number; // degrees from vertical, 0 to 60
}

Omitted fields are left alone, so flyTo({ zoom: 18 }) zooms without moving or rotating.

Values outside these ranges are rejected with INVALID_PAYLOAD rather than clamped. Silently moving the camera somewhere that was not asked for produces a bug that is very hard to see; an error is not.

Animation

interface CameraAnimation {
duration?: number; // milliseconds, default 700. 0 is instant.
animate?: boolean; // false is instant
essential?: boolean; // animate even under reduced-motion preferences
curve?: number; // opts into a flight arc
speed?: number; // opts into a flight arc
}

Two behaviours are worth knowing before you tune anything.

flyTo eases rather than arcs. The underlying rendering library's flight animation zooms out and back in on the way to its destination. Over campus distances that reads as a rendering glitch rather than as motion, so the default here is a direct ease. Passing curve or speed, and only those, opts into the arc.

Reduced motion is honoured. When the operating system asks for reduced motion, animated moves collapse to instant ones. Set essential: true to override that for motion the visitor is waiting on. Do not set it on ambient or decorative motion, which is exactly what the preference exists to suppress.

Framing

fitTo accepts any reference from the grammar, plus two reserved words.

ReferenceFrames
campusEvery building on the campus
routeThe route currently drawn
A buildingThat building's footprint
A floorThat floor's plan view
A roomThat room
here or pin:{lat},{lng}That point

campus and route are reserved, so a building whose slug happens to be either is shadowed by them.

A reference that resolves but cannot be framed right now returns NOT_READY; fitTo("route") with no active route is the usual case. A reference that does not resolve at all returns REF_NOT_FOUND.

Gesture locks

await map.camera.setInteractions({ pan: false, zoom: false, rotate: false });
AxisCovers
panDrag to pan, and arrow keys
zoomScroll, pinch, double-click, box zoom
rotateBearing and pitch together

Bearing and pitch cannot be separated, because the underlying drag handler produces both from one gesture.

Omitted axes are left as they are. Locking all three is the usual configuration for an ambient or decorative map. It does not disable selection clicks; use chrome flags for that.

Locks do not suspend the map's own framing. Selecting a building still flies to it, and a route still refits when the layout changes. The product's behaviour is not switched off because a host took the camera once.

Basemap

await map.camera.setBasemap("satellite");

Accepts "standard" or "satellite".

Scripted motion

Scroll runs backwards and animations do not, so a camera driven from page scroll cannot be built out of flyTo calls. Send the path once, then scrub it.

await map.camera.setPath({
keyframes: [
{ at: 0, ref: "campus" },
{ at: 0.5, camera: { zoom: 17, bearing: 90 }, easing: "easeInOut" },
{ at: 1, ref: "muntz-hall/170" },
],
});

window.addEventListener("scroll", () => {
map.camera.setProgress(window.scrollY / maxScroll);
});

setProgress is cheap enough to call from a scroll handler. The map damps jittery input on its own frame clock and interpolates a pose per frame, so you do not need to throttle it yourself.

interface CameraPathKeyframe {
at: number; // 0 to 1, ascending; first 0, last 1
camera?: CameraPosition; // an explicit pose
ref?: string; // or a reference, framed as fitTo would
easing?: "linear" | "easeIn" | "easeOut" | "easeInOut";
floorId?: string; // route paths only
}

interface CameraPath {
keyframes: CameraPathKeyframe[];
smoothing?: number; // 0 follows input exactly; default 0.18
}

Reference keyframes frame without selecting, exactly like fitTo. A path that ends on a room on the third floor shows the third floor's camera over whichever floor is currently active unless you also send selection.setRoom.

Bearing is interpolated literally rather than by shortest arc: 0 to 360 is a full revolution, and 350 to 370 turns twenty degrees through north. Guessing the shorter direction would make a complete orbit impossible to express. Longitude does take the shorter path across the antimeridian, and zoom interpolates in zoom space.

Playing a path

await map.camera.playPath({ keyframes, duration: 6000, loop: false });
await map.camera.orbit({ ref: "muntz-hall", secondsPerRevolution: 20 });
await map.camera.followRoute({ mode: "time", duration: 8000 });
MethodPurpose
playPath(path)Plays a path once, or on a loop
orbit(options)Builds a looping bearing path around a reference and plays it
followRoute(options)Builds a path from the route currently drawn

orbit is a shorthand: it resolves a centre, constructs a four-quarter bearing path, and plays it looping.

followRoute takes mode: "progress" to load the path for you to scrub, or mode: "time" to play it once over duration milliseconds.

Both playPath and orbit jump to their final pose under reduced-motion preferences unless essential: true. setProgress always scrubs, because scroll is visitor-driven rather than auto-playing motion. If you loop an orbit, providing a way to stop it is your responsibility.

camera.pathEnded fires when playback finishes, with a reason of completed, cancelled, or replaced.

Watching the camera

camera.changed and camera.idle are opt-in, because a single pan gesture would otherwise produce hundreds of messages. Subscribing through map.on() opts in automatically and unsubscribes when your last listener goes.

map.on("camera.idle", (position, meta) => {
if (meta.source === "user") savePosition(position);
});

camera.changed is throttled to roughly ten updates per second. camera.idle fires once after motion settles.

The source on a camera event is decided when the motion starts and held for the whole gesture. A fly triggered by selecting a building is system, not host, even when a host command caused the selection.

Next

  • Events for subscription mechanics.
  • Errors for what rejections mean.