Nomadic cattle rustler and inventor of the electric lasso.
Company Website
Follow me on twitter
Contact me for frontend answers.

SVG transformations with affine matrices

September 25, 2018

I recently had a requirement to add zooming and panning to an svg image. Panning and zooming are popular interaction techniques which let the user focus on a region of interest by restricting the view. The obvious choice was to use d3-zoom but as react is rendering the svg content, D3 is a bit of a bad fit as it mutates the DOM directly. I somehow ended up using affine matrices to compute the transformations.

Before I get to matrices it is important to understand that multiple coordinate systems are in play when working with svg elements and other container elements.

What is a coordinate system?

In geometry, a coordinate system is a system that uses one or more numbers to uniquely determine the position of the points of geometric elements on a Euclidean Space. Euclidean space is often called the Cartesian space and with Cartesian coordinates we mark a point on a graph by how far along and how far up it is.

Most people will recognise this as using an ordered xx and yy tuple that signify xx as the distance along the horizontal and yy as the distance along the vertical. In the image below the tuple PP or the point PP has the elements x=244x=244 and y=249y=249.

real time results

Mathematical Coordinate Systems

In a normal coordinate system the point (0,0)(0, 0) is at the lower left of the graph. As xx increases the points move to the right in the coordinate system and as yy increases the points move up.

SVG Coordinate system

In the SVG coordinate system the point (0,0)(0, 0) or (x=0,y=0)(x=0, y=0) is the upper left corner. The y-axis is essentially reversed compared to the normal graph coordinate system. As y increases, the points, shapes etc. move down, not up.

An SVG starts out with an initial user coordinate system which matches 1:1 with the viewport coordinate system. It’s origin (0,0) is at the top left of the viewport like above.

For example let us create a simple rect element.

<svg width="500" height="150">
  <rect width="50" height="50" stroke="blue" stroke-width="2" style="fill: transparent"></rect>
</svg>

We have not specified an xx or yy attribute for the rect so the starting point is at the origin of (0,0)(0, 0).

Coordinate system transformations

A new user space (i.e. a new coordinate system) can be established by specifying transformations in the form of a transform attribute on a container element. The transform attribute defines a list of of tranform definitions that are applied to an element and the element’s children. This new user space is often referred to as a new user coordinate system.

If we take our rect example one step forward and wrap it in a g or group element, we can define a transformation to the g or group element and any children elements defined in the group, we do this by using anyone of the tranform functions in the transform attribute. The example below uses the translate function.

<svg width="500" height="150">
  <g transform="translate(20, 20)">
    <rect width="50" height="50" stroke="blue" stroke-width="2" style="fill: transparent"></rect>
  </g>
</svg>
  • On line 4 a transform attribute is added and the translate function is called with 2 arguments (20,20)(20, 20). This moves the group element and its children by the xx and yy values you pass as arguments to the function. In this case we are moving the g element and its children 20px to the right and 20px to the down.

If a container element such as an svg g element has a tranform applied to it, then we consider that it has established a new user coordinate system. All the child elements inside that container element have no idea that they have a location somewhere else on the page. Its (0,0)(0, 0) may not be at the top left of the viewport at all. It may be on a different angle and it may be skewed or scaled relative to the user coordinate system of the parent.

In the above example we still have not specified an xx or yy attribute for the rect but it is at (20, 20) of the original svg coordinate system because we have a established a new user coordinate system in the g element specified in the transform attribute.

Why is this useful? Well at the simplest level, it’s an easy way to move a group of shapes to a different position on a page.

Affine transformation matrix

Simply put, a matrix is an array of numbers with a predefined number of rows and columns. Matrices are used to transform coordinates. Some common applications are:

  • Translation (moving position)
  • Scaling (size changes, the original requirement I need to fulfil)
  • Rotation (chagning orientation)

An affine transformation matrix performs a linear mapping from 2D coordinates to other 2D coordinates that preserves the straightness and parallelness of the lines (is parallelness a real word?). Such a coordinate transformation can be represented by a 3 row and 3 column matrix with an implied last row of (001)\begin{pmatrix}0 & 0 & 1 \end{pmatrix} (more on this later). This matrix transforms source coordinates (x,y)(x, y) into destination coordinates (x,y)(x', y') by applying the following expression to transform a point by an affine matrix.

The transformation expression can be thought of simply like this:

Transformed Point = Transform Matrix x Original point.

We use matrix multipication to transform a coordinate by multiplying a coordinate by an affine matrix that defines a transformation.

(xy1)×(actxbdty001)=(a×x+c×y+txb×x+d×y+ty1)\begin{pmatrix}x \\ y \\ 1 \end{pmatrix} \times \begin{pmatrix} a & c & tx \\ b & d & ty \\ 0 & 0 & 1 \end{pmatrix} = \begin{pmatrix} a \times x + c \times y + tx \\ b \times x + d \times y + ty \\ 1 \end{pmatrix}`

We represent the coordinate as a column vector. Don’t worry too much about the 3rd weird row, just think of it like this:

(xy)×(actxbdty)=(a×x+c×y+txb×x+d×y+ty)\begin{pmatrix}x \\ y \end{pmatrix} \times \begin{pmatrix} a & c & tx \\ b & d & ty \end{pmatrix} = \begin{pmatrix} a \times x + c \times y + tx \\ b \times x + d \times y + ty \end{pmatrix}`

Transform matrix function

I have only just discovered that you can apply one or more transformation to an SVG element using the matrix() function. The syntax is:

matrix({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 })

The matrix(<a> <b> <c> <d> <e> <f>) specifies an affine transformation in the form of a transformation matrix of six values. matrix(a, b, c, d, e,f) is equivalent to applying the transformation matrix:

