Camera Follow
Track a moving target with the viewport camera. The viewport smoothly follows via linear interpolation on each animation frame, keeping the target centered.
Click play to animate a node across the canvas — the camera follows it:
<div x-data="flowCanvas({
nodes: [
{ id: 'mover', position: { x: 0, y: 0 }, data: { label: 'Follow me' } },
{ id: 'waypoint', position: { x: 500, y: 250 }, data: { label: 'Waypoint' }, draggable: false },
],
edges: [],
background: 'dots',
fitViewOnInit: true,
controls: false,
})" class="flow-container" style="height: 250px;"
x-init="
let anim = null;
let followHandle = null;
document.getElementById('demo-follow-node').addEventListener('click', () => {
if (anim) anim.stop();
if (followHandle) followHandle.stop();
$flow.update({ nodes: { mover: { position: { x: 0, y: 0 } } } });
anim = $flow.animate({
nodes: { mover: { position: { x: 500, y: 250 } } },
}, { duration: 3000, easing: 'easeInOut' });
followHandle = $flow.follow(anim, { zoom: 2 });
});
document.getElementById('demo-follow-node-reset').addEventListener('click', () => {
if (anim) { anim.stop(); anim = null; }
if (followHandle) { followHandle.stop(); followHandle = null; }
$flow.update({ nodes: { mover: { position: { x: 0, y: 0 } } } });
$flow.fitView({ duration: 300 });
});
">
<div x-flow-viewport>
<template x-for="node in nodes" :key="node.id">
<div x-flow-node="node">
<div x-flow-handle:target></div>
<span x-text="node.data.label"></span>
<div x-flow-handle:source></div>
</div>
</template>
</div>
</div>
Programmatic API
const handle = $flow.follow('node-1', {
zoom: 1.5, // optional target zoom level
padding: 0.1, // optional viewport padding
});
// Stop following
handle.stop();
Only one follow can be active at a time. Starting a new follow cancels the previous one.
Follow Targets
The first argument accepts four target types:
| Target | Type | Behavior |
|---|---|---|
| Node ID | string |
Centers on the node, respects nodeOrigin and dimensions |
| Position | { x, y } |
Centers on a fixed point in canvas coordinates |
| ParticleHandle | ParticleHandle |
Tracks a moving particle; auto-stops when particle completes |
| AnimationHandle | FlowAnimationHandle |
Tracks an animation in progress |
Options
| Option | Type | Default | Description |
|---|---|---|---|
zoom |
number |
Current zoom | Zoom level to maintain while following |
padding |
number |
— | Viewport padding around the target |
Return Value
Returns a FlowAnimationHandle with stop() and finished:
interface FlowAnimationHandle {
pause(): void;
resume(): void;
stop(): void;
reverse(): void;
readonly finished: Promise<void>;
}
Follow an Animated Node
const anim = $flow.animate({
nodes: { 'node-1': { position: { x: 800, y: 400 } } },
}, { duration: 3000 });
$flow.follow(anim, { zoom: 1.5 });
The top demo shows this in action — the camera tracks the animation handle as the node moves.
Follow a Particle
Fire a particle and have the camera track it along the edge path. The follow auto-stops when the particle completes:
const particle = $flow.sendParticle('edge-1', { duration: '3s' });
$flow.follow(particle, { zoom: 2 });
<div x-data="flowCanvas({
nodes: [
{ id: 'a', position: { x: 0, y: 0 }, data: { label: 'Start' } },
{ id: 'b', position: { x: 300, y: 150 }, data: { label: 'Middle' } },
{ id: 'c', position: { x: 600, y: 0 }, data: { label: 'End' } },
],
edges: [
{ id: 'e1', source: 'a', target: 'b' },
{ id: 'e2', source: 'b', target: 'c' },
],
background: 'dots',
fitViewOnInit: true,
controls: false,
})" class="flow-container" style="height: 250px;"
x-init="
let p1 = null, p2 = null, followHandle = null, cancelled = false;
document.getElementById('demo-follow-particle').addEventListener('click', () => {
if (p1) p1.stop();
if (p2) p2.stop();
if (followHandle) followHandle.stop();
cancelled = false;
p1 = $flow.sendParticle('e1', { color: '#DAA532', size: 6, duration: '2s' });
followHandle = $flow.follow(p1, { zoom: 2 });
p1.finished.then(() => {
if (cancelled) return;
p2 = $flow.sendParticle('e2', { color: '#8B5CF6', size: 6, duration: '2s' });
followHandle = $flow.follow(p2, { zoom: 2 });
p2.finished.then(() => {
if (cancelled) return;
$flow.fitView({ duration: 500 });
});
});
});
document.getElementById('demo-follow-particle-reset').addEventListener('click', () => {
cancelled = true;
if (p1) { p1.stop(); p1 = null; }
if (p2) { p2.stop(); p2 = null; }
if (followHandle) { followHandle.stop(); followHandle = null; }
$flow.fitView({ duration: 300 });
});
">
<div x-flow-viewport>
<template x-for="node in nodes" :key="node.id">
<div x-flow-node="node">
<div x-flow-handle:target></div>
<span x-text="node.data.label"></span>
<div x-flow-handle:source></div>
</div>
</template>
</div>
</div>
Follow a Static Position
Pan the camera to a fixed point in the canvas:
$flow.follow({ x: 500, y: 300 }, { zoom: 1 });
<div x-data="flowCanvas({
nodes: [
{ id: 'a', position: { x: 0, y: 0 }, data: { label: 'Origin' } },
{ id: 'b', position: { x: 400, y: 200 }, data: { label: 'Far away' } },
],
edges: [
{ id: 'e1', source: 'a', target: 'b' },
],
background: 'dots',
fitViewOnInit: true,
controls: false,
})" class="flow-container" style="height: 220px;"
x-init="
let followHandle = null;
document.getElementById('demo-follow-pos-a').addEventListener('click', () => {
if (followHandle) followHandle.stop();
followHandle = $flow.follow({ x: 0, y: 0 }, { zoom: 2 });
});
document.getElementById('demo-follow-pos-b').addEventListener('click', () => {
if (followHandle) followHandle.stop();
followHandle = $flow.follow({ x: 400, y: 200 }, { zoom: 2 });
});
document.getElementById('demo-follow-pos-reset').addEventListener('click', () => {
if (followHandle) { followHandle.stop(); followHandle = null; }
$flow.fitView({ duration: 300 });
});
">
<div x-flow-viewport>
<template x-for="node in nodes" :key="node.id">
<div x-flow-node="node">
<div x-flow-handle:target></div>
<span x-text="node.data.label"></span>
<div x-flow-handle:source></div>
</div>
</template>
</div>
</div>
x-flow-follow Directive
Pan the camera to keep a node centered using a declarative directive on any clickable element.
Usage
<!-- Follow a node by ID -->
<button x-flow-follow="'node-1'">Follow Node 1</button>
<!-- Follow with options -->
<button x-flow-follow="{ target: 'node-1', zoom: 1.5, speed: 300 }">
Follow (zoomed)
</button>
<!-- Toggle: click again to stop following -->
<button x-flow-follow.toggle="'node-1'">Toggle Follow</button>
Expression
The expression accepts either a plain node ID string or an options object:
| Property | Type | Default | Description |
|---|---|---|---|
target |
string |
— | The node ID to follow (required when using object form) |
zoom |
number |
Current zoom | Zoom level to maintain while following |
Drag a node around — the button starts/stops the camera tracking it:
<div x-data="flowCanvas({
nodes: [
{ id: 'tracked', position: { x: 0, y: 0 }, data: { label: 'Drag me around' } },
{ id: 'static1', position: { x: 300, y: 0 }, data: { label: 'Static' }, draggable: false },
{ id: 'static2', position: { x: 150, y: 150 }, data: { label: 'Static' }, draggable: false },
],
edges: [
{ id: 'e1', source: 'tracked', target: 'static1' },
{ id: 'e2', source: 'tracked', target: 'static2' },
],
background: 'dots',
fitViewOnInit: true,
controls: false,
})" class="flow-container" style="height: 250px;">
<div class="canvas-overlay" @mousedown.stop @pointerdown.stop style="position:absolute;top:8px;left:8px;z-index:20;">
<button x-flow-follow.toggle="{ target: 'tracked', zoom: 2 }" class="rounded-md border border-border-subtle bg-elevated px-3 py-1 font-mono text-[11px] text-text-muted cursor-pointer hover:text-text-body">Toggle Follow</button>
</div>
<div x-flow-viewport>
<template x-for="node in nodes" :key="node.id">
<div x-flow-node="node">
<div x-flow-handle:target></div>
<span x-text="node.data.label"></span>
<div x-flow-handle:source></div>
</div>
</template>
</div>
</div>
.toggle Modifier
Without .toggle, clicking the button always starts following. With .toggle, clicking while already following the same target will stop following.
CSS Classes
| Class | Applied To | Meaning |
|---|---|---|
flow-following |
The follow button element | The camera is currently following |
[x-flow-follow].flow-following {
background-color: var(--flow-accent);
color: white;
}