Barry
Barry

Reputation: 33

How can I get the actual bounding borders of a SVG after rotation?

I need to get the position of the actual bounding borders of a SVG image after its parent being applied CSS rotation. The grey color in the image below indicates its parent element(In my case it is transparent - I added the grey color in the image below just for indicating the parent element's border)

getBoundingClientRect() or getBBox() seems to only show the bounding borders of the original rectangle - the red borders shown below. But what I want is the bounding borders without SVG's transparent background after rotation - like the green borders.

screenshot


$(function() {

  let svgString = '<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 425.58 635.88"><defs><style>.cls-1{fill:#000000;fill-rule:evenodd;}</style></defs><title>1</title><path id="path" class="cls-1" d="M918.94,831.44c-103,110.83-133.9,110.83-236.86,0-68.44-73.68-80.26-150.84-84.92-241.2-4.22-81.95-26.86-194.68,18.05-248.36,70.51-84.25,300.09-84.25,370.59,0,44.92,53.68,22.28,166.41,18.06,248.36C999.2,680.6,987.39,757.77,918.94,831.44Z" transform="translate(-587.72 -278.69)"/></svg>'

  let svgElement = new DOMParser().parseFromString(svgString, 'text/xml').documentElement

  $('#box').html(svgElement)

  $('#box').css({
    transform: 'rotate(60deg)'
  })

  $('#rect').click(function() {

    let rect = svgElement.getBoundingClientRect()

    $('#bounding-border').css({
      left: rect.left + 'px',
      top: rect.top + 'px',
      width: rect.width + 'px',
      height: rect.height + 'px',
    })
  })

  $('#bbox').click(function() {

    let bbox = svgElement.getBBox()

    $('#bounding-border').css({
      left: bbox.left + 'px',
      top: bbox.top + 'px',
      width: bbox.width + 'px',
      height: bbox.height + 'px',
    })
  })
})
#box {
  position: absolute;
  left: 100px;
  top: 100px;
  width: 100px;
  height: 150px;
}

svg {
  path:hover {
    fill: red;
  }
}

#bounding-border {
  position: absolute;
  border: 2px solid red;
  z-index: -1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>

<button id="rect">getBoundingClientRect</button>
<button id="bbox">getBBox</button>
<div id="box"></div>
<div id="bounding-border"></div>

codepen

Upvotes: 3

Views: 82

Answers (1)

herrstrietzel
herrstrietzel

Reputation: 17316

To this date you can't tweak native methods to get a tight bounding box as expected by the visual boundaries after transformations.

The best you can do is

  • rotate SVG child elements instead of the parent SVG
  • convert the transformed Geometry elements to paths with "hard-coded" pathData coordinates

$(function() {
  let svgString =
    `<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 425.58 635.88"><defs><style>.cls-1{fill:#000000;fill-rule:evenodd;}</style></defs><title>1</title><path id="path" class="cls-1" d="M918.94,831.44c-103,110.83-133.9,110.83-236.86,0-68.44-73.68-80.26-150.84-84.92-241.2-4.22-81.95-26.86-194.68,18.05-248.36,70.51-84.25,300.09-84.25,370.59,0,44.92,53.68,22.28,166.41,18.06,248.36C999.2,680.6,987.39,757.77,918.94,831.44Z" 
    transform-origin="center" 
  transform="rotate(60) translate(-587.72 -278.69)"/></svg>`;

  let svgElement = new DOMParser().parseFromString(svgString, "text/xml")
    .documentElement;
  let path = svgElement.getElementById("path");

  // append svg
  $("#box").html(svgElement);


  /**
   * "flatten" transformations
   * for hardcoded path data cordinates
   */
  flattenSVGTransformations(svgElement);


  // get bounding box of flattened svg
  let bb = svgElement.getBBox();


  // adjust viewBox according to bounding box
  svgElement.setAttribute('viewBox', [bb.x, bb.y, bb.width, bb.height].join())

  // get DOM bounding box
  let rect = svgElement.getBoundingClientRect();

  // adjust absolutely positioned wrapper
  $("#bounding-border").css({
    left: rect.x + "px",
    top: rect.y + "px",
    width: rect.width + "px",
    height: rect.height + "px"
  });

});


/**
 * flattening/detransform helpers
 */

