Phrogz
Phrogz

Reputation: 303549

Get bounding box of element accounting for its transform

Given this SVG:

<svg xmlns="http://www.w3.org/2000/svg">
  <rect width="50" height="50"
        transform="translate(75,75) rotate(45) translate(-25,-25)" />
  <script>
    var bb = document.querySelector('rect').getBBox();
    console.log([bb.x,bb,y,bb.width,bb.height]);
  </script>
</svg>​

The resulting output is [0, 0, 50, 50].
The desired result is [39.645,39.645,70.711,70.711].

Visual Version: http://jsfiddle.net/2wpju/7/

What's the simplest, most efficient way to calculate the bounding box of an element with respect to its parent element?


Below is the best answer I've come up with so far, but there are two problems:

  1. It seems like a lot of work for what might be handled better, and
  2. As seen in this demo it's not necessarily the minimum bounding box (notice the upper circle), since the axis-aligned bounding box of a rotated axis-aligned bounding box always increases the size.

 

// Calculate the bounding box of an element with respect to its parent element
function transformedBoundingBox(el){
  var bb  = el.getBBox(),
      svg = el.ownerSVGElement,
      m   = el.getTransformToElement(el.parentNode);

  // Create an array of all four points for the original bounding box
  var pts = [
    svg.createSVGPoint(), svg.createSVGPoint(),
    svg.createSVGPoint(), svg.createSVGPoint()
  ];
  pts[0].x=bb.x;          pts[0].y=bb.y;
  pts[1].x=bb.x+bb.width; pts[1].y=bb.y;
  pts[2].x=bb.x+bb.width; pts[2].y=bb.y+bb.height;
  pts[3].x=bb.x;          pts[3].y=bb.y+bb.height;

  // Transform each into the space of the parent,
  // and calculate the min/max points from that.    
  var xMin=Infinity,xMax=-Infinity,yMin=Infinity,yMax=-Infinity;
  pts.forEach(function(pt){
    pt = pt.matrixTransform(m);
    xMin = Math.min(xMin,pt.x);
    xMax = Math.max(xMax,pt.x);
    yMin = Math.min(yMin,pt.y);
    yMax = Math.max(yMax,pt.y);
  });

  // Update the bounding box with the new values
  bb.x = xMin; bb.width  = xMax-xMin;
  bb.y = yMin; bb.height = yMax-yMin;
  return bb;
}

Upvotes: 36

Views: 21743

Answers (6)

Islam Abdalla
Islam Abdalla

Reputation: 1514

If you do not want to deal with math, a good workaround is to wrap the SVG element in a <g> group and then call getBBox() on the new group.

Here is pseudo code for it


function getBBoxTakingTransformIntoAccount(element) {
        let group = wrapInGroup(element)
        let bBox = group.getBBox()
        unwrapChild(group) // Optionally, you can unwrap the child node
        return bBox
    }

function wrapInGroup(element) {
        let group = document.createElementNS("http://www.w3.org/2000/svg", "g")
        element.parentNode.appendChild(group)
        group.appendChild(element)
        return group
    }

function unwrapChild(element) {
        let child = element.children[0]
        element.parentNode.appendChild(child)
        element.remove()
    }

Upvotes: 3

Stefan Steiger
Stefan Steiger

Reputation: 82406

Necromancing.

The bounding-box coordinates you need are always relative to the transform of the element you want to use the coordinates in.

e.g. you have a path in a

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2000 577">
  <g id="test">
    <path id="e123" d="m906.76 442.4h51.77v39.59h-51.77z" fill="#B3B3B3" fill-rule="evenodd" transform="matrix(1,0,0,0.999749,0,-75.2041)"/>
  </g>
</svg>