(acebdf001)\begin{pmatrix} a & c & e \\ b & d & f \\ 0 & 0 & 1 \end{pmatrix}

which maps coordinates from a previous coordinate system to a new user coordinate system by the following matrix multipication.

(x×newCoordSysy×newCoordSys1)=(acebdf001)(x×prevCoordSysy×prevCoordSys1)=(ax×prevCoordSys+cy×prevCoordSys+ebx×prevCoordSys+dy×prevCoordSys+f1)\begin{pmatrix} x\times{\mathrm{newCoordSys}} \\ y\times{\mathrm{newCoordSys}} \\ 1 \end{pmatrix} = \begin{pmatrix} a & c & e \\ b & d & f \\ 0 & 0 & 1 \end{pmatrix} \begin{pmatrix} x\times{\mathrm{prevCoordSys}} \\ y\times{\mathrm{prevCoordSys}} \\ 1 \end{pmatrix} = \begin{pmatrix} a x\times{\mathrm{prevCoordSys}} + c y\times{\mathrm{prevCoordSys}} + e \\ b x\times{\mathrm{prevCoordSys}} + d y\times{\mathrm{prevCoordSys}} + f \\ 1 \end{pmatrix}

I will explain how elements a, b, c, d, e and f effect transformations first before explaining the weird third row (001)\begin{pmatrix}0 & 0 & 1 \end{pmatrix} later.

With the svg matrix function you only specify the first 6 values of the matrix a, b, c, d, e and f. The matrix function will multiply the affine matrix derived from the a, b, c, d, e and f arguments and apply it to all elements in the container.

I like to think of the affine matrix like this:

(a:scalingXc:originXe:translationXb:originYd:scalingYf:translationY001)\begin{pmatrix} a:scalingX & c: originX & e: translationX \\ b: originY & d: scalingY & f: translationY \\ 0 & 0 & 1 \end{pmatrix}
  • b and c are the origin coordinates
  • a and d control scaling
  • e and f are used to translate or move the element

Translate matrix

A translate matrix takes this form:

(10tx01ty001)\begin{pmatrix} 1 & 0 & tx \\ 0 & 1 & ty \\ 0 & 0 & 1 \end{pmatrix}

As you only supply the first 6 values of the matrix, the transform attribute would look like this:

transform="matrix(1,0,0,1,tx,ty)

Going back to the rect example at the beginning, if I wanted to translate the rect by (20, 20), I could apply the following matrix to the g or group element:

(10200120001)\begin{pmatrix} 1 & 0 & 20 \\ 0 & 1 & 20 \\ 0 & 0 & 1 \end{pmatrix}

Which would result in the following call to the matrix function in the transform attribute:

 transform="matrix(1,0,0,1,20,20)

Here is how this looks:

<svg width="500" height="150">
  <g transform="matrix(1,0,0,1,20,20)">
    <rect width="50" height="50" stroke="blue" stroke-width="2" style="fill: transparent"></rect>
  </g>
</svg>

Scale

This was my original requirement that took me to affine matrices. A scaling matrix transformation would take this form:

(sx000sy0001)\begin{pmatrix} sx & 0 & 0 \\ 0 & sy & 0 \\ 0 & 0 & 1 \end{pmatrix}

Below has the original rect with no transform attribute:

<svg width="500" height="150">
  <rect width="50" height="50" style="fill: green"></rect>
</svg>

If I want to scale the rect out, I can apply this transformation

(0.50000.50001)\begin{pmatrix} 0.5 & 0 & 0 \\ 0 & 0.5 & 0 \\ 0 & 0 & 1 \end{pmatrix}

with this transform function

trnasform(matrix(0.5, 0, 0, 0.5, 0, 0))

Would result in the rect appearing to be scaled out.

<svg width="500" height="150">
  <rect width="50" height="50" transform="matrix(.5,0,0,.5,0,0)" style="fill: blue"></rect>
</svg>

And if I want to scale in:

I would apply this transformation:

(1.50001.50001)\begin{pmatrix} 1.5 & 0 & 0 \\ 0 & 1.5 & 0 \\ 0 & 0 & 1 \end{pmatrix}

with this transform function

transform(matrix(1.5, 0, 0, 1.5, 0, 0))

Would appear to bring the rect closer:

<svg width="500" height="150">
  <rect width="50" height="50" transform="matrix(1.5,0,0,1.5,0,0)" style="fill: blue"></rect>
</svg>

The true power of affine matrices is that we can combine them into one transformation matrix, we can scale, rotate and transform in one matrix by multiplying the two transformations togehter, which brings us back to the weird row (001)\begin{pmatrix}0 & 0 & 1 \end{pmatrix}?.

What is with this this weird 3rd row

Why are we defining a 2D transformation with a 3X3 matrix? Why are we adding an extra row? To combine rotation and translation in one operation we need to be able to multiply 2 affine matrices together and the rules of matrix multipication show that:

(??????)×(??????)=???\begin{pmatrix} ? & ? & ? \\ ? & ? & ? \end{pmatrix} \times \begin{pmatrix} ? & ? & ? \\ ? & ? & ? \end{pmatrix} = ???

Is not a valid matrix multipication so we need to have that extra row in order to make the matrix square, and thus be able to be mutliplied with other matrices.

transformation-matrix npm package

You might be able to do simple maths to do the transformation matrix algebra above but this package exists to aid you with doing the transformation maths.


Paul Cowan

Nomadic cattle rustler and inventor of the electric lasso.
Company Website
Follow me on twitter
Contact me for frontend answers.