function flattenSVGTransformations(svg) {
  let els = svg.querySelectorAll('text, path, polyline, polygon, line, rect, circle, ellipse');
  els.forEach(el => {
    // convert primitives to paths
    if (el instanceof SVGGeometryElement && el.nodeName !== 'path') {
      let pathData = el.getPathData({
        normalize: true
      });
      let pathNew = document.createElementNS('http://www.w3.org/2000/svg', 'path');
      pathNew.setPathData(pathData);
      copyAttributes(el, pathNew);
      el.replaceWith(pathNew)
      el = pathNew;
    }
    reduceElementTransforms(el);
  });
  // remove group transforms
  let groups = svg.querySelectorAll('g');
  groups.forEach(g => {
    g.removeAttribute('transform');
    g.removeAttribute('transform-origin');
    g.style.removeProperty('transform');
    g.style.removeProperty('transform-origin');
  });
}

function reduceElementTransforms(el, decimals = 3) {
  let parent = el.farthestViewportElement;
  // check elements transformations
  let matrix = parent.getScreenCTM().inverse().multiply(el.getScreenCTM());
  let {
    a,
    b,
    c,
    d,
    e,
    f
  } = matrix;
  // round matrix
  [a, b, c, d, e, f] = [a, b, c, d, e, f].map(val => {
    return +val.toFixed(3)
  });
  let matrixStr = [a, b, c, d, e, f].join('');
  let isTransformed = matrixStr !== "100100" ? true : false;
  if (isTransformed) {


    // if text element: consolidate all applied transforms 
    if (el instanceof SVGGeometryElement === false) {
      if (isTransformed) {
        el.setAttribute('transform', transObj.svgTransform);
        el.removeAttribute('transform-origin');
        el.style.removeProperty('transform');
        el.style.removeProperty('transform-origin');
      }
      return false
    }
    /**
     * is geometry elements: 
     * recalculate pathdata
     * according to transforms
     * by matrix transform
     */
    let pathData = el.getPathData({
      normalize: true
    });
    let svg = el.closest("svg");
    pathData.forEach((com, i) => {
      let values = com.values;
      for (let v = 0; v < values.length - 1; v += 2) {
        let [x, y] = [values[v], values[v + 1]];
        let pt = new DOMPoint(x, y);
        let pTrans = pt.matrixTransform(matrix);
        // update coordinates in pathdata array
        pathData[i]["values"][v] = +(pTrans.x).toFixed(decimals);
        pathData[i]["values"][v + 1] = +(pTrans.y).toFixed(decimals);
      }
    });
    // apply pathdata - remove transform
    el.setPathData(pathData);
    el.removeAttribute('transform');
    el.style.removeProperty('transform');
    return pathData;
  }
}


/**
 * get element transforms
 */
function getElementTransform(el, parent, precision = 6) {
  let matrix = parent.getScreenCTM().inverse().multiply(el.getScreenCTM());
  let matrixVals = [matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f].map(val => {
    return +val.toFixed(precision)
  });
  return matrixVals;
}


/**
 * copy attributes:
 * used for primitive to path conversions
 */
function copyAttributes(el, newEl) {
  let atts = [...el.attributes];
  let excludedAtts = ['d', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx',
    'ry', 'points', 'height', 'width'
  ];
  for (let a = 0; a < atts.length; a++) {
    let att = atts[a];
    if (excludedAtts.indexOf(att.nodeName) === -1) {
      let attrName = att.nodeName;
      let attrValue = att.nodeValue;
      newEl.setAttribute(attrName, attrValue + '');
    }
  }
}
* {
  box-sizing: border-box;
}

svg {
  display: block;
  outline: 1px solid #ccc;
  overflow: visible;
}

#box {
  position: absolute;
  width: 75%;
  outline: 1px solid #ccc;
}


#bounding-border {
  position: absolute;
  border: 2px solid rgba(255, 0, 0, 0.5);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>

<!-- path data parser -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/path-data-polyfill.min.js"></script>


<div id="box"></div>
<div id="bounding-border"></div>

Obviously, the detransformation/flattening process will change the current transformation.
We also need a path data parser to get calculable absolute command coordinates. I'm using Jarek Foksa's getpathData() polyfill (which may soon become obsolete at least for Firefox =).

Worth noting, we need to adjust the svg's viewBox after flattening – otherwise we won't get the correct layout offsets via getBoundingClientRect() as this method respects the SVG's viewBox and thus returns a 'clipped' bounding box.

See also

Upvotes: 2

Related Questions