获取元素的边界框,说明其转换

Get bounding box of element accounting for its transform

本文关键字:说明 转换 元素 边界 获取      更新时间:2023-09-26

给定此 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>​

生成的输出为 [0, 0, 50, 50]
所需的结果是 [39.645,39.645,70.711,70.711] .

视觉版本:http://jsfiddle.net/2wpju/7/

计算元素相对于其父元素的边界框的最简单、最有效的方法是什么?


以下是我到目前为止想出的最佳答案,但有两个问题:

    似乎有很多
  1. 工作可以更好地处理,并且
  2. 如本演示所示,它不一定是最小边界框(注意上面的圆圈(,因为旋转轴对齐边界框的轴对齐边界框总是会增加大小。

 

// 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;
}

你只需要使用这个简单的Javascript函数:

object.getBoundingClientRect();

它受以下支持:

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

查看此处以获取更多信息。链接

如果您不想处理数学,一个好的解决方法是将 SVG 元素包装在 <g> 组中,然后在新组上调用 getBBox()

这是它的伪代码


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()
    }

这似乎是当前的方法,除非/直到getBBox((认为对象上的转换直接定义了边界框,即使SVG 1.1 SE定义了:

SVGRect getBBox(( 返回当前用户空间中(即,在应用"transform"属性后,如果有的话(对所有包含的图形元素的几何图形的紧密边界框,不包括描边、剪切、遮罩和滤镜效果(。请注意,getBBox 必须在调用该方法时返回实际的边界框,即使元素尚未呈现也是如此。

我想这完全取决于您如何定义">当前用户空间">

这是一个解释旋转椭圆的答案。转换方法适用于矩形,但在椭圆上,如果旋转对象,则会返回一个比对象大得多的边界框。

使用内置JavaScript函数的简单答案对我来说不是很有用,因为它仍然需要转换回绘图空间。

此代码不处理椭圆的倾斜或缩放(但处理任何其他对象(,但在我 http://nwlink.com/~geoffrey/svgBuild 程序中,我打算通过绘制应用倾斜或缩放的全新椭圆来处理倾斜和缩放。

椭圆的

公式来自这里:如何计算椭圆的轴对齐边界框? 非常感谢那里的帮助。

el 是 D3.js 元素,而不是节点。

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;
    }
}

死灵法术。

所需的边界框坐标始终相对于要在其中使用坐标的元素的变换。

例如,您在

<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>

现在您要添加另一个组

<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>

现在,您要将该新组中的文本元素坐标和矩形坐标放入id为"e123"(矩形(的元素的边界框中。

所以你拿

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

此边界框考虑了您在 e123 中的转换,也就是在应用"transform"属性之后。如果你把这些坐标放在 e123 之外(想象一下 e123 是一个组而不是一条路径(,那么坐标将被该变换关闭。

因此,您需要获取相关元素的边界框(fromSpace(,并将它们转换为适合目标元素的坐标,例如"e456"。

你获取fromSpace边界框的坐标,创建两个点(左上角,右下角(,将它们转换为目标空间,然后从这两个点计算新的边界框(请注意,当您仅将x2设置为bbox.width和y2设置为bbox.height时,它不起作用 - 您需要另一个点,然后在将矩阵应用于每个点后重新计算宽度和高度(。

在代码中:

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;
}

所以你像这样使用它:

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

如果你只想要父元素坐标中的边界框,这将是:

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

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

或作为辅助函数

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

然后易于使用:

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

(请注意,这假设 parentElement 不为 null(

一种方法是制作一个拉斐尔集并得到它的bb。或者将所有元素包装在 g 中并得到 g 的 bb,我认为 g 应该是最快的。