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>