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.
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();
| Method | Argument | Effect |
|---|---|---|
get() | none | Current position, or null while the map is not rendering |
jumpTo(position) | CameraPosition | Moves instantly |
flyTo(position) | CameraPosition & CameraAnimation | Moves with animation |
fitTo(ref) | reference | Frames whatever the reference names |
cancel() | none | Stops 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.
| Reference | Frames |
|---|---|
campus | Every building on the campus |
route | The route currently drawn |
| A building | That building's footprint |
| A floor | That floor's plan view |
| A room | That 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 });
| Axis | Covers |
|---|---|
pan | Drag to pan, and arrow keys |
zoom | Scroll, pinch, double-click, box zoom |
rotate | Bearing 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 });
| Method | Purpose |
|---|---|
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.