代码之家  ›  专栏  ›  技术社区  ›  indieman

从javascript中的字符串确定svg元素的边界框

  •  0
  • indieman  · 技术社区  · 6 年前

    如果我在chrome中加载SVG,我可以得到它的元素的边界框,如下所示: document.getElementById('char1').getBBox() -> SVGRect {x: 348.5628356933594, y: 78.95916748046875, width: 202.74807739257812, height: 845.1696166992188}

    但是,如果我使用DOMParser加载相同的SVG,我会得到一个值为0的边界框。

    如:

    const svgString = document.documentElement.outerHTML // in practice this would be an svg fetched from elsewhere
    let svgElement = new DOMParser().parseFromString(svgString, 'text/xml').documentElement
    svgElement.getElementById('char1').getBBox()
    

    ->

    SVGRect {x: 0, y: 0, width: 0, height: 0}
    

    a) 为什么会发生这种情况? b) 如何使用svg字符串而不将其加载到文档中来获得正确的边界框值?

    0 回复  |  直到 6 年前
        1
  •  0
  •   Keavon    3 年前

    它并不像它应该的那么漂亮,但解决方案是暂时添加、测量,然后从DOM中删除它。因为在运行这三行JS时执行永远不会返回,所以元素实际上不会出现在页面上。因此,这很可能没有副作用(除非,也许,你正在用 mutation observers 或类似)。

    const toRemove = document.body.insertAdjacentElement("beforeend", svgElement);
    const bounds = svgElement.getBBox();
    toRemove?.remove();
    
        2
  •  0
  •   herrstrietzel    3 年前

    正如@Robert Longson所评论的: getBBox() 需要将元素附加到DOM。临时附加svg(并最终删除它)通常是最方便的方法。

    如果你做不到。。。

    根据pathData计算近似边界框

    我们可以通过计算pathData数组中的点来获得边界框近似值

    1. 通过(polyfilled)解析路径数据 getPathData() 方法。我们还使用 normalize:true 参数,用于将所有命令转换为绝对坐标和坐标 A (弧线)至 c (立方比泽尔)

    2. 循环命令数组,通过自定义获得线段端点以及曲线上的插值点 pathDataToPolygonPoints() 帮手
      Exteme points
      我们需要计算 曲线上的插值点 分段以获得x/y极值。
      计算的插值点越多,精度就越高。

    3. 遍历点数组以查找 最小/最大x和y坐标

    let svgMarkup = `<svg  xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" >
        <path fill="#474bff" d="M 400 297 Q 396 362 353 402.5 Q 309 451 238.5 457 Q 176 570 137 400.5 Q 106 338 69.5 289 Q 7 236 62 186 Q 91 132 135.5 93 Q 124 12 239.5 56.5 Q 342 18 366 79.5 Q 433 100 418 170 Q 403 240 400 297 Z" />
    </svg>`;
    
    let svg = new DOMParser().parseFromString(svgMarkup, 'image/svg+xml').querySelector('svg');
    let path = svg.querySelector('path');
    let pathData = path.getPathData({
      normalize: true
    });
    // init
    upDataBBox();
    
    inputAccuracy.addEventListener('input', e => {
      upDataBBox()
    })
    
    function upDataBBox() {
    
      /**
       * calculate points from pathData 
       * interpolate additional points for curve segements 
       * to increase accuracy
       */
      let accuracy = +inputAccuracy.value;
      let polypoints = pathDataToPolygonPoints(pathData, true, accuracy);
      // approximated bbox
      let polyBBox = getPolygonBBox(polypoints);
      // compare with getBBox()
      let bb = pathInline.getBBox();
      let bbox = {
        x: bb.x,
        y: bb.y,
        width: bb.width,
        height: bb.height
      };
    
      bboxOrig.textContent = beautify(JSON.stringify(bbox));
      bboxPoly.textContent = beautify(JSON.stringify(polyBBox));
    
      points.innerHTML = '';
      polypoints.forEach(point => {
        renderPoint(points, point)
      })
    
    }
    
    
    function beautify(str) {
      str = str.replaceAll('{', '').replaceAll('}', '').split(',').join('\n')
      return str
    }
    
    
    function getPolygonBBox(polyPoints) {
      let xArr = [];
      let yArr = [];
      polyPoints.forEach(point => {
        xArr.push(point.x);
        yArr.push(point.y);
      })
      let xmin = Math.min(...xArr);
      let xmax = Math.max(...xArr);
      let ymin = Math.min(...yArr);
      let ymax = Math.max(...yArr);
      return {
        x: xmin,
        y: ymin,
        width: xmax - xmin,
        height: ymax - ymin
      }
    }
    
    
    /**
     * convert path d to polygon point array
     */
    function pathDataToPolygonPoints(pathData, addControlPointsMid = false, splitNtimes = 0, splitLines = false) {
    
      let points = [];
      // close path fix
      pathData = addClosePathLineto(pathData);
      pathData.forEach((com, c) => {
        let type = com.type;
        let values = com.values;
        let valL = values.length;
    
        // optional splitting
        let splitStep = splitNtimes ? (0.5 / splitNtimes) : (addControlPointsMid ? 0.5 : 0);
        let split = splitStep;
    
        // M 
        if (c === 0) {
          let M = {
            x: pathData[0].values[valL - 2],
            y: pathData[0].values[valL - 1]
          };
          points.push(M);
        }
    
        if (valL && c > 0) {
          let prev = pathData[c - 1];
          let prevVal = prev.values;
          let prevValL = prevVal.length;
          let p0 = {
            x: prevVal[prevValL - 2],
            y: prevVal[prevValL - 1]
          };
    
          // cubic curves
          if (type === "C") {
            if (prevValL) {
              let cp1 = {
                x: values[valL - 6],
                y: values[valL - 5]
              };
              let cp2 = {
                x: values[valL - 4],
                y: values[valL - 3]
              };
              let p = {
                x: values[valL - 2],
                y: values[valL - 1]
              };
    
              if (addControlPointsMid && split) {
                // split cubic curves
                for (let s = 0; split < 1 && s < 9999; s++) {
                  let midPoint = getPointAtCubicSegmentLength(p0, cp1, cp2, p, split);
                  points.push(midPoint);
                  split += splitStep
                }
              }
              points.push({
                x: values[valL - 2],
                y: values[valL - 1]
              });
            }
          }
    
          // quadratic curves
          else if (type === "Q") {
            if (prevValL) {
              let cp1 = {
                x: values[valL - 4],
                y: values[valL - 3]
              };
              let p = {
                x: values[valL - 2],
                y: values[valL - 1]
              };
    
              //let coords = prevCoords.concat(values);
              if (addControlPointsMid && split) {
                // split cubic curves
                for (let s = 0; split < 1 && s < 9999; s++) {
                  let midPoint = getPointAtQuadraticSegmentLength(p0, cp1, p, split);
                  points.push(midPoint);
                  split += splitStep
                }
              }
              points.push({
                x: values[valL - 2],
                y: values[valL - 1]
              });
            }
          }
    
          // linetos
          else if (type === "L") {
            if (splitLines) {
              //let prevCoords = [prevVal[prevValL - 2], prevVal[prevValL - 1]];
              let p1 = {
                x: prevVal[prevValL - 2],
                y: prevVal[prevValL - 1]
              }
              let p2 = {
                x: values[valL - 2],
                y: values[valL - 1]
              }
    
              if (addControlPointsMid && split) {
                for (let s = 0; split < 1; s++) {
                  let midPoint = interpolatedPoint(p1, p2, split);
                  points.push(midPoint);
                  split += splitStep
                }
              }
            }
            points.push({
              x: values[valL - 2],
              y: values[valL - 1]
            });
          }
        }
      });
      return points;
    }
    
    
    /**
     * Linear  interpolation (LERP) helper
     */
    function interpolatedPoint(p1, p2, t = 0.5) {
      //t: 0.5 - point in the middle
      if (Array.isArray(p1)) {
        p1.x = p1[0];
        p1.y = p1[1];
      }
      if (Array.isArray(p2)) {
        p2.x = p2[0];
        p2.y = p2[1];
      }
      let [x, y] = [(p2.x - p1.x) * t + p1.x, (p2.y - p1.y) * t + p1.y];
      return {
        x: x,
        y: y
      };
    }
    
    
    /**
     * calculate single points on segments
     */
    
    function getPointAtCubicSegmentLength(p0, cp1, cp2, p, t) {
      let t1 = 1 - t;
      return {
        x: t1 ** 3 * p0.x + 3 * t1 ** 2 * t * cp1.x + 3 * t1 * t ** 2 * cp2.x + t ** 3 * p.x,
        y: t1 ** 3 * p0.y + 3 * t1 ** 2 * t * cp1.y + 3 * t1 * t ** 2 * cp2.y + t ** 3 * p.y
      }
    }
    
    function getPointAtQuadraticSegmentLength(p0, cp1, p, t = 0.5) {
      let t1 = 1 - t;
      return {
        x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t ** 2 * p.x,
        y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t ** 2 * p.y
      }
    }
    
    
    /**
     * Add closing lineto:
     * needed for path reversing or adding points
     */
    
    function addClosePathLineto(pathData) {
      let pathDataL = pathData.length;
      let closed = pathData[pathDataL - 1]["type"] == "Z" ? true : false;
    
      let M = pathData[0];
      let [x0, y0] = [M.values[0], M.values[1]];
      let lastCom = closed ? pathData[pathDataL - 2] : pathData[pathDataL - 1];
      let lastComL = lastCom.values.length;
      let [xE, yE] = [lastCom.values[lastComL - 2], lastCom.values[lastComL - 1]];
      if (closed && (x0 != xE || y0 != yE)) {
        //console.log('add final lineto')
        pathData.pop();
        pathData.push({
          type: "L",
          values: [x0, y0]
        }, {
          type: "Z",
          values: []
        });
      }
      return pathData;
    }
    
    
    
    /**
     * render point
     * accepts coordinate array and point object
     **/
    function renderPoint(svg, coords, fill = "red", r = "2") {
      if (Array.isArray(coords)) {
        coords = {
          x: coords[0],
          y: coords[1]
        };
      }
      let marker = `<circle cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
          <title>${coords.x} ${coords.y}</title></circle>`;
      svg.insertAdjacentHTML("beforeend", marker);
    }
    body {
      font-family: sans-serif
    }
    
    svg {
      overflow: visible;
      border: 1px solid #ccc;
      width: 20em;
    }
    
    p {
      white-space: pre-line;
    }
    <p>Accuracy: <input type="range" id="inputAccuracy" min="1" max="10" steps="1" value="1"></p>
    
    <p><strong>BBox exact: </strong>
      <span id="bboxOrig"></span></p>
    <p><strong>BBox poly:</strong>
      <span id="bboxPoly"></span></p>
    
    <svg id="svgInline" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="36.3 40.73 385.5 461.5">
        <path id="pathInline" fill="#474bff" d="M 400 297 Q 396 362 353 402.5 Q 309 451 238.5 457 Q 176 570 137 400.5 Q 106 338 69.5 289 Q 7 236 62 186 Q 91 132 135.5 93 Q 124 12 239.5 56.5 Q 342 18 366 79.5 Q 433 100 418 170 Q 403 240 400 297 Z" />
      <g id="points"></g>
    </svg>
    
    
    <script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.4/path-data-polyfill.min.js"></script>

    这种方法对所有人都很有效 几何元素 ,因为 getpathData() 还可以从基元中检索pathData数组:

    • <circle> , <ellipse> ,
    • <polygon> , <polyline>
    • <rect> , <line>

    不起作用 <text> <use> 元素。