Beneath the World Tree: Understanding SVG as Scene Graph
In this Post ...
I'll look at Scalable Vector Graphics (SVG) as a scene graph. Then, in light of this conceptualization, I'll share some very practical insights I had working with SVG transforms ...
Introduction
Modern computer graphics systems rely heavily on scene graphs. Part of the beauty of SVG is that it is, inherently, itself precisely that. As such, when you adopt and work with SVG you get all the benefits of a scene-graph structure for free. If you're interested in creating complex SVG artwork, understanding that structure and what it carries with it is invaluable.
SVG as Scene Graph
Let's step back and revisit exactly what a scene graph is:
A scene graph is a tree-structured hierarchy of nodes and leaves that defines the relationships between all parts of a scene.
In the SVG scene graph, the root node is the svg element itself, branch nodes are the g elements, and leaf nodes are the shapes, paths, and curves that make up the visible parts of a scene.
The World Tree Analogy
While working on a video game recently, thinking deeply about the SVG scene graph reminded me of old Norse cosmology. Now a staple of comic books and Disney movies, I've been a fan of Norse myths ever since reading the Poetic Eddas in my teens. Remember Yggdrasil, the World Tree? In Norse myths, the realms of gods, humans, elves, giants, dwarves, and so on are connected by the roots and branches of this cosmic tree. Yggdrasil is the center of the Norse universe, holding together the nine realms and all the creatures living within them. In this regard, it is like the "axis of the cosmos".
I like this analogy because it's practically a literal mapping. The notion of mythical realms connected by the branches of Yggdrasil neatly parallels the nested coordinate spaces -- or subtrees -- in a scene graph, each with its own local behavior, timing and visual scale.
-
Like the world tree, the SVG scene-graph has a root which defines the axes of the SVG world coordinate system.
-
The root can give rise to any number of branches -- the
gelements that form transform hierarchies for sprites, backgrounds, and other scene-graph objects. -
And the branches ultimatly end in leaves -- the renderable shapes and paths that bring the scene to life (like "the gods riding the graph" in the world-tree artwork).
In the following discussion I'll use the world-tree illustration to outline a practical process for creating modular artwork using the SVG scene graph concept.
A Process for Creating Modular Artworks
First, what do I mean by modularity in design. For me, it's a guiding principle in creating SVG art. Modularity is another blog-post in-and-of-itself but, for now, just think of it as ...
... creating complex visuals with self-contained re-usable graphical components that can be combined, transformed, and animated independently, yet composed seamlessly to create a whole that is "greater than the sum of its parts".
To illustrate, let's look at some examples from the World Tree.
Process part 1: Know your root
First and foremost, it bears reiterating that all SVG's have a root element: the <svg> tag itself. The <svg> root offers intriguing possibilities as a "window" into your SVG worlds. With respect to the scene graph, the <svg> node defines a coodinate system relative to which all the branches in the scene graph can be positioned scaled and rotated ...
The SVG "World Coordinate System" esblishes the base origin and axes for all descendant coordinate systems in your scene graph.
Process part 2: Define modular components around the SVG world origin
Perhaps the most important tip I can offer for SVG scene creation is this:
Develop your model components (e.g., sprites, scene elements, etc.) around the SVG world origin.
Here's an example. If you look closely at "the world tree" (try zooming in with a pinch or mouse), you'll spot the mythic Thor riding down the rainbow bridge in his goat-drawn cart.
How would you create something like that?
The naive approach would be to try to create the sprite directly inside one large monolithic SVG. The modular approach is to create the sprite independently centering it around the SVG world coordinates origin. This will make your life so much easier downstream when it's time to bring the sprite into a larger composition.
[[ INSERT FIGURE ROTAING OUT THE WHEEL ... ]]
The important takeaway here is you don't have to worry about sprite placement details at creation time. If you build modular components centered around the SVG world origin scene composition downstream is dramatically simplified. Trust me on this one.
Process part 3: Use a scene graph "wrapper element" to integrate modular components
For scene composition: use SVG <g> tags to create scene graph branches holding your components.
For example, after creating the Thor sprite I wrapped the whole rig in a g-tag. This group-structure gives artists and developers both a powerful handle: each scene graph group can be positioned rotated and scaled independently of other elements using the magic of the transformation matrix.
Advanced Topic: SVG Transformation Matrices
(OK to skip if you're a math hater -- but I know we all love math here...)
So what exactly is an SVG transformation matrix anyway?
I've written extensively on SVG transformations elsewhere 1, 2 but it's worth revisiting the concepts here because repetion aids learning. (Full disclosure; I, myself, never stop learning).
This time around I want to add some insights that tie directly into the preceding discussion; namely how transformation matrices connect the scene graph hierarchy and modular design.
Matrix structure
First, let's recall the SVG transformation matrix. In SVG (and CSS too, by the way), transformations are applied through a $3 \times 3$ affine matrix.
Each element corresponds to part of an affine transformation -- translation, scale and skew:
- a : scale on x
- b : y sheer (skew factor)
- c : x sheer (skew factor)
- d : scale on y
- e : translation along x
- f : translation along y
Rotation can also be captured in matrix form:
And rotation can be combined with scale by adding scale factors, $Sx$ and $Sy$ :
- a = $S_x \cdot cos(\theta)$
- b = $S_x \cdot sin(\theta)$
- c = $-S_y \cdot sin(\theta)$
- d = $S_y \cdot cos(\theta)$
Yes. That's why poor Alice is in tears. But the good news is you don't have to do any of this math by hand. You'll get it for free from the browser engine and your well designed SVG scene graph!
Bottom line?
The SVG transform matrix elements -- the a, b, c, d, e, and f paramters encode all the affine transformations (including translation, rotation and scale) at each step along the scene graph.
Thats powerful magic!
In Practice: Working with Transforms on the SVG Scene Graph
Armed with this deeper understanding of transformation matrices, let's explore how to use them in practice. In SVG, we have two main methods to inspect an element's transformation matrix: getCTM() and getScreenCTM(). It's mission critical to understand the differences between these two, so let's demystify them here.
getCTM()
getCTM() returns the cumulative transformation matrix associated with any SVG graphics element. This means that it gives you all the accumulated transformations (in matrix form) from the SVG root all the way down to the SVG element in question [INSERT FOOTNOTE ABOUT VIEWBOX TRANSFORMS ... ].
PRO-TIP
Use getCTM() when you need to work on element transformations relative to the SVG world coordinate system.
For example, suppose we're in the world-tree and want to plan a trip to Asgard. To find where Asgard sits relative to the world origin, we can use getCTM().
Given the structure:
<svg ...>
...
<g id="asgard" ...>
<path ... />
...
</g>
...
</svg>
Where <svg> is the root, and <g id="asgard"> is a scene-graph node containing all the paths (leaf nodes) that define "Asgard". Given all that we can find our way to asgard using:
const asgardGroup = document.getElementById( "asgard" ); const M_root_to_asgard = asgardGroup.getCTM( );
M_root_to_asgard will take the form of an SVG transform matrix as I just described -- with properties a through f embodying the affine transformation up through to the SVG root. So if we want the x and y coordinates for the asgard group's translation we can simply write:
const tx = M_root_to_asgard.e ; const ty = M_root_to_asgard.f ;
Understanding the transformation matrix is the very essense of understanding scene graphs -- not just for SVG but in all computer graphics systems. In SVG, the cumulative transformation matrix (CTM) elegantly captures the translation, rotation and scale operations applied from any branch node all the way back to the SVG root. We can express that relationsip in a singular expression:
$$ \text{SVG world coordinates} \xrightarrow{M_{CTM}} \text{Element Local Coordinates} $$
In plain English: "Get me from the SVG World coordinate system to the local Asgard platform coordinate system using the cumulative transform matrix".
If we ever need to "undo" this transformation (for example to find our way back from Asgard and work with world normalized coordinates we can do so simply by applying the inverse matrix:
$$ \text{Element Local Coordinates} \xrightarrow{M_{CTM}^{-1}} \text{SVG world coordinates} $$
Bottom line: Just remember to use getCTM() to map between a node's local coordinate space and the global, SVG world coordinate system.
getScreenCTM()
In contrast, getScreenCTM() returns the cumulative transformation matrix of an element all the way up through the SVG root and beyond (into the SVG host's coordinate system).
In creating interactive artworks with SVG you'll often need to consider not one, not just two, but three coordinate systems:
-
Element local coordinates -- the coordinate system of your local branch or leaf node(s)),
-
SVG world coordinates -- the SVG root coordinate basis, and also
-
Client (screen) coordinates -- the coordinate system of the host HTML document or browser window.
To see what I mean, play around with the interactive widget I included below (if you're reading this on a computer).
As we've seen, the SVG viewport defines its own world coordinate system relative to which all scene-graph elements are positioned. But the browser (or "client viewport") adds another layer of coordinates on top of that.
So if you want to support user interaction -- like touches clicks pinches etc. -- you need to account for the offset between the SVG's internal origin and the client's coordinate origin. This can get tricky, especially when scrolling or layout changes are involved.
The good news: getScreenCTM() handles all this for us transparently.
In summary: use getScreenCTM() when you need a matrix that includes all transformations, from your target element up through the SVG root and beyond (i.e., one that includes any client-space offsets introduced by the host page).
This is especially useful for interactive SVGs embedded in web pages. When mapping user input events (like mouse clicks or touch positions) from client coordinates into your SVG scene graph, remember to use the inverse of the screen CTM:
$$ \text{Client (browser) Coordinates} \xrightarrow{M_{ScreenCTM}^{-1}} \text{Scene Graph Element Local Coordinates} $$
One Last Example
Let's wrap up the discussion with one last example. To see getScreenCTM() in action (and better understand why you might need it) try looking for the pot of gold at the end of the rainbow (hint: try tapping or clicking near the end of the rainbow bridge in the World Tree SVG). If you find the gold it will show you its coordinates -- both the client and also the SVG world coordinates. For handling objects like this based on user events you'll need to be able to work your way back and forth between the client coordinate system and SVG world coordinates as the following fragment using getScreenCTM() shows:
const svgRoot = document.getElementById( "my_svg_root" );
svgRoot.addEventListener('pointerdown',
( event ) => {
const pt = svgRoot.createSVGPoint();
pt.x = event.clientX;
pt.y = event.clientY;
const svgP = pt.matrixTransform( camera.getScreenCTM().inverse() );
...
}
);
Notice, in this example we set up an event listener to handle mouse clicks (touches on mobile screens) which:
-
Uses the factory to create an SVG point object,
-
Reads in client $X$ and $Y$ coordinates from the event, and
-
Converts them to SVG world coordinates using
camera.getScreenCTM().inverse().
This example works because of the SVG scene graph structure. I have a <g> element holding transformations associated with a virtual camera which enables me to invert the transformations applied to simulate zoom, panning tilt, etc.. That's what enables mapping the client coordinates to the correct coordinates on the SVG world coordinate system.
Summary and Conclusion
In this blog post I illustrated the concept of the SVG scene graph by way of analogy with Yggdrasil, the metaphorical world tree from norse mythology. What I love about the world-tree myth is that it echoes the concept of the scene graph not just decoratively but structurally. To me it makes perfect sense: the World Tree as the root node, branches as transform hierarchies, leaves or subrealms as local coordinate systems. And Thor's chariot, coursing through one branch or realm, becomes a living example of hierarchical motion within that structure. It's a beautiful conceptual bridge -- both mythically and technically. It makes perfect sense.
So it was fun for me to leverage the mythology to illustrate conceptually the idea of SVG-as-scene-graph. The beauty of the structure is that it makes transormation, layering and animation so much easier to mangage. Especially for multi-layered pseudo-3D platforms and perspectives. I truely hope this work inspires others to consider SVG as star player for a wide range of artistic applications!
More to Come
When I originallly sat down to hammer out this post I'd intended to include coverage of the SVG transform API for creative coders. But it quickly became apparent that there is a lot of material to cover there. So, rather than overwhelm here, look for a future post on this important topic. In the meantime, I'm including a number of appendices with the source code for important utility functions available in the SVG Creators Collaborative™ framework. Functions that creative coders can employ to create SVG artwork starting today.
Appendix: Utility Functions to Get Local Transformation Mattrices in SVG
For those who've made it this far you're doing great. One quesion you may be asking though is; "Fine Dr. Nick. You've shown how to get the cumulative transformation matrix from the SVG root to any branch, but what about the transformation for more deeply nested branches and their parents?
... ELABORATE??!!!
getTM
/**
* Get the *local* transformation matrix on an element (i.e.,
* the transform matrix between the element and its parent).
*
* @param(SVG Graphics Element) el -- any SVG graphics element
*
* @returns any transformations applied to the element in
* matrix form or identity matrix if none.
*/
function getTM( el ) {
const svg = el.ownerSVGElement || el;
const list = el.transform?.baseVal;
if (!list || list.numberOfItems === 0)
return svg.createSVGMatrix(); // identity
return list.consolidate().matrix;
}
Example Usage:
const mySprite = document.querySelector('my_sprite');
// get the local transformation matrix off your sprite...
const M = getTM( mySprite );
console.log(M.a, M.b, M.c, M.d, M.e, M.f);
getTM as a Prototype Extension
For those who'd prefer to add the getTM utility as a prototype extension (consistent with the CTM getter semantics) here's the implementation from the SVG Creator's Collab™ framework.
// Extend SVGGraphicsElement (covers <g>, <path>, <rect>, <circle>, etc.)
if ( !SVGGraphicsElement.prototype.getTM ) {
SVGGraphicsElement.prototype.getTM = function() {
const svg = this.ownerSVGElement || this;
const list = this.transform?.baseVal;
if (!list || list.numberOfItems === 0) {
return svg.createSVGMatrix(); // return the identity matrix
}
// Consolidate the transform list into a single matrix
const consolidated = list.consolidate();
// defensive coding against browsers that return null
// rather than identity...
return consolidated ? consolidated.matrix : svg.createSVGMatrix();
};
}
Example Usage:
const mySprite = document.querySelector('my_sprite');
const M = mySprite.getTM();
console.log(M.a, M.b, M.c, M.d, M.e, M.f);
Appendix 2: BONUS! Get the Cumulative Transformation Matrix between Arbitrary Scene Graph Nodes
/**
* SVGGraphicsElement prototype extension to get the
* *cumulative transformation matrix* relative to any
* arbitrary ancestor in the SVG scene graph ...
*/
if ( !SVGGraphicsElement.prototype.getRelativeCTM ) {
/**
* Get the *cumulative transformation matrix* relative to ...
*
* @param( SVGGraphicsElement) ancestralGE -- any arbitrary
* ancestor in the SVG scene graph ...
*
* @returns the cumulative transformation matrix between nodes...
*/
SVGGraphicsElement.prototype.getRelativeCTM = function( ancestralGE ) {
if ( !( ancestralGE instanceof SVGGraphicsElement ) ) {
throw new TypeError("[getRelativeCTM]: parameter is not an SVGGraphicsElement");
}
// Ensure the element is on the SVG DOM
const svg = this.ownerSVGElement;
if (!svg || svg !== ancestralGE.ownerSVGElement) {
throw new Error("[getRelativeCTM]: Elements must share the same <svg> root...");
}
// ( Multiply elementCTM by inverse( ancestorCTM ) )
const elementCTM = this.getCTM();
const ancestorCTM = ancestralGE.getCTM();
const relativeMatrix = ancestorCTM.inverse().multiply( elementCTM );
return relativeMatrix;
};
}