Pallas Athena

Escaping SVG Transformation Hell: Guide Posts toward Working with SVG Transforms

Introduction

Recently, over the course of developing a tool to support animating Scalable Vector Graphics, I found myself deep in the embrace a special kind of madness; deconvolving the SVG transformation matrix. If you've ever tried to animate or interact with SVG elements using JavaScript then likely you've faced something similar, namely: trying to wrap your brain around not just one, not just two, but multiple computer graphics coordinate systems each subject to any number of transformations related to projecting graphics on a 2D screen. Welcome to SVG transformation hell.

Now don't get me wrong. I'm not here to hate on SVG. In fact, I love scalable vector graphics. I'm so passionate about it that I'm in the process of developing framework for technical artists and developers to work with the medium. That being said, despite the fact that the SVG specification has been around for quite awhile, certain aspects of working with it demand better explanation. My purpose in this post is to walk the reader through a simple problem that took me way too long to solve: getting the actual position of a transformed <g> element in SVG user coordinates. Along the way, I'll share a lot of knowledge and a number of useful tips I wish someone had handed me before I had to spend hours spelunking through broken documents and half-solutions on Stack Overflow. This post isn't just some workaround -- it's a field guide for understanding and taming SVG transforms when you're creating dynamic interactive graphics and animation for the World Wide Web.

The Problem: Accessing the Right Transformation Matrix

In this post I'll be focussing on a specific problem: accessing the transformation matrix associated with a specific group of SVG render objects. This might seem simple enough -- anyone who's ever done anything non-trivial with SVG must have had to grapple with the concept of the transform -- and applying transformations to elements and groups is one of the facets of this art form that makes it so powerful. But at the same time, the API available to manipulate scalable vector graphics programmatically can seem, well, obtuse on a good day and downright hellish on a bad. So what follows are a set of guideposts to navigate the path to mastery of the SVG transform API.

A Brief Guide to the Transform Functions

Elsewhere I've described the nature of SVG transformations at length and I invite anyone interested to go back and read or revisit the exploration of the SVG transformation matrix I provided there. Here I'm going to jump right in and provide a brief guide to some of the key SVG transform functions. The goal is to understand how to dynamically obtain transformation information associated with SVG artworks. First I'll provide a brief set of guideposts. I'll follow that with a deeper dive into the system for those interested in fully understanding the richness of SVG transformation matrices.

Get the transformations that define local coordinate systems as a consolidated matrix.

As a programmer and SVG artist I am constantly performing operations like translation, rotation and scaling over SVG elements and groups. With its hierarchical structure SVG provides a number of ways to work with these sorts of transformations but, believe it or not, the best way to reduce complexity and improve performance is to understand and use the transformation matrix. This first guidepost addresses accessing the matrix for an element or group. Do that with:

element.transform.baseVal.consolidate().matrix

This call returns a consolidated transformation list in the form of a matrix for an element or group in the SVG user coordinate system.

I'll elaborate below, but long story short: a lot of confusion for the uninitiated stems from not understanding the difference between the SVG user coordinate system and viewport coordinates. In brief, the important thing here is that element.transform.baseVal.consolidate().matrix retrieves the transformation matrix applied to an SVG element. It's this matrix which defines the element's local coordinate system. Importantly, it can also be used to undo transformations by invoking its inverse when you want to map back to the SVG initial coordinate system.

Get the transformation matrix mapping the *initial coordinate system* to the *viewport* with: 'getCTM()'

To map from the SVG coordinate system to the viewport use:

svgDoc.getCTM()

getCTM() gets you the cumulative transformation matrix mapping SVG user coordinates to the SVG viewport. If you're working purely within the SVG this is likely what you'll need. If you're working with SVG embedded in HTML then you'll want to use getScreenCTM(). Probably.

Get the transformation matrix mapping the *initial coordinate system* to the *client* viewport with: 'getScreenCTM()'

