SVG Pan Zoom Matrix Transformations Explained
Libraries like svg-pan-zoom manage interactive panning
and zooming by intercepting pointer events and applying affine matrix
transformations to an SVG container. During viewport dragging, the
library calculates the displacement between mouse or touch positions,
converts screen-space deltas into the SVG coordinate space, updates the
2D transformation matrix, and writes the resulting matrix back to the
target SVG element.
The 2D Affine Transformation Matrix
SVG elements use a 3x3 affine transformation matrix represented in
CSS and SVG attributes as a 6-value tuple:
matrix(a, b, c, d, e, f).
aanddrepresent horizontal and vertical scaling.bandcrepresent horizontal and vertical shearing/skewing.eandfrepresent horizontal and vertical translation (pan offsets).
When a user drags the viewport, the scale (a,
d) and skew (b, c) components
remain constant, while the translation components (e,
f) are continuously updated to shift the rendered graphics
across the screen.
Capturing Pointer Events and Calculating Deltas
The panning cycle begins when the user triggers a drag action:
- Pointer Down (
mousedown/pointerdown): The library records the initial coordinates \((x_0, y_0)\) in screen space (e.g.,event.clientXandevent.clientY) and sets an active dragging state. It also retrieves the current transformation matrix of the viewport. - Pointer Move (
mousemove/pointermove): As the pointer moves to a new position \((x_t, y_t)\), the library computes the displacement vector: \[\Delta x = x_t - x_{prev}\] \[\Delta y = y_t - y_{prev}\] The previous position is then updated to \((x_t, y_t)\) for the next frame. - Pointer Up (
mouseup/pointerup): The drag state is cleared, and event listeners are reset or detached.
Coordinate Space Conversion
Screen pixels do not always have a 1:1 relationship with SVG user units due to CSS scaling, device pixel ratios, and existing zoom levels. To prevent erratic movement during drags:
- The library accesses the element’s Current Transformation Matrix
using
svgElement.getScreenCTM(). - To translate screen coordinates to SVG coordinates, the library
calculates the inverse matrix via
getScreenCTM().inverse(). - The displacement \((\Delta x, \Delta y)\) is mapped through this inverse transformation. When panning under uniform scaling, this simplifies to dividing the screen-space displacement by the current zoom level: \[\Delta x_{svg} = \frac{\Delta x}{scale}\] \[\Delta y_{svg} = \frac{\Delta y}{scale}\]
Updating and Applying the Matrix
Once the correct translation delta is determined, the library updates the transformation matrix:
- Matrix Translation: A translation matrix representing the movement is premultiplied or added to the existing transformation matrix: \[e_{new} = e_{current} + \Delta x\] \[f_{new} = f_{current} + \Delta y\]
- DOM Update: The new matrix values are written to
the target viewport layer—typically a top-level
<g class="svg-pan-zoom_viewport">element grouping all visible SVG elements.
The update is applied either via the transform
attribute:
<g transform="matrix(a, b, c, d, e_new, f_new)">
<!-- SVG child nodes -->
</g>or via the SVG DOM interface:
const matrix = viewportElement.getCTM();
matrix.e = newE;
matrix.f = newF;
const transform = svgRoot.createSVGTransformFromMatrix(matrix);
viewportElement.transform.baseVal.initialize(transform);By wrapping all visual nodes in a single transformed group and mutating its matrix via hardware-accelerated transforms, libraries achieve smooth, high-performance viewport dragging without needing to recalculate the individual coordinates of underlying vector shapes.