Justin's IT and Security Pages

SVG Matrix Transformations and JavaScript

4 comments

No matter how much you might resist using matrix transformations with SVG documents, if you intend to modify an image dynamically (and cumulatively), matrixes are your only viable option.

Many sites tell you that you should use matrixes, ostensibly for speed purposes. In my opinion, speed is not the issue. The issue is the complexity associated with applying multiple transformations to an element; you just can’t do it with simple transformations (e.g., rotate, translate, skew and/or scale).

Here are a few notes about the stumbling blocks that I encountered in my journey towards using matrix transformations:

  1. Most online guides seem to assume that you will be working with a static image; they tell you how to convert simple transformations to matrixes as a one-time operation, but give you no (straightforward) information on how to subsequently alter those transformations dynamically. The JavaScript method, element.getCTM() is your key to handling this situation. By using this method (short for get Current Transformation Matrix), you can obtain a matrix that includes all of the transformations currently applied to your graphic element. That matrix can then be used to generate updated matrixes after applying dynamically updated transformations.
  2. The sylvester.js JavaScript library is a great resource to handle your matrix math needs. You’ll use the method matrix.x() to multiply the current matrix with the matrix representing the transforms you want to apply to obtain your newly combined matrix to apply to your DOM element.
  3. Some guides talk about changing the matrix.e and matrix.f variables directly to apply translation transformations. When dealing with multiple transforms, that will just cause you a world of grief (e.g., rotation transforms update the e and f variables in complex ways that are difficult to calculate without matrix math).
  4. Rotation transformations use sine and cosine methods extensively. At first glance, one would think that the JavaScript methods Math.sin() and Math.cosin() would work nicely. They do, but keep in mind that those methods deal in radians, not degrees. If you want to rotate something in (e.g., 45) degrees, you’ll need to convert that value to radians (e.g., 0.785398163) before using those methods.
  5. You should be able to combine a translation transformation with a rotation transformation in a single matrix to choose the center of rotation, but I haven’t been able to get that to work. Instead, I perform a pre-shift to move my desired rotation point to the origin and then a post-shift to move it back after the rotation. That seems to work reliably and allows me to rotate the graphic where my pointer is hovering.

Here is an example of how I have implemented these concepts in my Flower Network Flow Analysis Visualization server (I also have some prototype.js markup in here and this.nonce refers to a random, one-time string I use to distinguish between multiple, similar, generated SVGs existing in a single DOM):

var content = $('content_' + this.nonce);
var matrix = content.getCTM();

var map = $('map');

var leftVal = map.offsetLeft;
var topVal = map.offsetTop;
var parent = map.offsetParent;

while(parent != null) {
	leftVal += parent.offsetLeft;
	topVal += parent.offsetTop;
	parent = parent.offsetParent;
}

var pointerX = event.clientX - leftVal;
var pointerY = event.clientY - topVal;

var radians = rotation * (Math.PI/180);
var cos = Math.cos(radians);
var sin = Math.sin(radians);

var current = $M([[ matrix.a, matrix.c, matrix.e ], [ matrix.b, matrix.d, matrix.f ], [0, 0, 1]]);
var preshift = $M([[ 1, 0, -pointerX], [0, 1, -pointerY], [0, 0, 1]]);
var rotated = $M([[cos, -sin, 0], [sin, cos, 0], [0, 0, 1]]);
var postshift = $M([[ 1, 0, pointerX], [0, 1, pointerY], [0, 0, 1]]);

var updated = postshift.x(rotated.x(preshift.x(current)));

content.setAttribute("transform", "matrix(" +
	updated.e(1, 1) + " " + updated.e(2, 1) + " " +
	updated.e(1, 2) + " " + updated.e(2, 2) + " " +
	updated.e(1, 3) + " " + updated.e(2, 3) + ")");

