如何获取 SVG 线或路径的边界框,包括 javascript 中的笔划宽度?

问题描述 投票:0回答:1

我正在尝试获取线(或路径、多边形等)的边界框

根据文档,我应该能够做到,line.getBBox({"stroke": true}),但它似乎不起作用。

https://jsfiddle.net/3x5thjsn/

<svg width="100" height="100">
   <line x1="0" y1="40" x2="200" y2="40" style="stroke:rgb(255,0,0);stroke-width:20"/>
   Sorry, your browser does not support inline SVG.
</svg> 
const line = document.querySelector("svg line");
console.log(line.getBBox({"stroke": true}))

如果我有一条笔划宽度为 x 的线条,则该线条的高度或宽度应该占它。在这种情况下,我期望高度为 20,但它给了我 0.

javascript svg
1个回答
0
投票

正如 Robert Longson 评论的那样:
大多数浏览器尚不支持

getBBox({"stroke": true}))
功能选项。

解决方法

  • 如果您正在处理 矩形和非变换 元素:您可以根据
    stroke-width
    stroke-linecap
    属性值轻松添加一些空间/偏移量
  • 对于具有锐角或变换的元素,您可以通过将元素渲染为
    canvas
    来计算 bbox,并按照此处所述搜索不透明像素“计算任意基于像素的绘图的边界框”

示例:自定义 getBBox 辅助函数

// usage:
(async() => {

  let bb1 = await getBBoxCustom(line1);
  //draw bbox
  drawBBox(svg, bb1)
  console.log(bb1)

  let bb2 = await getBBoxCustom(line2);
  drawBBox(svg, bb2)

  let bb3 = await getBBoxCustom(line3);
  drawBBox(svg, bb3)

  let bb4 = await getBBoxCustom(path);
  drawBBox(svg, bb4)

  let bb5 = await getBBoxCustom(rect);
  drawBBox(svg, bb5)

  let bb6 = await getBBoxCustom(rect2);
  drawBBox(svg, bb6)

})();


async function getBBoxCustom(el) {

  let bb = el.getBBox();
  const ns = 'http://www.w3.org/2000/svg';

  /**
   * check element type 
   * stroke properties and transformations
   */
  let type = el.nodeName;
  let parent = el.farthestViewportElement;
  let {
    a,
    b,
    c,
    d,
    e,
    f
  } = parent.getScreenCTM().inverse().multiply(el.getScreenCTM());
  let matrixStr = [a, b, c, d, e, f].map(val => {
    return +val.toFixed(3)
  }).join(' ');
  let hasTransforms = matrixStr === '1 0 0 1 0 0' ? false : true;
  let style = window.getComputedStyle(el);
  let hasStroke = style.strokeWidth && style.stroke ? true : false;
  let hasStrokeLinecap = hasStroke && style.strokeLinecap !== 'butt' ? true : false;
  let strokeWidth = parseFloat(style.strokeWidth);


  /**
   * no stroke - no transforms: 
   * return standard bBox
   */
  if (!hasStroke && hasTransforms) {
    console.log('standard bbox');
    return bb;
  }

  /**
   * no transforms and straight angles: 
   * return standard bBox + stroke widths
   */
  if (
    (type === 'rect' || type === 'line') &&
    !hasTransforms
  ) {
    if (type === 'line') {
      let isVertical = +el.getAttribute('x1') === +el.getAttribute('x2') ? true : false;
      let isHorizontal = +el.getAttribute('y1') === +el.getAttribute('y2') ? true : false;

      if (hasStrokeLinecap || isHorizontal) {
        bb.y -= strokeWidth / 2;
        bb.height += strokeWidth;
      }

      if (hasStrokeLinecap || isVertical) {
        bb.x -= strokeWidth / 2;
        bb.width += strokeWidth;
      }
      return bb;
    }

    if (type === 'rect') {
      bb.x -= strokeWidth / 2;
      bb.width += strokeWidth;
      bb.y -= strokeWidth / 2;
      bb.height += strokeWidth;
      return bb;
    }
  }


  let canvas = document.createElement("canvas");
  let ctx = canvas.getContext("2d");


  //create temporary svg
  let svgTmp = document.createElementNS(ns, 'svg');
  let elCloned = el.cloneNode(true);
  document.body.append(svgTmp)
  //document.body.append(canvas)

  // if transformed - wrap in group recalculate bbox
  if (hasTransforms) {
    let g = document.createElementNS(ns, 'g');
    g.append(elCloned)
    svgTmp.append(g);

    // update bbox
    bb = g.getBBox();
  } else {
    svgTmp.append(elCloned);
  }


  // crop viewBox to element to reduce pixel checks
  let strokeOffset = strokeWidth * 10;
  let {
    x,
    y,
    width,
    height
  } = bb;


  let viewBoxNew = [
    Math.ceil(x - strokeOffset / 2),
    Math.ceil(y - strokeOffset / 2),
    Math.ceil(width + strokeOffset),
    Math.ceil(height + strokeOffset)
  ];

  svgTmp.setAttribute("viewBox", viewBoxNew.join(" "));
  let w = viewBoxNew[2];
  let h = viewBoxNew[3];

  /**
   * set height and width for Firefox
   */
  svgTmp.setAttribute("width", w);
  svgTmp.setAttribute("height", h);
  let svgURL = await new XMLSerializer().serializeToString(svgTmp);


  // draw to canvas
  canvas.width = w;
  canvas.height = h;
  let img = new Image();
  img.src = "data:image/svg+xml; charset=utf8, " + encodeURIComponent(svgURL);
  await img.decode();
  ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

  /**
   * get bbox from 
   * opaque pixels
   */
  let data = ctx.getImageData(0, 0, w, h).data;
  let currentX = 0;
  let currentY = 0;

  let xMin = x + w;
  let xMax = 0;
  let yMin = y + h;
  let yMax = 0;


  /**
   * loop through color array 
   * every 4. item is new pixel
   */
  for (let i = 0; i < data.length; i += 4) {

    // separate array chunk into rgba values
    let [r, g, b, a] = [data[i], data[i + 1], data[i + 2], data[i + 3]];
    if (a > 0) {
      xMin = currentX < xMin ? currentX : xMin;
      xMax = currentX > xMax ? currentX : xMax;
      yMin = currentY < yMin ? currentY : yMin;
      yMax = currentY > yMax ? currentY : yMax;
    }

    // is new "pixel row" – increment y value, reset x to "0"
    if (currentX + 1 >= w) {
      currentX = 0;
      currentY++
    } else {
      currentX++;
    }
  }


  // recalculate viewBox according to previous offset
  let bboxCanvas = {
    x: xMin + viewBoxNew[0],
    y: yMin + viewBoxNew[1],
    width: xMax - xMin + 1,
    height: yMax - yMin + 1
  }

  // remove tmp svg
  svgTmp.remove();
  return bboxCanvas;
}

