The SVG Scene Graph
In this Post ...
I'll look at SVG as a scene graph. Then, in light of this conceptualization I'll share some very practical insights I had working with the SVG transform APIs ...
Introduction
Modern computer graphics systems rely heavily on the concept of scene-graphs. Part of the beauty of SVG is that it is, inherently, itself a scene graph structure and so you get all the benefits thereunto pertaining for free. So if you're interested in creating any sort of complex artwork in SVG understanding the scene-graph structure and what it buys you is invaluable.
SVG as Scene Graph
First, allow me to step back an (re) visit just exactly what a scene-graph is:
A scene graph is a hierarhical structure comprised of nodes connected by arcs defining relationships among all the parts in a scene.
In SVG the scene graph nodes are the shapes and graphic primitive elements (e.g., circles, rectangles, paths, etc.) and, importantantly grouping elements such as <g> and the root <svg> element itself.
By way of example consider the following structure I developed creating a reference game application for the SVG Creators Collaborative™.
TODO: CREATE SVG IMAGE FOR THIS...
+ svg [root] Msvg
+ g [world_tree_root] Mw <--| WORLD TREE ICONOGRAPHY
+ g [biome_platform_00] Mplatform
+ path
+ g [sprite_root] Msprite
Figure 1: The SVG Scenegraph structure for a hybrid isometric/platform game.
Creating this structure for use in a video game reminded me of the norse cosmology (I've been a fan of norse mythology since reading the poetic eddas in my teens). Now the stuff of comic books and Disney movies, norse myths are popular enough that I feel the analogy is apt so please indulge me.
The World Tree Analogy
Remember Yggdrasil the world tree? In norse myths the realms of gods, women and men, elves, giants, dwarves and so-on are connected by the roots and branches of a "world" tree. The tree, Yggdrasil is the center of the of the norse universe and holds together the nine realms and all the creatures living in them. In this regard it is the "axis of the cosmos".
INSERT THE MAIN ARTWORK HERE ...
width="600" height="600"
I like this analogy because it's almost literal -- 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. Again, it's practically a literal mapping:
-
Like the world tree, a good SVG scene-graph structure will have a root. For reasons I'll expand below, I prefer a g-node that wraps all the graphics and elements in the scene.
-
This world-tree root can contain any arbitrary number of branches which, in SVG, form transform hierarchies for sprites backgrounds and other scene-graph objects.
-
Ultimately, the branches have leaves -- the renderable shapes and paths that comprise the scene (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
Before going to deep, Let me clarify the terminolgy. All SVG's have a root element; the <svg> tag. 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 transformed ...
The SVG "World Coordinate System" is the coordinate system that esblishes an origin for scene graph coordinates.
Process part 2: Define modular components around the SVG world origin
Perhaps the most important tip I can offer for SVG scene creation is to develop 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" (pinch or zoom in with your mouse wheel) you might see Thor riding down the rainbow bridge in his goat-drawn cart. How would you create something like that? The naive approach is to try to create the sprite directly in the scene. A more modular approach is to create the sprite separately, modelling it around the SVG world coordinates origin. Below is an animation showing a small piece of the process...
[[ INSERT ANIMATION OF ROTAING OUT THE WHEEL ... ]]
The important takeaway here is you don't have to worry about placement of the sprite components within the scene at creation time. That will happen downstream in the process.
Process part 3: Use a scene graph "wrapper element" to integrate modular components
Use SVG <g> tags, or groupings, to create scene graph branches containing your components. For example, after creating the "Thor sprite" I wrapped the whole rig in a g-element. This group-structure enables artists and developers to position elements, rotate and scale them independently of other elements using the magic of the transformation matrix.
[[ INSERT EXAMPLE ANIMATION (Do a screen capture) ]]
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. "Repetition aids learning" I always say. And I never stop learning. In any case, I want to add some insights from the above discussion to working with the transformation matrix here.
Matrix structure
First, recall the SVG matrix structure. In SVG (and CSS too by the way), transformations can be applied through a 3 x 3 affine matrix.
The matrix elements map SVG affine transformations including translation, scale and skew as follows:
- a : scale on x
- b : y sheer (skew factor)
- c : x sheer (skew factor)
- d : scale on y
- e : translation on the x axis
- f : translation on the y axis
Rotation can also be captured in matrix form:
And rotation can be combined with scale by adding a scaling factor, $S$ as follows:
- 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 is why poor Alice is in tears. But the good news is you won't have to do any linalg here. SVG and the browser engine will do it for you! So, bottom line?
In SVG transform matrices: the a, b, c, d, e, and f matrix elements can take care of all the affine transformations for you (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
So, armed with this deeper understanding, let's explore working with transforms on the SVG scene graph in practice. In SVG, we have two methods to get a handle to an element's transformation matrix: getCTM() and getScreenCTM(). It's mission critical to understand the differences between the two so it's well worth paying attention as I demystify them here.
getCTM()
getCTM() gets you a handle to the cumulative transformation matrix associated with any SVG graphics element. In other words, it gives you 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, let's say I'm in the world-tree and I want to plan a trip to Asgard. In that case I want to know where Asgard sits relative to the SVG world coordinate system origin. Given a proper scene-graph setup I can easily get that information using getCTM().
Given the structure:
<svg ...>
...
<g id="asgard" ...>
<path ... />
...
</g>
...
</svg>
Where <svg> is my root, and <g id="asgard"> is a scene-graph node containing all the leaf nodes (paths) that define "Asgard". Given all that I can readily find my way there using getCTM():
const asgardGroup = document.getElementById( "asgard" ); const M_root_to_asgard = asgardGroup.getCTM( );
getCTM( ) returns an SVG transform matrix with properties a through f as described above. So if I want the x and y coordinates for the asgard group's translation I write:
const tx = M_root_to_asgard.e ; const ty = M_root_to_asgard.f ;
Understanding the application of the transformation matrix is the very essense of understanding scene graphs -- not just for SVG but in any computer graphics system. In SVG, its cumulative transformation matrix captures translation, rotation and scale operations from any branch node all the way back to the SVG root. Its eloquence can be captured in a singular expression:
$$ \text{SVG world coordinates} \xrightarrow{M_{CTM}} \text{Element Local Coordinates} $$
Which reads: "Get me from the SVG World coordinate system to the local Asgard platform coordinate system using the cumulative transform matrix". If I want to "undo" the scene graph element transform (for example to work on the scene graph element using "normalized" (SVG world coordinates) I can do that simply by applying the inverse transform:
$$ \text{Element Local Coordinates} \xrightarrow{M_{CTM}^{-1}} \text{SVG world coordinates} $$
Bottom line? Just remember: use getCTM() to map between a node's local coordinate system and the SVG world coordinates.
getScreenCTM()
In contrast, getScreenCTM() gets you a handle to the cumulative transformation matrix of an element all the way up through the SVG root and beyond (up through the host page if the SVG lives in an HTML document). In creating interactive artworks with SVG you may find yourself needing to consider not just element local coordinants, not just SVG world-coordinants but also the host system screen coordinants as well. To see what I mean, play around with the interactive widget I included below (if you're reading this on a computer).
We know the SVG viewport defines it's own world coordinate system relative to which the elements of an SVG scene graph can be positioned. But the client viewport has it's own coordinate system on top of the SVG. If you want to support interaction when a user interacts with the SVG (by touching the display for example) the offsets between the viewport origin and the SVG coordinate system must be taken into account, and this can get complicated (especially if scrolling is involved). The good news is in SVG getScreenCTM() takes care of all of that for us!
// TO DO: CONSIDER DROPPING POTS O' GOLD IN MIDGARD AS EXAMPLE ... // THEN YOU CAN SHOW: midgard.getScreenCTM().inverse to position pot using local coordinates ...
In summary: use getScreenCTM() to get the cumulative transformation matrix including offsets based on client coordinate systems. This is especially useful for SVG hosted in web pages. Especially when you need to work with user inputs in these contexts just remember to use the screen CTM inverse to map client coordinates (e.g, mouse-clicks, touches etc.) to SVG scene-graph local coordinates.
$$ \text{Client (browser) Coordinates} \xrightarrow{M_{ScreenCTM}^{-1}} \text{Scene Graph Element Local Coordinates} $$
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 here 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 scene graph 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 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 look for a future post on this important topic. In the meantime, I'm including two appendices with the source for important utility functions available in the SVG Creators Collaborative framework for development which creative coders can employ to create artwork 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 there 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;
};
}