Now you want to add another group

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2000 577">
  <g id="test">
    <path id="e123" d="m906.76 442.4h51.77v39.59h-51.77z" fill="#B3B3B3" fill-rule="evenodd" transform="matrix(1,0,0,0.999749,0,-75.2041)"/>
  </g>

  <g id="e456" data-group-id="AP">
    <rect class="BG" fill="yellow" x="906.760009765625" y="390.5399169921875" width="15.96875" height="16.125"/>
    <text id="foobar" fill="#000" font-size="1.5mm" data-lines-count="4" x="906.760009765625" y="390.5399169921875">
      <tspan x="906.760009765625" dy="1.2em">.</tspan>
      <tspan x="906.760009765625" dy="1.2em">ABC123</tspan>
      <tspan x="906.760009765625" dy="1.2em">Test</tspan>
      <tspan x="906.760009765625" dy="1.2em">123</tspan>
    </text>
  </g>


</svg>

And now you want to put the text-element coordinates and the rectangle coordinates in that new group to the bounding-box of element with id "e123" (a rectangle).

So you take

document.getElementById("e123").getBBox();

This bounding box takes into account the transform you have in e123, aka after application of the ‘transform’ attribute. And if you take these coordinates outside e123 (imagine e123 was a group instead of a path), then the coordinates will be off by that transform.

Therefore, you need to take the bounding box of your element in question (fromSpace), and transform them to coordinates appropriate for your target element, e.g. "e456".

You take the coordinates of the bounding-box of your fromSpace, create two points (top-Left, bottom-right), transform them into the target space, and from these two points, compute the new bounding-box (note that it doesn't work when you just set x2 to bbox.width and y2 to bbox.height - you need the other point, and then recompute width and height after applying the matrix to each point).

In code:

if (!SVGElement.prototype.getTransformToElement)
{
    SVGElement.prototype.getTransformToElement = function (elem)
    {
        return elem.getScreenCTM().inverse().multiply(this.getScreenCTM());
    };
}


function boundingBoxRelativeToElement(fromSpace, toSpace, bbox)
{
  bbox = bbox || fromSpace.getBBox();
  
  // var m = fromSpace.transform.baseVal[0].matrix;
  var m = fromSpace.getTransformToElement(toSpace);
    
  var bbC = document.documentElement.createSVGPoint(); bbC.x = bbox.x; bbC.y = bbox.y;
  var bbD = document.documentElement.createSVGPoint(); bbD.x = bbox.x + bbox.width; bbD.y = bbox.y + bbox.height;
  // console.log("bbC", bbC, "bbD", bbD);
  bbC = bbC.matrixTransform(m);
  bbD = bbD.matrixTransform(m);
    
  var bb = { x: bbC.x, y: bbC.y, width: Math.abs(bbD.x - bbC.x), height: Math.abs(bbD.y - bbC.y)};
  console.log("bb", bb);
  return bb;
}

So you use it like this:

var elFrom = document.getElementById("e123");
var elTo = document.getElementById("e456");
var bbox = boundingBoxRelativeToElement(elFrom, elTo);

And if you just want the boundingBox in parentElement-coordinates, this would be:

var elFrom = document.getElementById("e123");
var elTo = elFrom.parentElement;
var bbox = boundingBoxRelativeToElement(elFrom, elTo);

aka

var elFrom = document.getElementById("e123");
var bbox = boundingBoxRelativeToElement(elFrom, elFrom.parentElement);

or as a helper function

function boundingBoxRelativeToParent(el)
{
    var bbox = boundingBoxRelativeToElement(el, el.parentElement);
    return bbox;
}

Which is then easy to use:

var bbox = boundingBoxRelativeToParent(document.getElementById("e123"));

(note that this assumes that parentElement is NOT null)

Upvotes: 2

GeeWhizBang
GeeWhizBang

Reputation: 857

This is an answer that accounts for rotated ellipses. The transformation method works OK for rectangles but on ellipses returns a bounding box considerably larger than the object if it is rotated.

The simpler answer of using the builtin JavaScript function isn't very useful to me because it would still have to be translated back into the drawing space.

This code doesn't handle skew or scale for ellipses (but does for any other object), but in my program at http://nwlink.com/~geoffrey/svgBuild I intend to handle skew and scale by drawing an entirely new ellipse with the skew or scale applied.

The formula for the ellipses came from here: How do you calculate the axis-aligned bounding box of an ellipse? much thanks for the help there.

el is a d3.js element, not the node.

function toNumberIfNumeric(x) {
    var result = parseFloat(x);
    return isNaN(result) ? x : result;
}

function getAttrs(el, attributes) {
    var result = {};
    attributes.forEach(function (attr) {
        result[attr] = toNumberIfNumeric(el.attr(attr));
    });
    return result;
}    

function getTransformedBbox(el, referenceNode) {

    var node = el.node();

    switch (node.nodeName) {
        case "ellipse":

            var rotate = getTransform(el, "rotate");
            if (rotate.angle === 0) {
                return node.getBBox();
            }
            var attr = getAttrs(el, ["cx", "cy", "rx", "ry"]);
            var radians = rotate.angle / 180 * Math.PI;
            var radians90 = radians + Math.PI / 2;

            var ux = attr.rx * Math.cos(radians);
            var uy = attr.rx * Math.sin(radians);
            var vx = attr.ry * Math.cos(radians90);
            var vy = attr.ry * Math.sin(radians90);

            var halfWidth = Math.sqrt(ux * ux + vx * vx);
            var halfHeight = Math.sqrt(uy * uy + vy * vy);

            return {
                x: attr.cx - halfWidth,
                y: attr.cy - halfHeight,
                width: 2 * halfWidth,
                height: 2 * halfHeight
            };

        default:
            var bb = node.getBBox(),
                svg = node.ownerSVGElement,
                m = node.getTransformToElement(referenceNode);

            // Create an array of all four points for the original bounding box
            var pts = [
                svg.createSVGPoint(), svg.createSVGPoint(),
                svg.createSVGPoint(), svg.createSVGPoint()
            ];
            pts[0].x = bb.x;
            pts[0].y = bb.y;
            pts[1].x = bb.x + bb.width;
            pts[1].y = bb.y;
            pts[2].x = bb.x + bb.width;
            pts[2].y = bb.y + bb.height;
            pts[3].x = bb.x;
            pts[3].y = bb.y + bb.height;

            // Transform each into the space of the parent,
            // and calculate the min/max points from that.    
            var xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity;
            pts.forEach(function (pt) {
                pt = pt.matrixTransform(m);
                xMin = Math.min(xMin, pt.x);
                xMax = Math.max(xMax, pt.x);
                yMin = Math.min(yMin, pt.y);
                yMax = Math.max(yMax, pt.y);
            });

            // Update the bounding box with the new values
            bb.x = xMin;
            bb.width = xMax - xMin;
            bb.y = yMin;
            bb.height = yMax - yMin;
            return bb;
    }
}

Upvotes: 2

Minok
Minok

Reputation: 522

That seems to be the current method, unless/until getBBox() considers the transform on the object as directly defining the bounding box, even though SVG 1.1 SE defines:

SVGRect getBBox() Returns the tight bounding box in current user space (i.e., after application of the ‘transform’ attribute, if any) on the geometry of all contained graphics elements, exclusive of stroking, clipping, masking and filter effects). Note that getBBox must return the actual bounding box at the time the method was called, even in case the element has not yet been rendered.

I guess its all about how you define 'current user space'

Upvotes: 2

Progo
Progo

Reputation: 3490

All you need to use is this simple Javascript function:

object.getBoundingClientRect();

It is supported in:

Chrome  Firefox (Gecko)   Internet Explorer Opera   Safari
1.0           3.0 (1.9)   4.0               (Yes)   4.0

Look here for more about it. Link

Upvotes: 3

Timo K&#228;hk&#246;nen
Timo K&#228;hk&#246;nen

Reputation: 12210

One way is to make a raphael set and get bb of it. Or wrap all elements inside g and get bb of g, i assume that g should be fastest.

Upvotes: 1

Related Questions