493: Undecipherable

-Blog-

-Projects-

-About me-

-RSS-

Pitfalls in SVG Positioning

Dennis Guse

_Update_: SVGGraphicsElement.getTransformToElement() was removed from the current draft.

In difference to pixel-based images formats, like JPEG or PNG, do SVG images allow to add animations, can be programmed and can react to user input. If an SVG image is embedded in a webpage, such an image can be modified with JavaScript as needed. For example change the text, color or change position of elements in the image.

In SVG elements can be positioned with by setting their a) x/y-coordinates and b) using transform. Whereas the first just sets the coordinates relative to the current coordinate system of the object, transform modifies the coordinate system for all this object and all childs.

Positioning via coordinates only:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="300" height="80">
  <rect x="160" y="10" width="60" height="60" fill="blue"/>
  <rect x="230" y="10" width="60" height="60" fill="green"/>
</svg>

Positioning via transform=”translate(x, y)”:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="80">
  <rect x="0" y="0" width="60" height="60" fill="gray" transform="translate(0, 0)"/>
  <rect x="0" y="0" width="60" height="60" fill="green" transform="translate(120, 0)"/>
</svg>

Both images are rendered exactly the same and from a viewer’s perspective are therefore equal. However, those are not equal from a programmatic perspective they are not.

TL;DR: When you need to re-position SVG-elements, try to avoid transform.

Here is an example with three rectangles. id=”rect1” and id=”rect3” are positioned via coordinates whereas id=”rect2” and id=”rect2-inner” are positioned via transform(s).

<rect id="rect2-inner" x="15" y="15" width="30" height="30" fill="black""/>

Two functions are required without transform svg-object.getBBox() and with transform svg-object.getTransformToElement(svg-object). The first, returns the minimal rectangle that contains the current object including x/y-coordinates. The second, calculates the shift between the virtual coordinate systems due to transform.

var rect1 = document.getElementById("rect1")
var rect2 = document.getElementById("rect2");
var rect2inner = document.getElementById("rect2-inner");
var rect3 = document.getElementById("rect3");
var rect3inner = document.getElementById("rect3-inner");

rect3.getTransformToElement(rect1).e //x-shift = 0 (no transform)
rect2.getTransformToElement(rect1).e //x-shift = 120 (transform)
rect2inner.getTransformToElement(rect1).e //x-shift = 120 (transform)

//Get x-coordinates _relative_ each coordinate system.
rect1.getBBox().x //0
rect2.getBBox().x //0 (moved by transform)
rect2inner.getBBox().x //15 (moved by transform AND x)
rect3.getBBox().x //240

//Move rect3inner to center of rect1: x-only (no transform)
rect3inner.setAttributeNS(null, "transform", "translate(" + (rect1.getBBox().x - rect3inner.getBBox().x + rect3inner.getBBox().width/2) + ",0)");
//Move rect2inner to center of rect3: x-only (transform)
transform = rect2inner.getTransformToElement(rect3)
rect2inner.setAttributeNS(null, "transform", "translate(" + (-transform.e + Math.abs(rect3.getBBox().x + rect2inner.getBBox().x) - rect2inner.getBBox().width/2) + ",0)");

In the end both approaches work, but applying transform in addition makes moving more complicated. And as their are nicer things to do than debugging SVGs - try to avoid transform or at least RTFM.

NOTE: For some SVGs (created with (Inkscape)[https://inkscape.org]), I had some trouble due to transform (getTransformToElement() always returned the same values although different transform were applied). Inkscape removes transform attributes, when grouping and then ungroup elements; x/y-coordinates are re-calculated appropriately.