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.
$(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>
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
$(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/path-data-polyfill@1.0.4/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