To map from the SVG user coordinate system to the browser window use:

svgDoc.getScreenCTM()

getScreenCTM() is useful when you need to relate SVG coordinates to screen-based events (like mouse-clicks or the position of other HTML elements).

The above guide posts summarize the key functions you'll need to obtain all the transformation information necessary to map from the SVG initial coordinate system to the client viewport. If that summary gives you what you need, great. But if you want to get a deeper understanding of all these coordinate spaces read on!

Understanding SVG Coordinate Systems

The Big Picture

A big part of the problem of applying and untangling SVG transformations stems from vague understanding of SVG's coordinate systems. So to clarify let's zoom out and look at the big picture.

Insert visual depicting svg user coordinates vs. svg screen coordinates
Figure 1: SVG Coordinate Systems.

Figure 1 is an abstraction intended to conceptually illustrate coordinate systems that must be considered in programmatically applying and/or unravelling SVG transformations. Superficially, what the end-user of an SVG embedded in a web page will see occurs in the context of a screen coordinate system. Embedded in that is an SVG viewport defined by the SVG viewBox attribute. Behind the scenes lies the SVG user coordinate system -- coordinate space used by the SVG artist to create and position the elements of an SVG. Finally, I've also shown a renderable group which, in SVG, will always comprise elements over which transformations can be applied to define a local coordinate system. To me, that's huge. Let's take a moment to summarize all that now.

Important

When SVG is embedded in a web page (inline, object, or <img>), there are several coordinate systems you'll need to consider. Understanding how to move between them is essential for correct interaction, positioning, and transformation.

Summary of SVG Coordinate Systems

Screen Coordinates

The screen coordinates include the renderable area of the browser window. The origin is the top-left corner of the browser's content viewport (note that this doesn't include the title bar provided by the device OS). This system has to be accounted for when obtaining user inputs like mouse events or considering element relationships between SVG and DOM elements outsider the renderable context of the SVG viewport.

Viewport Coordinates

