How DrawDB Implements Pan and Zoom on the Canvas: A Deep Dive
DrawDB implements pan and zoom through a React context-based transform store that captures scale and translation values, with mouse and wheel event handlers in a custom hook to update the transform and re-render the SVG canvas in real-time.
DrawDB's canvas pan and zoom functionality relies on a lightweight but precise state management pattern. The implementation in the drawdb-io/drawdb repository avoids heavy libraries by using React's built-in context system to synchronize transform state across the diagram surface. This article breaks down exactly how each piece works, with source references from the actual codebase.
The TransformContext: Central State for Scale and Offset
All pan and zoom data lives in src/context/TransformContext.js. This file creates a plain React context that stores three mutable values:
scale— the current zoom leveloffsetX— horizontal translation in pixelsoffsetY— vertical translation in pixels
The context also exposes setTransform, a function to batch-update these values.
The provider wraps the entire application in App.jsx, ensuring any component can subscribe to transform changes. This design means the diagram, toolbar, and minimap all share the same source of truth without prop drilling.
useTransform: The Consumption Hook
To access the context, components import useTransform from src/hooks/useTransform.js:
import { useContext } from "react";
import { TransformContext } from "../context/TransformContext";
export default function useTransform() {
return useContext(TransformContext);
}
This thin wrapper standardizes how components read the current pan and zoom state. Any call to useTransform() returns the latest scale, offsetX, offsetY, and setTransform function.
useCanvas: Event Handling and Transform Calculation
The heavy lifting happens in src/hooks/useCanvas.js. This hook registers event listeners on the canvas element and computes new transform values based on user input.
Panning Implementation
On mousedown, the hook records the initial cursor position. During mousemove with the button held, it calculates the delta between current and starting positions, then updates offsetX and offsetY by that delta. This creates the dragging sensation where the canvas follows the cursor.
Zooming Implementation
The wheel event handler performs smooth zooming centered on the cursor position. The algorithm ensures the point under the mouse stays stationary while the scale changes—a critical detail for intuitive navigation.
Here is the core zoom logic from src/hooks/useCanvas.js:
function handleWheel(e) {
e.preventDefault();
const { scale, offsetX, offsetY } = transform;
// Calculate zoom factor from wheel delta
const zoom = Math.exp(-e.deltaY * 0.001);
// Get cursor position relative to canvas
const rect = e.currentTarget.getBoundingClientRect();
const cx = e.clientX - rect.left;
const cy = e.clientY - rect.top;
const newScale = scale * zoom;
// Adjust offsets so (cx, cy) remains fixed in screen space
const newOffsetX = cx - (cx - offsetX) * (newScale / scale);
const newOffsetY = cy - (cy - offsetY) * (newScale / scale);
setTransform({
scale: newScale,
offsetX: newOffsetX,
offsetY: newOffsetY
});
}
The math here is worth unpacking:
Math.exp(-e.deltaY * 0.001)converts linear wheel deltas into exponential scale factors, giving consistent zoom speed at any magnification- The offset adjustment formulas translate the zoom origin from the top-left corner (default SVG behavior) to the cursor location
On component unmount, the hook removes all listeners to prevent memory leaks.
Applying the Transform to the SVG Canvas
The diagram component—typically src/components/Diagram.jsx—reads the transform context and applies it directly to the SVG element:
const { scale, offsetX, offsetY } = useTransform();
return (
<svg
style={{ cursor: isPanning ? "grabbing" : "grab" }}
transform={`translate(${offsetX}, ${offsetY}) scale(${scale})`}
{...canvasProps}
>
{/* diagram nodes, relationships, etc. */}
</svg>
);
Because useTransform subscribes to context changes, every pan or zoom operation triggers a re-render with the updated transform string. SVG's transform attribute handles the matrix math natively, keeping the JavaScript overhead minimal.
The cursor style switches between grab and grabbing based on the internal isPanning state, providing immediate visual feedback during drag operations.
Key Design Decisions
Context over refs. DrawDB could have used refs and direct DOM manipulation for slightly better performance. However, the context approach keeps the transform inspectable, time-travel debuggable, and accessible to React DevTools.
Pure SVG transforms. By using the SVG transform attribute rather than CSS transforms, DrawDB ensures precise coordinate mapping for hit-testing diagram elements at any zoom level.
Fixed-point zooming. The offset recalculation in handleWheel eliminates the "drift" problem where zooming repeatedly causes the viewport to wander away from the targeted area.
Summary
TransformContextinsrc/context/TransformContext.jsholds the single source of truth forscale,offsetX, andoffsetYuseTransforminsrc/hooks/useTransform.jsprovides standardized access to the contextuseCanvasinsrc/hooks/useCanvas.jsimplements mouse-drag panning and wheel zooming with cursor-centered scaling- The diagram component applies the transform via SVG's native
transformattribute for hardware-accelerated rendering - All state updates flow through React's context system, enabling instantaneous UI synchronization
Frequently Asked Questions
How does DrawDB keep the zoom centered on the cursor rather than the origin?
The handleWheel function in src/hooks/useCanvas.js recalculates offsetX and offsetY using the formula newOffset = cursor - (cursor - oldOffset) * (newScale / oldScale). This transformation moves the coordinate system origin so that the point under the cursor maintains the same screen position after scaling.
What zoom sensitivity does DrawDB use for mouse wheels?
The implementation uses Math.exp(-e.deltaY * 0.001), which translates wheel deltas into approximately 0.1% scale change per wheel tick. This exponential mapping keeps zooming feel consistent regardless of current magnification level.
Could this pan and zoom implementation work with HTML Canvas instead of SVG?
Yes, with modifications. The state management in TransformContext and useTransform is rendering-agnostic. For HTML Canvas, you would replace the SVG transform attribute with ctx.setTransform(scale, 0, 0, scale, offsetX, offsetY) in your render loop, keeping the same event handling logic from useCanvas.
Why does DrawDB use a React context instead of a state management library like Redux?
The transform state is localized and updated frequently (every mouse move during panning). React context provides sufficient performance for this use case without adding external dependencies. The codebase prioritizes minimal bundle size and direct control over re-render boundaries.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →