Reputation: 303549
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]
.
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:
// 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
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
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
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
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
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
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