Navigator
A navigator is an interactive pan / zoom / brush controller — the flat-scene analogue of the 3D Camera. It owns a single affine view transform ({ k, x, y } — a uniform scale plus a translation) and an optional rectangular brush, and it emits events as that transform changes. The navigator never repaints anything itself; you react to its events and re-render your scene under the new transform.
The controller is split in two, mirroring the Context / DOMContext split:
Navigator(in@ripl/core) is context-agnostic. It owns the view model and the imperative commands that mutate it (panBy,zoomBy,fitBounds, …) but attaches no input listeners. Non-DOM environments can drive it programmatically.DOMNavigator(in@ripl/dom, viacreateNavigator) extends it to translate real wheel / pointer / touch gestures into those commands.
NOTE
For the full API, see the Core API Reference.
Demo
Drag to pan, use the wheel (or pinch) to zoom toward the pointer, and shift-drag to zoom into a region. Translation is unbounded, so you can roam past the framed content — use the buttons to jump back.
Creating a navigator
createNavigator binds a DOMNavigator to a context's element and (optionally) wires up gestures. Pass interactions: true to enable everything, or an object to enable them individually — each interaction also accepts a sensitivity multiplier.
import {
createNavigator,
} from '@ripl/web';
const navigator = createNavigator(context, {
// Clamp zoom between 0.5× and 20×.
scaleExtent: [0.5, 20],
interactions: {
zoom: {
sensitivity: 1.5,
},
pan: true,
brush: true,
},
});The gesture model is intentionally Figma-like: click-and-hold (left or middle button, with or without ⌘/Ctrl) pans, the wheel and two-finger pinch zoom toward the pointer, and shift-drag brushes a rectangle. Call navigator.destroy() to detach every listener.
The view transform
The transform is a plain { k, x, y } object: a content-space point p maps to the screen as k · p + [x, y]. Two helpers convert between spaces:
navigator.applyPoint([100, 50]); // content → screen
navigator.invertPoint([320, 240]); // screen → contenttransform returns a copy, so mutating it never affects the navigator — always go through a command.
Commands
Every command clamps k to the scale extent and emits an event when it changes the view.
| Command | Description |
|---|---|
panBy(dx, dy) | Pan by a pixel delta |
panTo(x, y) | Pan to an absolute translation |
zoomBy(factor, center?) | Multiply the zoom, keeping center (screen px) fixed |
zoomTo(k, center?) | Set an absolute zoom factor |
centerOn(point, viewport?) | Centre a content point in the viewport, keeping zoom |
fitBounds(bounds, options?) | Zoom and pan so a content box fills the viewport |
reset() | Return to the identity transform |
setTransform(transform) | Replace the transform outright |
Because translation is unbounded, fitBounds and centerOn can jump the view to content that currently lies entirely outside the viewport — the "navigate anywhere" behaviour the demo's Frame all button relies on.
// Zoom to 2× toward the top-left corner of the viewport.
navigator.zoomTo(2, [0, 0]);
// Frame a bounding box with 24px of padding.
navigator.fitBounds({ x0: 40, y0: 40, x1: 520, y1: 300 }, { padding: 24 });Events
| Event | Payload | Fires when |
|---|---|---|
zoom | NavigatorTransform | The zoom factor changes |
pan | NavigatorTransform | The translation changes |
change | NavigatorTransform | Any transform change (zoom or pan) |
brush | NavigatorBrush | The brush selection updates |
brushend | NavigatorBrush | null | A brush gesture finishes (or is cleared) |
Subscribe to change for the common "repaint on any view change" case; use zoom / pan when you need to react to just one.
navigator.on('change', event => scene.update(event.data));
navigator.on('brushend', event => event.data && zoomToSelection(event.data));Rescaling chart axes
A cartesian chart doesn't transform its rendered geometry — it rescales its axis domains and redraws. rescaleDomain does exactly that: it inverts the transformed pixel range back through a scale to produce the visible data domain.
import {
rescaleDomain,
} from '@ripl/web';
navigator.on('change', transform => {
const [x0, x1] = rescaleDomain(xScale, transform, [0, width]);
chart.setDomain([x0, x1]);
});This keeps geometry crisp at any zoom level (no scaled stroke widths or blurred text) and is how the opt-in navigator support on the cartesian charts works under the hood.
Programmatic use
Because the base Navigator in @ripl/core owns the view model without any input wiring, you can drive it directly — for animations, tests, or non-DOM contexts — and consume the same events:
import {
Navigator,
} from '@ripl/web';
const navigator = new Navigator({
viewport: {
width: 800,
height: 600,
},
});
navigator.on('change', ({ k, x, y }) => render(k, x, y));
navigator.zoomTo(4, [400, 300]);