Skip to content

Camera and 3D

The default orthographic camera makes the graph behave as a native 2D surface. The perspective camera is available on demand for structures where depth carries meaning. Both modes use the same node and edge contract.

graph.setMode("3d");
graph.fitView();

setMode() replaces the camera and controls and fits the current graph. Data, selection and IDs do not change. getMode() returns the active mode.

fitView() fits the camera to every node. Pass { nodeIds } to frame a subset, for example a highlighted path:

graph.fitView({ nodeIds: ["person:ada", "place:london"] });

Unknown IDs are ignored; an empty or unmatched list frames every node. In 2D it also resets wheel zoom. In 3D it returns to the front view and keeps every node, glow halos included, on the canvas.

flyTo({ nodeIds }) animates the camera to the framing fitView({ nodeIds }) would give, with an ease-in-out, and returns a promise:

const arrived = await graph.flyTo({ nodeIds: cluster, durationMs: 1200, distanceScale: 1.4 });

In 3D the camera and the orbit target move together and the camera keeps its current view direction instead of returning to the front view, at the fitted distance times distanceScale (default 1; above 1 leaves air around the nodes). In 2D it pans and zooms. durationMs defaults to 1200; 0 jumps, and so does every flight when the person has prefers-reduced-motion: reduce set.

The promise resolves true on arrival and false when the flight is cancelled: by pointer, wheel or key input on the canvas, another flyTo, fitView, setData, setMode, a 2D resize (which refits) or destroy(). A cancelled flight leaves the camera where it was. Unknown IDs are ignored; when none match, it resolves false without moving. Each flight frame renders once and calls onViewChange listeners, so labels follow; in 3D it does not repack node or edge instances. After arrival the far plane still holds the whole graph, so the rest of it stays visible around the framed nodes.

flyTo works with the auto-orbit: the orbit keeps turning the camera during the flight and carries on around the new target afterwards. Input still cancels the flight and pauses the orbit as usual. An ambient tour can fly on to the next cluster every few seconds:

async function tour(clusters: string[][]) {
for (const nodeIds of clusters) {
if (!(await graph.flyTo({ nodeIds, distanceScale: 1.5 }))) return; // the person took over
await new Promise((resolve) => setTimeout(resolve, 8000));
}
}

In 2D, drag pans and the wheel or a pinch zooms. One finger pans on touch screens. In 3D, drag rotates, and two fingers zoom and pan.

if (!matchMedia("(prefers-reduced-motion: reduce)").matches) {
graph.setMode("3d");
graph.setAutoOrbit({ speed: 0.07, resumeAfterMs: 3000 });
}
graph.setAutoOrbit(false);

speed is in radians per second (default 0.07, about 90 seconds per turn; above 0 and at most 2) and holds at any frame rate. Pointer and wheel input on the canvas pause the orbit; it resumes after resumeAfterMs (default 3000) without new input. A drag in progress keeps the camera still even if that delay passes. Pointer input on DOM overlays above the canvas, such as a toolbar or focus cards, does not pause it. The orbit also stops while the tab is hidden or the canvas is scrolled off screen, and resumes when visible again.

While the orbit is active graphraum runs a frame loop instead of rendering only on change. Each active frame rotates the camera, renders once, and calls onViewChange listeners so overlays can follow. It does not repack node or edge instances: in 3D they do not depend on the camera angle, so there is no per-frame CPU repacking or buffer re-upload that grows with graph size. The GPU draw still scales with what is drawn. Paused and disabled orbits run no loop. graphraum does not read prefers-reduced-motion; check it before enabling the orbit, as above. getDiagnostics().autoOrbit reports "off", "active" or "paused".

theme.endpointAttach ("boundary" by default) stops each edge at its node’s outline, stroke included, with a small clearance; "center" joins node centers. The same trim is exported for hosts that draw their own edge geometry, such as overlays or exported paths:

import { attachPointOnOutline, trimEdgeEndpoints } from "@yggy-tech/graphraum";
const { source, target } = trimEdgeEndpoints({
source: { x: 0, y: 0, z: 0 },
target: { x: 30, y: 0, z: 40 },
sourceOutline: { size: 4 },
targetOutline: { shape: "pill", width: 12, height: 3 },
clearance: 0.75, // world units, the default
mode: "3d", // "2d" by default
});
const end = attachPointOnOutline(center, farPoint, { size: 4 }, 0.75, "3d");

Pass the renderer’s mode. "2d" measures the outline in the xy plane and ignores z. "3d" stops one outline radius from each center along the 3D segment, because node billboards face the camera. The radius is taken along the edge’s xy heading, so circles meet their outline from every view; other shapes are approximate in 3D and can end slightly inside or short of the outline, depending on the camera.