我想要这个页面的功能
http://codepen.io/netsi1964/full/vNoemp/
我有路径,需要知道它的边界框作为矩形元素,它是 x,y,宽度和高度 给定代码
<path d="M147.5 55.8c-5.8-7.2-13.6-14.4-25.5-14.4-8.4 0-15.4 8.2-27 8.2-9 0-13-7.8-23-7.8C51.4 41.8 31 60.4 31 84.5c0 12.8 4.2 32.5 13.6 49.7C51 146.7 59.4 155 69 155c6.7 0 14.7-6.3 24.2-6.3 8.4 0 16.2 5.6 23.8 5.6 18 0 35-23.5 35-39.3 0-.8-.3-1.4-.3-2v-1c-11.8-6.3-18.2-15.7-18.2-29.3 0-11 4.8-20.5 13.6-26.7l.5-.2zm-53-8.8c13.7-4.2 26.3-14.4 26.3-32 0-1.5-.2-3.3-.4-5.3l-.2-.8C106.4 12.6 94 23.4 94 40.3c0 1.6.2 3.6.6 5.8v.8z" style="translate(0px,-212.47488403320312px) scale(1,1)" >
并了解 rect 属性
使用纯 JavaScript:为您的路径提供一个 ID,并使用
getBBox()
获取其边界框。
var myPathBox = document.getElementById("myPath").getBBox();
console.log(myPathBox);
这是一个演示:
var myPathBox = document.getElementById("myPath").getBBox();
console.log(myPathBox);
<svg width="400" height="400">
<path id="myPath" d="M147.5 55.8c-5.8-7.2-13.6-14.4-25.5-14.4-8.4 0-15.4 8.2-27 8.2-9 0-13-7.8-23-7.8C51.4 41.8 31 60.4 31 84.5c0 12.8 4.2 32.5 13.6 49.7C51 146.7 59.4 155 69 155c6.7 0 14.7-6.3 24.2-6.3 8.4 0 16.2 5.6 23.8 5.6 18 0 35-23.5 35-39.3 0-.8-.3-1.4-.3-2v-1c-11.8-6.3-18.2-15.7-18.2-29.3 0-11 4.8-20.5 13.6-26.7l.5-.2zm-53-8.8c13.7-4.2 26.3-14.4 26.3-32 0-1.5-.2-3.3-.4-5.3l-.2-.8C106.4 12.6 94 23.4 94 40.3c0 1.6.2 3.6.6 5.8v.8z" style="translate(0px,-212.47488403320312px) scale(1,1)" >
</svg>
getBBox()
要求将您的元素附加到 DOM。
如果您无法使用本机支持的
getBBox()
方法,例如因为您在虚拟 DOM 环境中工作,您还可以根据原始路径数据d
字符串计算边界框。
d
路径数据属性字符串转换为可计算命令值的数组。我正在使用基于 SVGPathData 接口方法的 w3c 工作草案的独立解析器,但您也可以使用 Jarek Foksa 的 getPathData polyfill。h
、 v
、s
、t
需要标准化为其对应的普通命令)t
值 – 然后计算这些 t
值处的实际点坐标。参见崔西平的回答“计算三次贝塞尔曲线的边界框”A
弧线命令的 x/y 极值点 - 需要对弧线命令值进行参数化(rx、ry、角度、largearc、sweep、finalX、finalY) - 基于这篇文章 “如何计算轴-椭圆的对齐边界框?”// wrapper function to calculate boundaries for aeach segment type
function getPathBBox(pathData) {
let M = {
x: pathData[0].values[0],
y: pathData[0].values[1]
}
let xArr = [M.x];
let yArr = [M.y];
for (let i = 1; i < pathData.length; i++) {
let com = pathData[i]
let {
type,
values
} = com;
let valuesL = values.length;
let comPrev = pathData[i - 1]
let valuesPrev = comPrev.values;
let valuesPrevL = valuesPrev.length;
if (valuesL) {
let p0 = {
x: valuesPrev[valuesPrevL - 2],
y: valuesPrev[valuesPrevL - 1]
};
let p = {
x: values[valuesL - 2],
y: values[valuesL - 1]
};
// add final on path point
xArr.push(p.x)
yArr.push(p.y)
if (type === 'C' || type === 'Q') {
let cp1 = {
x: values[0],
y: values[1]
};
let cp2 = type === 'C' ? {
x: values[2],
y: values[3]
} : cp1;
let pts = type === 'C' ? [p0, cp1, cp2, p] : [p0, cp1, p];
let bezierExtremesT = getBezierExtremeT(pts)
bezierExtremesT.forEach(t => {
let pt = getPointAtBezierT(pts, t);
xArr.push(pt.x);
yArr.push(pt.y);
})
} else if (type === 'A') {
let arcExtremes = getArcExtemes(p0, values)
arcExtremes.forEach(pt => {
xArr.push(pt.x);
yArr.push(pt.y);
})
}
}
}
let xMin = Math.min(...xArr)
let xMax = Math.max(...xArr)
let yMin = Math.min(...yArr)
let yMax = Math.max(...yArr)
let bbox = {
x: xMin,
y: yMin,
width: xMax - xMin,
height: yMax - yMin
}
return bbox
}
// wrapper functions for quadratic or cubic bezier point calculation
function getPointAtBezierT(pts, t) {
let pt = pts.length === 4 ? getPointAtCubicSegmentT(pts[0], pts[1], pts[2], pts[3], t) : getPointAtQuadraticSegmentT(pts[0], pts[1], pts[2], t)
return pt
}
function getBezierExtremeT(pts) {
let tArr = pts.length === 4 ? cubicBezierExtremeT(pts[0], pts[1], pts[2], pts[3]) : quadraticBezierExtremeT(pts[0], pts[1], pts[2]);
return tArr;
}
/**
* based on Nikos M.'s answer
* how-do-you-calculate-the-axis-aligned-bounding-box-of-an-ellipse
* https://stackoverflow.com/questions/87734/#75031511
*/
function getArcExtemes(p0, values) {
// compute point on ellipse from angle around ellipse (theta)
const arc = (theta, cx, cy, rx, ry, alpha) => {
// theta is angle in radians around arc
// alpha is angle of rotation of ellipse in radians
var cos = Math.cos(alpha),
sin = Math.sin(alpha),
x = rx * Math.cos(theta),
y = ry * Math.sin(theta);
return {
x: cx + cos * x - sin * y,
y: cy + sin * x + cos * y
};
}
//parametrize arcto data
let arcData = svgArcToCenterParam(p0.x, p0.y, values[0], values[1], values[2], values[3], values[4], values[5], values[6]);
let {
rx,
ry,
pt,
endAngle,
deltaAngle
} = arcData;
// arc rotation
let deg = values[2];
// final on path point
let p = {
x: values[5],
y: values[6]
}
// circle/elipse center coordinates
let [cx, cy] = [pt.x, pt.y];
// collect extreme points – add end point
let extremes = [p]
// rotation to radians
let alpha = deg * Math.PI / 180;
let tan = Math.tan(alpha),
p1, p2, p3, p4, theta;
// find min/max from zeroes of directional derivative along x and y
// along x axis
theta = Math.atan2(-ry * tan, rx);
let angle1 = theta;
let angle2 = theta + Math.PI;
let angle3 = Math.atan2(ry, rx * tan);
let angle4 = angle3 + Math.PI;
// get point for this theta
p1 = arc(angle1, cx, cy, rx, ry, alpha);
// get anti-symmetric point
p2 = arc(angle2, cx, cy, rx, ry, alpha);
// get point for this theta
p3 = arc(angle3, cx, cy, rx, ry, alpha);
// get anti-symmetric point
p4 = arc(angle4, cx, cy, rx, ry, alpha);
// inner bounding box
let xArr = [p0.x, p.x]
let yArr = [p0.y, p.y]
let xMin = Math.min(...xArr)
let xMax = Math.max(...xArr)
let yMin = Math.min(...yArr)
let yMax = Math.max(...yArr)
// on path point close after start
let angleAfterStart = endAngle - deltaAngle * 0.001
let pP2 = arc(angleAfterStart, cx, cy, rx, ry, alpha);
// on path point close before end
let angleBeforeEnd = endAngle - deltaAngle * 0.999
let pP3 = arc(angleBeforeEnd, cx, cy, rx, ry, alpha);
/**
* expected extremes
* if leaving inner bounding box
* (between segment start and end point)
* otherwise exclude elliptic extreme points
*/
// left
if (pP2.x < xMin || pP3.x < xMin) {
extremes.push(p2)
}
// top
if (pP2.y < yMin || pP3.y < yMin) {
extremes.push(p4)
}
// right
if (pP2.x > xMax || pP3.x > xMax) {
extremes.push(p1)
}
// bottom
if (pP2.y > yMax || pP3.y > yMax) {
extremes.push(p3)
}
return extremes;
}
// wrapper functions for quadratic or cubic bezier point calculation
function getPointAtBezierT(pts, t) {
let pt = pts.length === 4 ? getPointAtCubicSegmentT(pts[0], pts[1], pts[2], pts[3], t) : getPointAtQuadraticSegmentT(pts[0], pts[1], pts[2], t)
return pt
}
function getBezierExtremeT(pts) {
let tArr = pts.length === 4 ? cubicBezierExtremeT(pts[0], pts[1], pts[2], pts[3]) : quadraticBezierExtremeT(pts[0], pts[1], pts[2]);
return tArr;
}
// cubic bezier
function cubicBezierExtremeT(p0, cp1, cp2, p) {
let [x0, y0, x1, y1, x2, y2, x3, y3] = [p0.x, p0.y, cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
let extemeT = [],
a, b, c, t, t1, t2, b2ac, sqrt_b2ac;
for (let i = 0; i < 2; ++i) {
if (i == 0) {
b = 6 * x0 - 12 * x1 + 6 * x2;
a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3;
c = 3 * x1 - 3 * x0;
} else {
b = 6 * y0 - 12 * y1 + 6 * y2;
a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3;
c = 3 * y1 - 3 * y0;
}
if (Math.abs(a) < 1e-12) {
if (Math.abs(b) < 1e-12) {
continue;
}
t = -c / b;
if (0 < t && t < 1) {
extemeT.push(t);
}
continue;
}
b2ac = b * b - 4 * c * a;
if (b2ac < 0) {
if (Math.abs(b2ac) < 1e-12) {
t = -b / (2 * a);
if (0 < t && t < 1) {
extemeT.push(t);
}
}
continue;
}
sqrt_b2ac = Math.sqrt(b2ac);
t1 = (-b + sqrt_b2ac) / (2 * a);
if (0 < t1 && t1 < 1) {
extemeT.push(t1);
}
t2 = (-b - sqrt_b2ac) / (2 * a);
if (0 < t2 && t2 < 1) {
extemeT.push(t2);
}
}
var j = extemeT.length;
while (j--) {
t = extemeT[j];
}
return extemeT;
}
// quadratic bezier.
function quadraticBezierExtremeT(p0, cp1, p) {
let [x0, y0, x1, y1, x2, y2] = [p0.x, p0.y, cp1.x, cp1.y, p.x, p.y];
let extemeT = [];
for (var i = 0; i < 2; ++i) {
a = i == 0 ? x0 - 2 * x1 + x2 : y0 - 2 * y1 + y2;
b = i == 0 ? -2 * x0 + 2 * x1 : -2 * y0 + 2 * y1;
c = i == 0 ? x0 : y0;
if (Math.abs(a) > 1e-12) {
t = -b / (2 * a);
if (t > 0 && t < 1) {
extemeT.push(t);
}
}
}
return extemeT
}
/**
* parse pathData from d attribute
* the core function to parse the pathData array from a d string
**/
function parseDtoPathData(d) {
let dClean = d
// remove new lines and tabs
.replace(/[\n\r\t]/g, "")
// replace comma with space
.replace(/,/g, " ")
// add space before minus sign
.replace(/(\d+)(\-)/g, "$1 $2")
// decompose multiple adjacent decimal delimiters like 0.5.5.5 => 0.5 0.5 0.5
.replace(/(\.)(?=(\d+\.\d+)+)(\d+)/g, "$1$3 ")
// add new lines before valid command letters
.replace(/([mlcsqtahvz])/gi, "\n$1 ")
// remove duplicate whitespace
.replace(/\ {2,}/g, " ")
// remove whitespace from right and left
.trim();
// split commands
let commands = dClean.split("\n").map((val) => {
return val.trim();
});
// compile pathData
let pathData = [];
let comLengths = {
m: 2,
a: 7,
c: 6,
h: 1,
l: 2,
q: 4,
s: 4,
t: 2,
v: 1,
z: 0
};
let errors = [];
// normalize convatenated larceArc and sweep flags
const unravelArcValues = (values) => {
let chunksize = 7,
n = 0,
arcComs = []
for (let i = 0; i < values.length; i++) {
let com = values[i]
// reset counter
if (n >= chunksize) {
n = 0
}
// if 3. or 4. parameter longer than 1
if ((n === 3 || n === 4) && com.length > 1) {
let largeArc = n === 3 ? com.substring(0, 1) : ''
let sweep = n === 3 ? com.substring(1, 2) : com.substring(0, 1)
let finalX = n === 3 ? com.substring(2) : com.substring(1)
let comN = [largeArc, sweep, finalX].filter(Boolean)
arcComs.push(comN)
n += comN.length
} else {
// regular
arcComs.push(com)
n++
}
}
return arcComs.flat().filter(Boolean);
}
for (let i = 0; i < commands.length; i++) {
let com = commands[i].split(" ");
let type = com.shift();
let typeRel = type.toLowerCase();
let isRel = type === typeRel;
/**
* large arc and sweep flags
* are boolean and can be concatenated like
* 11 or 01
* or be concatenated with the final on path points like
* 1110 10 => 1 1 10 10
*/
if (typeRel === "a") {
com = unravelArcValues(com)
}
// convert to numbers
let values = com.map((val) => {
return parseFloat(val);
});
// if string contains repeated shorthand commands - split them
let chunkSize = comLengths[typeRel];
let chunk = values.slice(0, chunkSize);
pathData.push({
type: type,
values: chunk
});
// too few values
if (chunk.length < chunkSize) {
errors.push(
`${i}. command (${type}) has ${chunk.length}/${chunkSize} values - ${chunkSize - chunk.length} too few`
);
}
// has implicit commands
if (values.length > chunkSize) {
let typeImplicit = typeRel === "m" ? (isRel ? "l" : "L") : type;
for (let i = chunkSize; i < values.length; i += chunkSize) {
let chunk = values.slice(i, i + chunkSize);
pathData.push({
type: typeImplicit,
values: chunk
});
if (chunk.length !== chunkSize) {
errors.push(
`${i}. command (${type}) has ${chunk.length + chunkSize}/${chunkSize} - ${chunk.length} values too many `
);
}
}
}
}
if (errors.length) {
console.log(errors);
}
/**
* first M is always absolute/uppercase -
* unless it adds relative linetos
* (facilitates d concatenating)
*/
pathData[0].type = 'M'
return pathData;
}
/**
* converts all commands to absolute
*/
function pathDataToLonghands(pathData) {
let pathDataAbs = [];
let offX = pathData[0].values[0];
let offY = pathData[0].values[1];
let lastX = offX;
let lastY = offY;
// analyze pathdata
let commandTokens = pathData.map(com => {
return com.type
}).join('')
let hasShorthands = /[hstv]/gi.test(commandTokens);
pathData.forEach((com, i) => {
let {
type,
values
} = com;
let typeRel = type.toLowerCase();
let typeAbs = type.toUpperCase();
let valuesL = values.length;
let isRelative = type === typeRel;
let comPrev = i > 0 ? pathData[i - 1] : pathData[0];
let valuesPrev = comPrev.values;
let valuesPrevL = valuesPrev.length;
if (isRelative) {
com.type = typeAbs;
switch (typeRel) {
case "a":
com.values = [
values[0],
values[1],
values[2],
values[3],
values[4],
values[5] + offX,
values[6] + offY
];
break;
case "h":
case "v":
com.values = type === 'h' ? [values[0] + offX] : [values[0] + offY];
break;
case 'm':
case 'l':
case 't':
com.values = [values[0] + offX, values[1] + offY]
break;
case "c":
com.values = [
values[0] + offX,
values[1] + offY,
values[2] + offX,
values[3] + offY,
values[4] + offX,
values[5] + offY
];
break;
case "q":
case "s":
com.values = [
values[0] + offX,
values[1] + offY,
values[2] + offX,
values[3] + offY,
];
break;
}
}
// is absolute
else {
offX = 0;
offY = 0;
}
/**
* convert shorthands
*/
if (hasShorthands) {
let cp1X, cp1Y, cpN1X, cpN1Y, cp2X, cp2Y;
if (com.type === 'H' || com.type === 'V') {
com.values = com.type === 'H' ? [com.values[0], lastY] : [lastX, com.values[0]];
com.type = 'L';
} else if (com.type === 'T' || com.type === 'S') {
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
[cp2X, cp2Y] = valuesPrevL > 2 ? [valuesPrev[2], valuesPrev[3]] : [valuesPrev[0], valuesPrev[1]];
// new control point
cpN1X = com.type === 'T' ? lastX + (lastX - cp1X) : 2 * lastX - cp2X;
cpN1Y = com.type === 'T' ? lastY + (lastY - cp1Y) : 2 * lastY - cp2Y;
com.values = [cpN1X, cpN1Y, com.values].flat();
com.type = com.type === 'T' ? 'Q' : 'C';
}
}
// add command
pathDataAbs.push(com)
// update offsets
lastX = valuesL > 1 ? values[valuesL - 2] + offX : (typeRel === 'h' ? values[0] + offX : lastX);
lastY = valuesL > 1 ? values[valuesL - 1] + offY : (typeRel === 'v' ? values[0] + offY : lastY);
offX = lastX;
offY = lastY;
});
return pathDataAbs;
}
/**
* Linear interpolation (LERP) helper
*/
function interpolatedPoint(p1, p2, t = 0.5) {
return {
x: (p2.x - p1.x) * t + p1.x,
y: (p2.y - p1.y) * t + p1.y
};
}
/**
* calculate single points on segments
*/
function getPointAtCubicSegmentT(p0, cp1, cp2, p, t = 0.5) {
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 getPointAtQuadraticSegmentT(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
};
}
/**
* based on @cuixiping;
* https://stackoverflow.com/questions/9017100/calculate-center-of-svg-arc/12329083#12329083
*/
function svgArcToCenterParam(x1, y1, rx, ry, degree, fA, fS, x2, y2) {
const radian = (ux, uy, vx, vy) => {
let dot = ux * vx + uy * vy;
let mod = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
let rad = Math.acos(dot / mod);
if (ux * vy - uy * vx < 0) {
rad = -rad;
}
return rad;
};
// degree to radian
let phi = (degree * Math.PI) / 180;
let cx, cy, startAngle, deltaAngle, endAngle;
let PI = Math.PI;
let PIx2 = PI * 2;
if (rx < 0) {
rx = -rx;
}
if (ry < 0) {
ry = -ry;
}
if (rx == 0 || ry == 0) {
// invalid arguments
throw Error("rx and ry can not be 0");
}
let s_phi = Math.sin(phi);
let c_phi = Math.cos(phi);
let hd_x = (x1 - x2) / 2; // half diff of x
let hd_y = (y1 - y2) / 2; // half diff of y
let hs_x = (x1 + x2) / 2; // half sum of x
let hs_y = (y1 + y2) / 2; // half sum of y
// F6.5.1
let x1_ = c_phi * hd_x + s_phi * hd_y;
let y1_ = c_phi * hd_y - s_phi * hd_x;
// F.6.6 Correction of out-of-range radii
// Step 3: Ensure radii are large enough
let lambda = (x1_ * x1_) / (rx * rx) + (y1_ * y1_) / (ry * ry);
if (lambda > 1) {
rx = rx * Math.sqrt(lambda);
ry = ry * Math.sqrt(lambda);
}
let rxry = rx * ry;
let rxy1_ = rx * y1_;
let ryx1_ = ry * x1_;
let sum_of_sq = rxy1_ * rxy1_ + ryx1_ * ryx1_; // sum of square
if (!sum_of_sq) {
throw Error("start point can not be same as end point");
}
let coe = Math.sqrt(Math.abs((rxry * rxry - sum_of_sq) / sum_of_sq));
if (fA == fS) {
coe = -coe;
}
// F6.5.2
let cx_ = (coe * rxy1_) / ry;
let cy_ = (-coe * ryx1_) / rx;
// F6.5.3
cx = c_phi * cx_ - s_phi * cy_ + hs_x;
cy = s_phi * cx_ + c_phi * cy_ + hs_y;
let xcr1 = (x1_ - cx_) / rx;
let xcr2 = (x1_ + cx_) / rx;
let ycr1 = (y1_ - cy_) / ry;
let ycr2 = (y1_ + cy_) / ry;
// F6.5.5
startAngle = radian(1.0, 0, xcr1, ycr1);
// F6.5.6
deltaAngle = radian(xcr1, ycr1, -xcr2, -ycr2);
while (deltaAngle > PIx2) {
deltaAngle -= PIx2;
}
while (deltaAngle < 0) {
deltaAngle += PIx2;
}
if (fS == false || fS == 0) {
deltaAngle -= PIx2;
}
endAngle = startAngle + deltaAngle;
while (endAngle > PIx2) {
endAngle -= PIx2;
}
while (endAngle < 0) {
endAngle += PIx2;
}
let toDegFactor = 180 / PI;
let outputObj = {
pt: {
x: cx,
y: cy
},
rx: rx,
ry: ry,
startAngle_deg: startAngle * toDegFactor,
startAngle: startAngle,
deltaAngle_deg: deltaAngle * toDegFactor,
deltaAngle: deltaAngle,
endAngle_deg: endAngle * toDegFactor,
endAngle: endAngle,
clockwise: fS == true || fS == 1
};
return outputObj;
}
<svg id="svg" width="400" height="400">
<path id="path" d="M147.5 55.8c-5.8-7.2-13.6-14.4-25.5-14.4-8.4 0-15.4 8.2-27 8.2-9 0-13-7.8-23-7.8C51.4 41.8 31 60.4 31 84.5c0 12.8 4.2 32.5 13.6 49.7C51 146.7 59.4 155 69 155c6.7 0 14.7-6.3 24.2-6.3 8.4 0 16.2 5.6 23.8 5.6 18 0 35-23.5 35-39.3 0-.8-.3-1.4-.3-2v-1c-11.8-6.3-18.2-15.7-18.2-29.3 0-11 4.8-20.5 13.6-26.7l.5-.2zm-53-8.8c13.7-4.2 26.3-14.4 26.3-32 0-1.5-.2-3.3-.4-5.3l-.2-.8C106.4 12.6 94 23.4 94 40.3c0 1.6.2 3.6.6 5.8v.8z">
</svg>
<script>
window.addEventListener('DOMContentLoaded', e => {
let d = path.getAttribute('d')
let pathData = parseDtoPathData(d)
// normalize to all absolute and longhands
pathData = pathDataToLonghands(pathData)
// calculate bounding box
let bbPath = getPathBBox(pathData);
// render bounding box
let {
x,
y,
width,
height
} = bbPath;
let rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
rect.setAttribute('x', x)
rect.setAttribute('y', y)
rect.setAttribute('width', width)
rect.setAttribute('height', height)
rect.setAttribute('fill', 'none')
rect.setAttribute('stroke', 'red')
svg.append(rect)
})
</script>
我知道这是一个老问题,但我想我会把这个变体放到 Furtado 的答案中以供参考。
获取路径边界框的简单方法。
边界框信息将显示在控制台中。
同样的方法,只是不需要自定义代码。