The basic operations above are:

  1. Obtain the current transformation matrix. (lines 1 and 2)
  2. Determine how far the pointer is from the edges of the map DIV using offsetLeft and offsetTop. (lines 4-17)
  3. Pre-calculate the sine and cosine values of the desired rotation (convert to radians as an intermediate step). (lines 19-21)
  4. Convert the SVG matrix to a sylvester.js matrix ($M). (line 23)
  5. Build the pre-shift transformation matrix. Translation matrixes are constructed thusly, with X and Y being the number of pixels the graphic should be shifted in the X and Y directions, respectively (line 24):
      [ 1, 0, X ]
      [ 0, 1, Y ]
      [ 0, 0, 1 ]
  6. Build the rotation transformation matrix. Rotation matrixes are constructed thusly, with R being the rotation value (line 25):
      [  cos(R), sin(R),   0 ]
      [ -sin(R), cos(R),   0 ]
      [       0,      0,   1 ]
  7. Build the post-shift transformation matrix. (line 26)
  8. Calculate the updated matrix; note that the order of operations is important. I’m not precisely sure about the rules that apply here (I kind of guessed until I got the order right, to be perfectly honest). The really important thing to remember is that matrix multiplication is apparently sensitive to order (unlike simple multiplication). (line 28)
  9. Apply the updated matrix to your graphic element. Note that in my example above I’m using the sylvester.js (row, column) notation. The actual matrix() transform only uses the first six values of the full matrix – the last row of 0, 0, 1 never changes and should not be specified. (line 30-33)

By way of example, and if you have a newer version of Firefox, Chrome, Safari, Epiphany (or shockingly, even IE9!), visit a mock-up of a generated network map here. For anyone else, here is a still screenshot of a map that’s been twisted, translated and scaled.

 

Network Map Mockup

Network Map Mockup

 

If you do visit that page, try using your mouse wheel to rotate the map or the graphical slider to zoom in and out. You can also just drag the map around to reposition it. Clicking on a connection will open a small detail box and clicking on a node will narrow the display to only connections involving that node. Clicking on subsequent nodes will add those nodes’ connections. Double clicking in the white space will cause all connections to be visible again. Hovering over a connection or node will show you that connection or node’s details (at the bottom of the graphic). The information in there is mostly nonsense – I went through and “sanitized” the addresses – although protocol, port and volume information are real.

I hope the above information helps someone else! I know it would have saved me a lot of time to have a working example of JavaScript code that updates a transformation matrix dynamically based on DOM events.

Written by justin

February 19th, 2011 at 9:15 pm

Posted in programming

4 Responses to 'SVG Matrix Transformations and JavaScript'

Subscribe to comments with RSS or TrackBack to 'SVG Matrix Transformations and JavaScript'.

  1. Hi Alastair,

    Thanks for the comment; I'm glad I could help!

    Justin Thomas

    25 Feb 11 at 11:06 am

  2. Thanks for this Justin. SVG has been around for a long time yet there's so little documentation and even fewer examples/tutorials. I was struggling with multiple transforms on a matrix so sylvester.js and your tips are gold.

    Alastair

    25 Feb 11 at 10:47 am

  3. Thanks for this Justin. Have you looked at RaphaelJS and/or SVGPan? Curious how this might tie in with that. I'm particularly looking for a solution where a single button click would get the boundaries of an SVG Object and pan to it as well as find the correct zoom level for it to be within a certain boundary.

    i.e. per your own example, imagine a list of all of your nodes. Clicking on each will pan and zoom to that node.

    CG

    31 Aug 11 at 2:42 pm

  4. Thanks for the suggestions! I particularly like RaphaelJS, but it looks like the value it would add for me wouldn't necessarily be worth the effort for me to really invest in learning it and adding another dependency (I could be wrong, though). I'm not targeting anything older than IE9 and the wrappers for creating graphical elements aren't much simplified over just creating the elements myself. My answer would probably be different if I had come across it a couple of years ago, though!

    The zoom and pan on click is a neat idea. I've been thinking about how I might do that in my application since you suggested it. There, I'd probably narrow the flows to those involving the clicked node (as I do now). Then I'd determine the nodes that are the furthest apart, spin the map so that those nodes are aligned with the longest diagonal of the window and then zoom in or out until the rest of the nodes are just visible in the window. That could all be done with some fancy math and by manipulating the matrix attribute of the right SVG group. It would be a neat trick and I might look at writing some stuff up around that once I've finished porting my graph generation logic from Java on the server to JavaScript in the browser (I've just finished the map portion and am working on the area graphs now).

    Thanks for the comment!

    Justin Thomas

    5 Sep 11 at 4:13 pm

Leave a Reply