function drawBBox(svg, bb) {
  svg.insertAdjacentHTML('beforeend', `
    <rect x="${bb.x}" y="${bb.y}" width="${bb.width}" height="${bb.height}" class="bbox" stroke="green" stroke-width="0.75" fill="none"></rect>`);
}
svg {
      border: 1px solid #000;
      width: 100%;
      height: auto;
      overflow: visible;
    }
<svg id="svg" viewBox="0 0 700 200" xmlns="http://www.w3.org/2000/svg">
    <line id="line1" x1="50" y1="40" x2="200" y2="40" style="stroke:rgb(255,0,0);stroke-width:20px" />
    <line id="line2" x1="100" y1="150" x2="200" y2="150" stroke-linecap="square"
      style="stroke:orange;stroke-width:20px" />
      <rect x="500" y="50" width="60" height="20"  id="rect" stroke="gray" stroke-width="3" />
      <rect x="500" y="20" width="60" height="20"  id="rect2" stroke="gray" stroke-width="3" transform="rotate(45 400 100)"/>
    <line id="line3" x1="400" y1="20" x2="400" y2="150" stroke-linecap="square"
      style="stroke:gray;stroke-width:20px" />
    <path id="path" d="M600 150 l5 -50 l5 50z" stroke-width="5" stroke="red" stroke-linejoin="miter"
      stroke-miterlimit="20" fill="#fff" paint-order="stroke" />
  </svg>

如何运作

上面的辅助函数首先检查多个条件避免更昂贵的基于画布的计算

  • 完全没有中风——使用标准
    getBBox()
  • 元素是
    <rect>
    <line>
    且未转换:根据笔划属性增加宽度/高度和 x/y 值。例如:
/**
 * no transforms and straight angles: 
 * return standard bBox + stroke widths
*/ 
if (
    (type === 'rect' || type === 'line') &&
    !hasTransforms
) {
    if (type === 'line') {
        let isVertical = +el.getAttribute('x1') === +el.getAttribute('x2') ? true : false;
        let isHorizontal = +el.getAttribute('y1') === +el.getAttribute('y2') ? true : false;

        if (hasStrokeLinecap || isHorizontal) {
            bb.y -= strokeWidth / 2;
            bb.height += strokeWidth;
        }

        if (hasStrokeLinecap || isVertical) {
            bb.x -= strokeWidth / 2;
            bb.width += strokeWidth;
        }
        return bb;
    }

    if (type === 'rect') {
        bb.x -= strokeWidth / 2;
        bb.width += strokeWidth;
        bb.y -= strokeWidth / 2;
        bb.height += strokeWidth;
        return bb;
    }
}
  • 如果元素不是矩形或变形:将元素渲染为
    <canvas>
    并检查不透明像素——通过
    getImageData()
  • 检索
// draw to canvas
    canvas.width = w;
    canvas.height = h;
    let img = new Image();
    img.src = "data:image/svg+xml; charset=utf8, " + encodeURIComponent(svgURL);
    await img.decode();
    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);

    /**
     * get bbox from 
     * opaque pixels
     */
    let data = ctx.getImageData(0, 0, w, h).data;
    let currentX = 0;
    let currentY = 0;
    let xMin = x+w;
    let xMax = 0;
    let yMin = y+h;
    let yMax = 0;

    /**
     * loop through color array 
     * every 4. item is new pixel
     */
    for (let i = 0; i < data.length; i += 4) {

        // separate array chunk into rgba values
        let [r, g, b, a] = [data[i], data[i + 1], data[i + 2], data[i + 3]];
        if (a > 0) {
            xMin = currentX < xMin ? currentX : xMin;
            xMax = currentX > xMax ? currentX : xMax;
            yMin = currentY < yMin ? currentY : yMin;
            yMax = currentY > yMax ? currentY : yMax;
        }

        // is new "pixel row" – increment y value, reset x to "0"
        if (currentX + 1 >= w) {
            currentX = 0;
            currentY++
        }
        else {
            currentX++;
        }
    }
© www.soinside.com 2019 - 2024. All rights reserved.