When I needed to pass Calculus and Algebra during my Computer Science degree, I used the formula known to every Polish student called 'ZZZ' (Zakuć, Zdać, Zapomnieć) - Cram, Pass, Forget. It means memorizing just enough to pass the exam and then not caring if you'll remember any of it later.
Fast forward a few years into my web developer career and I needed to implement a mouseMove event on the SVG element. Every move I made with my mouse made the element's position shoot off into the void. I solved this issue using the transformation matrix.
The more I worked with SVGs and graphs, the more I realized that the math I tried to cram and forget shows up in everything that I do. From the coordinates to graph layouts, drag-and-drop to animations - we just don't call it 'math'.
This series attempts to show you that math. Each article takes one concept (that we had to cram in class) and shows exactly where it lives in real frontend code, with real examples I've built in production or in side projects. I don't want to show you mathematical proofs, I will explain a bit of theory with a demonstration how to use it when you are the one who has to position elements just the right way, resolve conflicts and overlaps or draw a line from one point to another.
Welcome to Know Your WebDev Math.
Part 2: Translating coordinates with transformation matrix
Introduction
This is the problemI already hinted at in the intro to the Know Your WebDev Math series. No big requirements this time - just trying to build drag-and-drop of SVG elements inside a viewport.
If you try to pass coordinates directly from drag-and-drop mouse event to SVG, this will happen:
Points will be moved seemingly independently from the moves of the mouse cursor.
And how we want it to look like? I believe like this:
What is the difference between the first example and the second? A single line of code:
p = p.matrixTransform(m.inverse());
And in this article, I will explain, why.
Solution: Transformation matrices
Theory: what is a transformation matrix?
(Last time we talked about triangles. Today's starting topic: matrices)
A matrix is a mathematical object that structures numbers (or other objects) in rows and columns. Matrices (especially square matrices) have multiple usages in algebra, numerical analysis and geometry, thanks to their properties. One of matrices used in geometry is a transformation matrix.
A transformation matrix is a structure that describes how to move, scale, rotate, or skew points in space. In the context of 2D graphics (which is where we live as web developers working with SVGs), it's a 3×3 matrix (3 rows, 3 columns) where third row is always [0, 0, 1]. The remaining 6 values - [a, b, c, d, e, f] - describe all possible 2D transformations:
| a c e |
| b d f |
| 0 0 1 |
-
aanddhandle scaling -
bandchandle rotation and skewing -
eandfhandle translation (moving)
You don't need to memorize this. What matters is understanding that this matrix encodes the transformation necessary, to move the point from one system of coordinates to another. In our case: the coordinate system of the screen, and the coordinate system inside the SVG viewport.
If you want to go deeper, MDN's Matrix math for WebGL and the Aspose SVG transformation docs both have excellent explanations.
Another special type of matrix is identity matrix. This is a matrix that has 1 on the diagonal and 0 everywhere else.
| 1 0 0 |
| 0 1 0 |
| 0 0 1 |
If we use an identity matrix as a transformation matrix - nothing will happen. Nic. Zilch. Nada.
Working with points and matrices in JavaScript
The browser exposes two classes from the Geometry Interfaces Module that we'll need:
DOMPoint - represents a 2D (or 3D) point:
const p = new DOMPoint(x, y);
DOMMatrix - represents a 2D or 3D transformation matrix. You can create one manually, but in practice you'll get it from the browser (more on that in a moment):
const m = new DOMMatrix([a, b, c, d, e, f]);
The useful part is that DOMPoint has a method called matrixTransform - it applies a given matrix to the point and returns the transformed coordinates:
const transformedPoint = point.matrixTransform(matrix);
These two classes allow us to represent points and matrices - mathematical structures - in JS environment.
The SVG viewport and viewBox
Here's where the coordinate problem actually comes from.
An SVG element lives in the browser page and has a size in screen pixels, let's say 600×600px. But the SVG also has its own internal coordinate system, defined by the viewBox attribute:
<svg viewBox="0 0 1000 1000" width="600" height="600">
This tells the SVG: "my internal coordinate system goes from 0 to 1000 in both x and y, but render it into a 600×600 pixel box on screen." Every point you draw at (500, 500) in SVG coordinates will appear at (300, 300) screen pixels - because 500 out of 1000 = 50%, and 50% of 600 is 300.
Now here's the problem: when your mouse fires a mousemove event, it gives you screen coordinates - pixels relative to the top-left of the browser viewport. If you naively pass those into your SVG element's position, you try to apply external coordinates to internal system, and things can get 'jumpy'.
The fix is to convert from screen coordinates into SVG coordinates. And that's exactly what the transformation matrix is for.
getScreenCTM() - getting the matrix
We don't need to guess how the transformation matrix should look - SVG elements expose a method called getScreenCTM() (CTM stands for Current Transformation Matrix). It returns an object of the DOMMatrix class that describes the transformation from the SVG's coordinate system to the screen's coordinate system:
const m = svgElement.getScreenCTM();
Think of it like this: m knows how to go from SVG to screen. Apply m to an SVG point, and you get where it appears on screen.
But we want to go the other way, from screen to SVG. We have a mouse position in screen coordinates, and we need the equivalent SVG coordinates.
Inverting the matrix - reversing the translation
To go in the opposite direction, we need to invert the matrix. Inverting a transformation matrix produces a new matrix that undoes exactly what the original does.
Mathematically, if A is our matrix and A-1 is inverted matrix A, multiplying them both will give us identity matrix.
And I remember that because I had to retake the test on inverting matrices during algebra course in uni.
So our m matrix will transform from SVG to screen, and we need to find mInversed that will transform from screen to SVG.
Fortunately for me and my algebra skills, DOMMatrix gives us this for free:
const mInverse = m.inverse();
And that brings us back to the one-liner from the intro:
p = p.matrixTransform(m.inverse());
We take the point p (in screen coordinates from the mouse event), apply the inverted CTM, and get back the correct position in SVG coordinates.
Calculations
Let's walk through exactly what happens in the final code. It's a Vue 3 app with an SVG element that has draggable endpoints.
Setting up the SVG and drag events
<svg
viewBox="0 0 1000 1000"
@pointerMove="onDrag($event)"
@pointerUp="onDragStop()"
ref="container"
>
<circle
r="4"
:cx="start.x"
:cy="start.y"
fill="purple"
@pointerDown="onDragStart('start')"
ref="start"
/>
<circle
r="4"
:cx="end.x"
:cy="end.y"
fill="purple"
@pointerDown="onDragStart('end')"
ref="end"
/>
</svg>
The pointerMove is on the SVG itself (not the circles) - this way we don't lose tracking if the mouse moves faster than the element. The ref attributes give us direct access to the DOM nodes, which we'll need to call getScreenCTM().
The drag handler
onDrag(e) {
if (this.dragging !== null) {
let p = new DOMPoint(e.clientX, e.clientY);
if (this.dragging === 'start') {
const m = this.$refs.start.getScreenCTM();
p = p.matrixTransform(m.inverse());
this.start = { x: p.x, y: p.y };
} else if (this.dragging === 'end') {
const m = this.$refs.end.getScreenCTM();
p = p.matrixTransform(m.inverse());
this.end = { x: p.x, y: p.y };
}
}
}
Step by step:
-
new DOMPoint(e.clientX, e.clientY)- wrap the mouse's screen coordinates in aDOMPoint. -
this.$refs.start.getScreenCTM()- get the transformation matrix of the dragged element. This gives us the full picture of how the SVG is scaled, translated, and positioned relative to the screen. -
m.inverse()- inverting the matrix - now the matrix translates screen → SVG. -
p.matrixTransform(m.inverse())- apply the inverted matrix to our screen-space point, getting the equivalent SVG-space coordinates. - Update the reactive data, and Vue re-renders the circle at the correct position.
Notice that we call getScreenCTM() on the circle element, not the SVG root. This is intentional - it accounts for any additional transforms applied to the element itself (rotation, scaling, nesting inside a <g> with a transform, etc.). The matrix you get is cumulative for the full chain from that element to the screen.
You can play with the full example here:
Result
With these ~3 lines of setup, you get perfectly tracked drag-and-drop in any SVG, regardless of:
- how the SVG is scaled or positioned on the page
- whether the
viewBoxmatches the rendered pixel size - whether the SVG is inside a scrolled container
The matrix handles all of this for you.
One thing I want to highlight about Screen CTM: notice that the math itself is invisible. You're not computing scale factors manually, not applying CSS transforms, not doing trigonometry. You just ask the DOM for the matrix it's already maintaining internally, invert it, and apply it. The browser has been tracking this all along and getScreenCTM() just allows you to use the existing transformation. The geometry is already there. You just need to know that you can use it and what to use it for.
This is why we need math in programming - even if I failed my first matrix inversion test, I knew that I could use matrix to transform a point position and I could start to look for matrixes in JS.
Next up: Bézier curves — the math behind every smooth path in CSS, SVG, and canvas, and why those control point handles in your design tools are doing exactly what the formula says.
If you've run into SVG nightmares of your own (and I know you have), drop them in the comments!


Top comments (0)