Viewport coordinates relate the SVG user coordinate system to the SVG Viewport. The SVG viewport is the rectangular region in screen pixels where the SVG content is rendered (think of it as the window through which you're looking at your SVG scene). In the case of embedded SVG, it's determined by the CSS layout box of the <svg> element in the HTML document.

It's important not to conflate the concept of the SVG viewport with the viewBox attribute. They are different things. Viewport refers to the area of the screen where the SVG is displayed. It is measured in screen pixels (i.e., css units). viewBox is an SVG attribute that, together with SVG width, height and preserveAspectRatio determine how the SVG user coordinate system is mapped to the viewport.

Pro Tip

For SVG artists, unless you have a good reason not to, it's a good practice to insure your viewBox dimensions match your SVG width and height attributes explicitly to insure a 1:1 mapping between your user coordinates and the viewport coordinates. This avoids unexpected surprises downstream in your workflow.

SVG User Coordinates

Finally your SVG user coordinates refer to the coordinate space in which SVG artists usually operate -- sculpting shapes, positioning elements, and constructing scenes. Think of the SVG user space as an abstract two dimensional plane that extends infinitely in all directions, where your drawing lives.

A Few Key Points

  • When you create an SVG document the <svg> element establishes a default user coordinate system with positive x pointing right and positive y pointing down (as in most computer graphics systems). Sometimes the default system is referred to as the initial coordinate system.

  • In the absence of width, height and viewBox attributes it's safe to assume a 1:1 mapping between SVG units and viewport units with one unit in the initial coordinate system typically representing one "pixel" in the viewport. Probably.

As I described above, the initial space can be modified by applying the viewBox attribute and any number of SVG transforms.

A Working Example

To help deepen understanding of the concepts I've discussed above I've included a working example below simplified from some tooling I've been developing for my SVG Artworks framework. I've distilled the example to a little toy that incorporates a number of renderable elements within an SVG document. The intent is to enable end-users to move the elements around the SVG space as a group as well as manipulate component elements within the group.

Setting up the Viewport

First, let's use the example to see how to manipulate the viewport. To keep things simple I designed the components in a very small initial space -- 20 X 20 units. First I'll load it with the following svg:

    <svg width="20"
         height="20"
         ...
    >
SVG default coordinate space

Remember, in the absence of information to the contrary the system should default to a 1:1 mapping of units to pixels. This is useful to consider for applications that might use very small sprites (e.g., fav-icons, game sprites, icons, etc.). And indeed if I load the present example without a viewBox attribute it looks really small. Maybe a little too small to see.

I can fix that by "zooming in a bit" using a combination of width, height and the viewBox attribute.

    <svg width="100"
         height="100"
         viewBox="-10 -10 20 20"
         ...
    >

Take a moment to look carefully here. Notice I've set width and height to 100 X 100. See, in the svg element it's the width and height that control the screen dimensions with which the graphic will be rendered (as opposed to their use in renderable elements which controls the width and height in SVG user coordinates).

SVG default coordinate space

Next, I've set the viewBox to -10 -10 20 20. This will cause the viewport to show the area starting at (-10, -10) in SVG user coordinates and spanning a 20 by 20 unit region of the SVG coordinate space (effectively panning 10 units to the left and upwards of the origin).

I did this on purpose to show how I typically develop sprites for model simulations and games. I center the sprites around the SVG coordinate space origin. This technique simplifies calculations for position and movement downstream in development workflows and is the recommended approach for sprite development in the SVG Artworks Framework.

Pro Tip

Artists developing sprite sheets in SVG should consider centering them around the SVG coordinate space origin in order to simplify downstream calculations.

Working with the Matrix

Next let's swallow that red pill and dive into working with the matrix. Below I've in-lined an interactive user interface to play with the example and highlight the concepts we've visited. If you hit the "Add Group" button the toy should display a group of renderables. Tor now let's just call that a "bone" and leave it at that. I'll have a lot more to say about bones in forthcoming announcements.

The bone can be manipulated by "grabbing" one of its three "handles". If you click on the dark red circle you should be able to move it around (i.e., translate the group in the SVG coordinate space). And if you click the pointy part (let's just call it the "nose") you should be able to rotate the group around it's pivot point. In the initial coordinate system the pivot point is set at the SVG origin by design. This makes it easier to rotate the group downstream. Finally there's another component -- the big red circle -- which can be moved along the x-axis of the bone's local coordinate system to visualize the length of the structure.

SVG Coords: ( 0 , 0 )
Screen Coords: ( 0 , 0 )
Angle: 0

Now if you the reader have made it this far down the scroll, well, first kudos to you! You've reached the heart of the matter and the lynch pin of this blog post. If you take a moment to play with the toy you'll see how it reports the coordinates cursor -- both in SVG user coordinates and also in screen coordinates. And this brings us back to the guide posts I provided at the outset of the post. What follows is a prescribed path for navigating the system.

Navigating the Path with Matrix Operations

If you want to create anything interactive you'll have be able to navigate the transformation pipeline end-to-end -- winding your way from the screen back to that abstract SVG coordinate space. For me, the easiest pathway is using the matrix. Let's start with the screen.

Working with Screen Coordinates

Information related to end-user inputs has to flow through the screen. You saw this if you played with the tool. When you click or touch the various handles the input registers in the form of mouse or touch events which have to be converted from screen coordinates to SVG. Here's a quick utility to wrap and transform mouse-coordinates for use in SVG. I've defined in pure vanilla javascript but you can be readily translate it to typescript or your js framework du jeur.

/**
 * Convert client coordinates to SVG user coordinates given 
 * mouse coordintates from screen coordinate system...
 *
 * @returns SVG Point in SVG user coordinate space
 */
function getSvgCoords(svg, clientX, clientY) {
    const pt = svg.createSVGPoint();
    pt.x = clientX;
    pt.y = clientY;
    const svgPoint = pt.matrixTransform( svg.getScreenCTM().inverse() );
    return svgPoint;
}

This function takes a handle to an svg element and $x$ and $y$ mouse coordinate values in screen coordinates. The mouse coordinates can be obtained off a javascript event object using (e.g.,) event.clientX and event.clientY .

Notice how in the utility we:

  1. Obtain a new SvgPoint to which we assign the screen coordinates and then,

  2. Invoke pt.matrixTransform( svg.getScreenCTM().inverse() ) to unravel the transformations that relate the screen event to the SVG user coordinate space using the inverse of the cumulative screen transform matrix.

What's accumulated in that matrix are all the transformations in the chain from the SVG user coordinate system to the final screen coordinate system with all the CSS, scrolling, zooming and etc. that may imply.

Working with SVG User Coordinates

Now in order to define operations on the SVG using user input you are going to need to be able to work your way back from the current state to the initial coordinate system. Again, the best way to do this is with the consolidated transform list you get from el.transform.baseVal.consolidate().matrix Here's a few things to keep in mind about that list for the present discussion.

  1. The SVG Transform List matrix is a matrix that encapsulates translation, rotation, skew and scaling in a single construct.

  2. The API provides it in the form of a set of properties; ${ a, b, c, d, e, f }$ where:

    • $a$ represents the cosine of rotation (or scale in $x$)
    • $b$ represents the sine of rotation (or skew in $y$)
    • $c$ represents the -sine of rotation (or skew in $x$)
    • $d$ represents the cosine of rotation (or scale in $y$)
    • $e$ represents translation in $x$, and
    • $f$ represents translation in $y$

With that in mind let's look at our example. The following listing is the event handler that enables moving the length marker in our toy. In order to move the marker we need to get the distance of the mouse cursor from the group origin within its local coordinate system and update the length marker's coordinates within the initial coordinate system. Whew. Welcome to my world. At this point dear reader, you may want to pause, catch your breath, take a moment to play with the toy. That is, think about what all that entails and then we can look at the solution.

drag: ( evt ) => {
    evt.stopPropagation();
    if( ! this.lengthening ) return;
    // GET THE CURSOR COORDINTATES IN SVG USER COORD SYSTEM 
    const mouseSvgCoords = getSVGPoint( this.el.ownerSVGElement, evt.clientX, evt.clientY );
    // GET THE BONE TRANSFORM MATRIX APPLIED OVER THE INITIAL SVG COORDINATE SYSTEM
    const boneTransformMatrix = this.el.transform.baseVal.consolidate()?.matrix;
    // GET THE UNTRANSFORMED ORIGIN COORDS INTO AN SVG Point
    const boneLocalOrigin = this.el.ownerSVGElement.createSVGPoint();
    boneLocalOrigin.x = parseFloat( this.origin.getAttribute("cx") );
    boneLocalOrigin.y = parseFloat( this.origin.getAttribute("cy") );
    // APPLY THE TRANSFORMATION MATRIX TO THE ORIGIN MARKER 
    // TO GET IT'S CURRENT LOCATION AND
    const boneOriginTransformed = boneLocalOrigin.matrixTransform( boneTransformMatrix );
    // COMPUTE THE EUCLIDEAN DISTANCE FROM CURSOR TO THE
    // TRANSFORMED BONE ORIGIN TO GET THE NEW LENGTH
    const distance = getSvgDistance( boneOriginTransformed, mouseSvgCoords );
    // PROJECT THE NEW LENGTH ONTO THE X AXIS OF THE GROUP'S LOCAL COORDINATE SYSTEM
    this.lengthMarker.setAttribute( "cx", distance );
},
  1. First notice how we get the mouse coordinates. Remember: the mouse coordinates come from the screen coordinate system. To use them we have to transform them to the SVG user coordinate system using the inverse of the screen transform in getSVGPoint as described above.

  2. Next we need to get the group's local transformation matrix in order to apply it for our calculations. Remember from our tip, we do that using baseval.consolodate ...

    const boneTransformMatrix = this.el.transform.baseVal.consolidate()?.matrix;

  3. Once we have the matrix we can apply it to get the euclidean distance we're after in the bone group's local (transformed) coordinate system.

  4. Finally we can project that distance onto the bone's x-axis back in the initial coordinate system. This works to set the new length because as we saw above the bone's inital orientation is 0 degrees and it's length component is entirely determined by the difference on $x$ with the origin.

Bang! With the matrix operation and inverse available it's as easy as 1, 2, 3, right? It's not so bad once stop and think about it.

Discussion

In this post we examined coordinate systems and transforms with an eye toward de-mystifying some of the core concepts central to creating artworks with SVG. In particular we applied transformation matrices to unwind the seemingly convoluted path spanning screen, viewport and SVG coordinate spaces. Along the way we gained a deeper understanding of SVG transforms and explored the benefits of working with transformation lists in matrix form to truly own important techniques for positioning and orienting components.

At the end of the day the most important thing boils down to knowing which coordinate system your API calls operate in and how this impacts the operations you wish to perform. I think it's safe to say most bugs that land you in transform hell boil down to getting this part wrong.

So what do you want to watch out for in designing and developing interactive SVG applications?

  • viewBox Scaling Understand and explicitly using SVG viewBox settings are key to identifying and controlling unexpected results in transforming svg elements and groups.

  • Knowing Which End is Up Understand the various coordinate systems at play in working with SVG and know how to effectively translate between them

  • API Calls Know that mouse coordinates are provided in CSS pixels and remember how to relate screen transformation matrices obtained with getScreenCTM to SVG transform histories obtained with element.transform.baseval.consolodate().matrix .

So don't feel like you have to memorize an abundance of arcane API calls. If you can keep these core notions in mind in reasoning about your issues the rest will follow!

Conclusion

Admittedly, the SVG specification can initially seem overwhelmingly complex. But as I've learned over and over again in life, "big ideas need big words (or in this case API's) to express them". For those willing to invest a bit of effort toward understanding, SVG offers a rich system within which to express artistic creativity.

The pathway out of SVG transformation hell

Happy coding!

Appendix 1: Bonus! A Custom Decorator to Make Matrix Retrieval Easier for Local Elements

If you've read throught the text you may have noticed a pattern for retrieving the SVG transformation matrix lists for various contexts. Specifically, we have functions like:

  1. svg.getScreenCTM(),

  2. svg.getCTM(), and

  3. element.transform.baseVal.consolidate().matrix

Well, maybe you see where I'm going with this. Clearly, one of these calls just ain't like the others...

So, as an added bonus, here's a nice little decoration to add to SVG elements to provide a cleaner, easier, more consitent interface to the matrix. Use it in good health!

/**
 * Decorator to get the local culmulative transformation matrix of
 * the SVGElement.
 */
SVGElement.prototype.getLCTM = function() {
  if (this.transform && this.transform.baseVal) {
    const matrix = this.transform.baseVal.consolidate()?.matrix;
    return matrix ? new DOMMatrix( matrix ) : this.ownerSVGElement.createSVGMatrix();
  } else {
    return this.ownerSVGElement.createSVGMatrix();
  }
};

To use this decorator simply add it to a relevent module. Then you can do things like...

const mySvgElement = document.getElementById('mySvgElement');
const lctm = myElement.getLCTM(); // expect a transform list matrix...

Resources

  1. W3C Coordinate Systems, Transformations and Units

  2. SVGTransformList

  3. SVGGraphicsElement: getCTM() method

  4. SVGTransform

  5. SVGGraphicsElement: getScreenCTM() method

  6. SVGGraphicsElement