假设有以下路径(例如),描述了SVG立方贝塞尔曲线:
M 300,140 C 300,40, 500,40, 500,140
并且假设连接端点300,140
和500,140
的直线(封闭曲线下的区域),是否可以计算所围面积?
有人能提供一个公式(或JavaScript)来完成这个任务吗?
假设有以下路径(例如),描述了SVG立方贝塞尔曲线:
M 300,140 C 300,40, 500,40, 500,140
并且假设连接端点300,140
和500,140
的直线(封闭曲线下的区域),是否可以计算所围面积?
有人能提供一个公式(或JavaScript)来完成这个任务吗?
将路径转换为任意精度的多边形, 然后 计算多边形的面积。
交互演示: 通过细分计算路径面积(已损坏)
// path: an SVG <path> element
// threshold: a 'close-enough' limit (ignore subdivisions with area less than this)
// segments: (optional) how many segments to subdivisions to create at each level
// returns: a new SVG <polygon> element
function pathToPolygonViaSubdivision(path,threshold,segments){
if (!threshold) threshold = 0.0001; // Get really, really close
if (!segments) segments = 3; // 2 segments creates 0-area triangles
var points = subdivide( ptWithLength(0), ptWithLength( path.getTotalLength() ) );
for (var i=points.length;i--;) points[i] = [points[i].x,points[i].y];
var doc = path.ownerDocument;
var poly = doc.createElementNS('http://www.w3.org/2000/svg','polygon');
poly.setAttribute('points',points.join(' '));
return poly;
// Record the distance along the path with the point for later reference
function ptWithLength(d) {
var pt = path.getPointAtLength(d); pt.d = d; return pt;
}
// Create segments evenly spaced between two points on the path.
// If the area of the result is less than the threshold return the endpoints.
// Otherwise, keep the intermediary points and subdivide each consecutive pair.
function subdivide(p1,p2){
var pts=[p1];
for (var i=1,step=(p2.d-p1.d)/segments;i<segments;i++){
pts[i] = ptWithLength(p1.d + step*i);
}
pts.push(p2);
if (polyArea(pts)<=threshold) return [p1,p2];
else {
var result = [];
for (var i=1;i<pts.length;++i){
var mids = subdivide(pts[i-1], pts[i]);
mids.pop(); // We'll get the last point as the start of the next pair
result = result.concat(mids)
}
result.push(p2);
return result;
}
}
// Calculate the area of an polygon represented by an array of points
function polyArea(points){
var p1,p2;
for(var area=0,len=points.length,i=0;i<len;++i){
p1 = points[i];
p2 = points[(i-1+len)%len]; // Previous point, with wraparound
area += (p2.x+p1.x) * (p2.y-p1.y);
}
return Math.abs(area/2);
}
}
// Return the area for an SVG <polygon> or <polyline>
// Self-crossing polys reduce the effective 'area'
function polyArea(poly){
var area=0,pts=poly.points,len=pts.numberOfItems;
for(var i=0;i<len;++i){
var p1 = pts.getItem(i), p2=pts.getItem((i+-1+len)%len);
area += (p2.x+p1.x) * (p2.y-p1.y);
}
return Math.abs(area/2);
}
<path>
转换为<polygon>
。
在其核心,上述演示使用函数来用多边形逼近路径和计算多边形的面积。
// Calculate the area of an SVG polygon/polyline
function polyArea(poly){
var area=0,pts=poly.points,len=pts.numberOfItems;
for(var i=0;i<len;++i){
var p1 = pts.getItem(i), p2=pts.getItem((i+len-1)%len);
area += (p2.x+p1.x) * (p2.y-p1.y);
}
return Math.abs(area/2);
}
// Create a <polygon> approximation for an SVG <path>
function pathToPolygon(path,samples){
if (!samples) samples = 0;
var doc = path.ownerDocument;
var poly = doc.createElementNS('http://www.w3.org/2000/svg','polygon');
// Put all path segments in a queue
for (var segs=[],s=path.pathSegList,i=s.numberOfItems-1;i>=0;--i)
segs[i] = s.getItem(i);
var segments = segs.concat();
var seg,lastSeg,points=[],x,y;
var addSegmentPoint = function(s){
if (s.pathSegType == SVGPathSeg.PATHSEG_CLOSEPATH){
}else{
if (s.pathSegType%2==1 && s.pathSegType>1){
x+=s.x; y+=s.y;
}else{
x=s.x; y=s.y;
}
var last = points[points.length-1];
if (!last || x!=last[0] || y!=last[1]) points.push([x,y]);
}
};
for (var d=0,len=path.getTotalLength(),step=len/samples;d<=len;d+=step){
var seg = segments[path.getPathSegAtLength(d)];
var pt = path.getPointAtLength(d);
if (seg != lastSeg){
lastSeg = seg;
while (segs.length && segs[0]!=seg) addSegmentPoint( segs.shift() );
}
var last = points[points.length-1];
if (!last || pt.x!=last[0] || pt.y!=last[1]) points.push([pt.x,pt.y]);
}
for (var i=0,len=segs.length;i<len;++i) addSegmentPoint(segs[i]);
for (var i=0,len=points.length;i<len;++i) points[i] = points[i].join(',');
poly.setAttribute('points',points.join(' '));
return poly;
}
area += (p2.x+p1.x) * (p2.y-p1.y)
被称为鞋带公式:https://en.wikipedia.org/wiki/Shoelace_formula - BallpointBengetPointAtLength
现在没有被弃用。不过,很遗憾,演示已经失效了,正如我在回答中编辑时所指出的那样。(这又是一个将演示限制在内部代码片段并附加到答案中的例子。) - ashleedawghttp://objectmix.com/graphics/133553-area-closed-bezier-curve.html (archived)
提供了闭合形式解决方案,使用散度定理。我惊讶于OP没有找到这个链接。
为防止网站失效,将文本复制并给予回复作者Kalle Rutanen信用。
一个有趣的问题。对于2D中的任何分段可微曲线,以下一般过程会给你曲线/曲线系列内部的面积。对于多项式曲线(Bezier曲线),您将得到闭合形式的解决方案。 设g(t)是一个分段可微曲线,其中0<=t<=1。 g(t)顺时针定向,g(1)=g(0)。 令F(x, y)=[x, y]/2 然后div(F(x, y))=1,其中div为发散。 现在,发散定理将为您提供沿着曲线的线积分内部的封闭曲线g(t)的面积: int(dot(F(g(t)), perp(g'(t))) dt, t = 0..1) = (1 / 2) * int(dot(g(t), perp(g'(t))) dt, t = 0..1) perp(x, y)=(-y, x) 其中int为积分,'为差分,dot为点积。必须将积分分为与平滑曲线段相应的部分。 现在举个例子。取Bezier度数3和具有控制点(x0, y0), (x1, y1), (x2, y2), (x3, y3)的曲线之一。这条曲线上的积分是: I:=3/10*y1*x0-3/20*y1*x2-3/20*y1*x3-3/10*y0*x1-3/20*y0*x2-1/20*y0*x3+3/20*y2*x0+3/20*y2*x1-3/10*y2*x3+1/20*y3*x0+3/20*y3*x1+3/10*y3*x2 计算每个曲线的值并将它们相加。总和是由曲线围成的区域(假设曲线形成一个循环)。 如果曲线只包含一个Bezier曲线,则必须x3=x0且y3=y0,面积为: Area:=3/20*y1*x0-3/20*y1*x2-3/20*y0*x1+3/20*y0*x2-3/20*y2*x0+3/20*y2*x1 希望我没有犯错。 --Kalle Rutanen http://kaba.hilvi.orgSVGPathElement.getPointAtLength()
which is used in the accepted answer is deprecated according to Mozilla.(x0/y0)
, (x1/y1)
, (x2/y2)
和(x3/y3)
描述贝塞尔曲线(其中(x0/y0)
是起始点,(x3/y3)
是终点)时,可以使用参数化形式:
在Bézier曲线上,B(t)表示点,Pi表示Bézier曲线定义点(参见上文,P0是起始点,...)。 t是运行变量,其中0≤t≤1。
这种形式使得近似Bézier曲线非常容易:您可以使用t=i/npoints生成尽可能多的点。(请注意,您必须添加起点和终点)。结果是一个多边形。然后,您可以使用shoelace formular(如@Phrogz在其解决方案中所做的那样)来计算面积。请注意,对于鞋带公式,点的顺序很重要。通过使用t作为参数,顺序将始终是正确的。
为了配合这个问题,这里提供了一个交互式示例的代码片段,也是用JavaScript编写的。这可以应用到其他语言中。它不使用任何特定于JavaScript(或SVG)的命令(除了绘图)。请注意,这需要支持HTML5的浏览器才能工作。/**
* Approximate the bezier curve points.
*
* @param bezier_points: object, the points that define the
* bezier curve
* @param point_number: int, the number of points to use to
* approximate the bezier curve
*
* @return Array, an array which contains arrays where the
* index 0 contains the x and the index 1 contains the
* y value as floats
*/
function getBezierApproxPoints(bezier_points, point_number){
if(typeof bezier_points == "undefined" || bezier_points === null){
return [];
}
var approx_points = [];
// add the starting point
approx_points.push([bezier_points["x0"], bezier_points["y0"]]);
// implementation of the bezier curve as B(t), for futher
// information visit
// https://wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B%C3%A9zier_curves
var bezier = function(t, p0, p1, p2, p3){
return Math.pow(1 - t, 3) * p0 +
3 * Math.pow(1 - t, 2) * t * p1 +
3 * (1 - t) * Math.pow(t, 2) * p2 +
Math.pow(t, 3) * p3;
};
// Go through the number of points, divide the total t (which is
// between 0 and 1) by the number of points. (Note that this is
// point_number - 1 and starting at i = 1 because of adding the
// start and the end points.)
// Also note that using the t parameter this will make sure that
// the order of the points is correct.
for(var i = 1; i < point_number - 1; i++){
let t = i / (point_number - 1);
approx_points.push([
// calculate the value for x for the current t
bezier(
t,
bezier_points["x0"],
bezier_points["x1"],
bezier_points["x2"],
bezier_points["x3"]
),
// calculate the y value
bezier(
t,
bezier_points["y0"],
bezier_points["y1"],
bezier_points["y2"],
bezier_points["y3"]
)
]);
}
// Add the end point. Note that it is important to do this
// **after** the other points. Otherwise the polygon will
// have a weird form and the shoelace formular for calculating
// the area will get a weird result.
approx_points.push([bezier_points["x3"], bezier_points["y3"]]);
return approx_points;
}
/**
* Get the bezier curve values of the given path.
*
* The returned array contains objects where each object
* describes one cubic bezier curve. The x0/y0 is the start
* point and the x4/y4 is the end point. x1/y1 and x2/y2 are
* the control points.
*
* Note that a path can also contain other objects than
* bezier curves. Arcs, quadratic bezier curves and lines
* are ignored.
*
* @param svg: SVGElement, the svg
* @param path_id: String, the id of the path element in the
* svg
*
* @return array, an array of plain objects where each
* object represents one cubic bezier curve with the values
* x0 to x4 and y0 to y4 representing the x and y
* coordinates of the points
*/
function getBezierPathPoints(svg, path_id){
var path = svg.getElementById(path_id);
if(path === null || !(path instanceof SVGPathElement)){
return [];
}
var path_segments = splitPath(path);
var points = [];
var x = 0;
var y = 0;
for(index in path_segments){
if(path_segments[index]["type"] == "C"){
let bezier = {};
// start is the end point of the last element
bezier["x0"] = x;
bezier["y0"] = y;
bezier["x1"] = path_segments[index]["x1"];
bezier["y1"] = path_segments[index]["y1"];
bezier["x2"] = path_segments[index]["x2"];
bezier["y2"] = path_segments[index]["y2"];
bezier["x3"] = path_segments[index]["x"];
bezier["y3"] = path_segments[index]["y"];
points.push(bezier);
}
x = path_segments[index]["x"];
y = path_segments[index]["y"];
}
return points;
}
/**
* Split the given path to the segments.
*
* @param path: SVGPathElement, the path
*
* @return object, the split path `d`
*/
function splitPath(path){
let d = path.getAttribute("d");
d = d.split(/\s*,|\s+/);
let segments = [];
let segment_names = {
"M": ["x", "y"],
"m": ["dx", "dy"],
"H": ["x"],
"h": ["dx"],
"V": ["y"],
"v": ["dy"],
"L": ["x", "y"],
"l": ["dx", "dy"],
"Z": [],
"C": ["x1", "y1", "x2", "y2", "x", "y"],
"c": ["dx1", "dy1", "dx2", "dy2", "dx", "dy"],
"S": ["x2", "y2", "x", "y"],
"s": ["dx2", "dy2", "dx", "dy"],
"Q": ["x1", "y1", "x", "y"],
"q": ["dx1", "dy1", "dx", "dy"],
"T": ["x", "y"],
"t": ["dx", "dy"],
"A": ["rx", "ry", "rotation", "large-arc", "sweep", "x", "y"],
"a": ["rx", "ry", "rotation", "large-arc", "sweep", "dx", "dy"]
};
let current_segment_type;
let current_segment_value;
let current_segment_index;
for(let i = 0; i < d.length; i++){
if(typeof current_segment_value == "number" && current_segment_value < segment_names[current_segment_type].length){
let segment_values = segment_names[current_segment_type];
segments[current_segment_index][segment_values[current_segment_value]] = d[i];
current_segment_value++;
}
else if(typeof segment_names[d[i]] !== "undefined"){
current_segment_index = segments.length;
current_segment_type = d[i];
current_segment_value = 0;
segments.push({"type": current_segment_type});
}
else{
delete current_segment_type;
delete current_segment_value;
delete current_segment_index;
}
}
return segments;
}
/**
* Calculate the area of a polygon. The pts are the
* points which define the polygon. This is
* implementing the shoelace formular.
*
* @param pts: Array, the points
*
* @return float, the area
*/
function polyArea(pts){
var area = 0;
var n = pts.length;
for(var i = 0; i < n; i++){
area += (pts[i][1] + pts[(i + 1) % n][1]) * (pts[i][0] - pts[(i + 1) % n][0]);
}
return Math.abs(area / 2);
}
// only for the demo
(function(){
document.getElementById('number_of_points').addEventListener('change', function(){
var svg = document.getElementById("svg");
var bezier_points = getBezierPathPoints(svg, "path");
// in this example there is only one bezier curve
bezier_points = bezier_points[0];
// number of approximation points
var approx_points_num = parseInt(this.value);
var approx_points = getBezierApproxPoints(bezier_points, approx_points_num);
var doc = svg.ownerDocument;
// remove polygon
var polygons;
while((polygons = doc.getElementsByTagName("polygon")).length > 0){
polygons[0].parentNode.removeChild(polygons[0]);
}
// remove old circles
var circles;
while((circles = doc.getElementsByTagName("circle")).length > 0){
circles[0].parentNode.removeChild(circles[0]);
}
// add new circles and create polygon
var polygon_points = [];
for(var i = 0; i < approx_points.length; i++){
let circle = doc.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', approx_points[i][0]);
circle.setAttribute('cy', approx_points[i][1]);
circle.setAttribute('r', 1);
circle.setAttribute('fill', '#449944');
svg.appendChild(circle);
polygon_points.push(approx_points[i][0], approx_points[i][1]);
}
var polygon = doc.createElementNS('http://www.w3.org/2000/svg', 'polygon');
polygon.setAttribute("points", polygon_points.join(" "));
polygon.setAttribute("stroke", "transparent");
polygon.setAttribute("fill", "#cccc00");
polygon.setAttribute("opacity", "0.7");
svg.appendChild(polygon);
doc.querySelector("output[name='points']").innerHTML = approx_points_num;
doc.querySelector("output[name='area']").innerHTML = polyArea(approx_points);
});
var event = new Event("change");
document.getElementById("number_of_points").dispatchEvent(event);
})();
<html>
<body>
<div style="width: 100%; text-align: center;">
<svg width="250px" height="120px" viewBox="-5 -5 45 30" id="svg">
<path d="M 0 0 C 10 15 50 40 30 0 Z" fill="transparent" stroke="black" id="path" />
</svg>
<br />
<input type="range" min="3" max="100" value="5" class="slider" id="number_of_points">
<br />
Approximating with
<output name="points" for="number_of_points"></output>
points, area is
<output name="area"></output>
</div>
</body>
</html>
我喜欢Phrogz提供的被采纳答案中的解决方案,但我也进一步寻找了一种使用Paper.js的CompoundPath
类和area
属性实现相同效果的方法。请查看我的Paper.js演示。
受 James Godfrey-Kittle 在 this bezierInfo thread: add section: area under a bézier curve 中的建议启发,我将此概念包装成了一个 js 助手函数,它可以获取 svg 的 <path>
和其他元素的区域。
它基于与 @nbonneel's answer 建议相同的公式。
主要步骤:
d
属性标准化为一系列绝对和三次贝塞尔曲线命令的数组。为此,我使用了 Jarek Foksa's path-data polyfill。该填充程序允许我们通过其 getPathData({normalize:true})
选项从任何路径检索绝对坐标。这样就不必担心相对、三次贝塞尔或速记命令。计算每个曲线段(b0和b1)的面积。
/**
* James Godfrey-Kittle@jamesgk
* https://github.com/Pomax/BezierInfo-2/issues/238
*/
function getBezierArea(coords) {
let x0 = coords[0];
let y0 = coords[1];
//如果是三次贝塞尔曲线命令
if (coords.length == 8) {
let x1 = coords[2];
let y1 = coords[3];
let x2 = coords[4];
let y2 = coords[5];
let x3 = coords[6];
let y3 = coords[7];
let area = (
x0 * (-2 * y1 - y2 + 3 * y3) +
x1 * (2 * y0 - y2 - y3) +
x2 * (y0 + y1 - 2 * y3) +
x3 * (-3 * y0 + y1 + 2 * y2)
) * 3 / 20;
return area;
} else {
return 0;
}
}
x0, y0
是当前 C
命令之前命令的最后坐标。 x1, y1, x2, y2, x3, y3
是当前路径数据值。
由于我们不需要基于相对昂贵的 getPointAtLength()
方法的多边形近似 - 计算相对较快。
我们可以轻松检查计算是否正确,因为预期结果应该是:
π·50²/2 = 3926.99
//example 1:
let svg = document.querySelector("svg");
let path = svg.querySelector("path");
let pathArea = getshapeAreaSimple(path);
let result = document.getElementById("result");
result.textContent = 'area: ' + pathArea;
function getshapeAreaSimple(el) {
let totalArea = 0;
let polyPoints = [];
let type = el.nodeName.toLowerCase();
let log = [];
let bezierArea = 0;
let pathData = el.getPathData({
normalize: true
});
pathData.forEach(function(com, i) {
let [type, values] = [com.type, com.values];
if (values.length) {
let prevC = i > 0 ? pathData[i - 1] : pathData[0];
let prevCVals = prevC.values;
let prevCValsL = prevCVals.length;
let [x0, y0] = [prevCVals[prevCValsL - 2], prevCVals[prevCValsL - 1]];
// C commands
if (values.length == 6) {
let area = getBezierArea([
x0,
y0,
values[0],
values[1],
values[2],
values[3],
values[4],
values[5]
]);
//push points to calculate inner/remaining polygon area
polyPoints.push([x0, y0], [values[4], values[5]]);
bezierArea += area;
}
// L commands
else {
polyPoints.push([x0, y0], [values[0], values[1]]);
}
}
});
let areaPoly = polygonArea(polyPoints, false);
//values have the same sign - subtract polygon area
if ((areaPoly < 0 && bezierArea < 0) || (areaPoly > 0 && bezierArea > 0)) {
totalArea = Math.abs(bezierArea) - Math.abs(areaPoly);
} else {
totalArea = Math.abs(bezierArea) + Math.abs(areaPoly);
}
return totalArea;
}
function getPathArea(pathData) {
let totalArea = 0;
let polyPoints = [];
pathData.forEach(function(com, i) {
let [type, values] = [com.type, com.values];
if (values.length) {
let prevC = i > 0 ? pathData[i - 1] : pathData[0];
let prevCVals = prevC.values;
let prevCValsL = prevCVals.length;
let [x0, y0] = [prevCVals[prevCValsL - 2], prevCVals[prevCValsL - 1]];
// C commands
if (values.length == 6) {
let area = getBezierArea([
x0,
y0,
values[0],
values[1],
values[2],
values[3],
values[4],
values[5]
]);
//push points to calculate inner/remaining polygon area
polyPoints.push([x0, y0], [values[4], values[5]]);
totalArea += area;
}
// L commands
else {
polyPoints.push([x0, y0], [values[0], values[1]]);
}
}
});
let areaPoly = polygonArea(polyPoints);
totalArea = Math.abs(areaPoly) + Math.abs(totalArea);
return totalArea;
}
/**
* James Godfrey-Kittle@jamesgk
* https://github.com/Pomax/BezierInfo-2/issues/238
*/
function getBezierArea(coords) {
let x0 = coords[0];
let y0 = coords[1];
//if is cubic command
if (coords.length == 8) {
let x1 = coords[2];
let y1 = coords[3];
let x2 = coords[4];
let y2 = coords[5];
let x3 = coords[6];
let y3 = coords[7];
let area =
((x0 * (-2 * y1 - y2 + 3 * y3) +
x1 * (2 * y0 - y2 - y3) +
x2 * (y0 + y1 - 2 * y3) +
x3 * (-3 * y0 + y1 + 2 * y2)) *
3) /
20;
return area;
} else {
return 0;
}
}
function polygonArea(points, absolute = true) {
let area = 0;
for (let i = 0; i < points.length; i++) {
const addX = points[i][0];
const addY = points[i === points.length - 1 ? 0 : i + 1][1];
const subX = points[i === points.length - 1 ? 0 : i + 1][0];
const subY = points[i][1];
area += addX * addY * 0.5 - subX * subY * 0.5;
}
if (absolute) {
area = Math.abs(area);
}
return area;
}
svg {
max-height: 20em;
max-width: 100%;
border: 1px solid #ccc;
fill: #ccc;
}
<p> Expected area: <br /> π·50²/2 = 3926.99</p>
<p id="result"></p>
<svg viewBox="0 0 100 50">
<path d="M50,0C22.383,0,0,22.385,0,49.998h100C100,22.385,77.613,0,50,0z" />
</svg>
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.3/path-data-polyfill.min.js"></script>
为了实现更加灵活的帮助函数,我们可以包括像<circle>
、<ellipse>
、<polygon>
等基本元素,并跳过对这些元素类型的bézier计算。
复合路径 - 就像字母O或i这样的形状需要计算每个子路径的区域。如果一个子路径在另一个形状(例如字母O)的边界内,我们还需要从总面积中减去内部形状的面积。
function getshapeArea(el, decimals = 0) {
let totalArea = 0;
let polyPoints = [];
let type = el.nodeName.toLowerCase();
switch (type) {
// 1. paths
case "path":
let pathData = el.getPathData({
normalize: true
});
//check subpaths
let subPathsData = splitSubpaths(pathData);
let isCompoundPath = subPathsData.length > 1 ? true : false;
let counterShapes = [];
// check intersections for compund paths
if (isCompoundPath) {
let bboxArr = getSubPathBBoxes(subPathsData);
bboxArr.forEach(function(bb, b) {
//let path1 = path;
for (let i = 0; i < bboxArr.length; i++) {
let bb2 = bboxArr[i];
if (bb != bb2) {
let intersects = checkBBoxIntersections(bb, bb2);
if (intersects) {
counterShapes.push(i);
}
}
}
});
}
subPathsData.forEach(function(pathData, d) {
//reset polygon points for each segment
polyPoints = [];
let bezierArea = 0;
let pathArea = 0;
let multiplier = 1;
pathData.forEach(function(com, i) {
let [type, values] = [com.type, com.values];
if (values.length) {
let prevC = i > 0 ? pathData[i - 1] : pathData[0];
let prevCVals = prevC.values;
let prevCValsL = prevCVals.length;
let [x0, y0] = [
prevCVals[prevCValsL - 2],
prevCVals[prevCValsL - 1]
];
// C commands
if (values.length == 6) {
let area = getBezierArea([
x0,
y0,
values[0],
values[1],
values[2],
values[3],
values[4],
values[5]
]);
//push points to calculate inner/remaining polygon area
polyPoints.push([x0, y0], [values[4], values[5]]);
bezierArea += area;
}
// L commands
else {
polyPoints.push([x0, y0], [values[0], values[1]]);
}
}
});
//get area of remaining polygon
let areaPoly = polygonArea(polyPoints, false);
//subtract area by negative multiplier
if (counterShapes.indexOf(d) !== -1) {
multiplier = -1;
}
//values have the same sign - subtract polygon area
if (
(areaPoly < 0 && bezierArea < 0) ||
(areaPoly > 0 && bezierArea > 0)
) {
pathArea = (Math.abs(bezierArea) - Math.abs(areaPoly)) * multiplier;
} else {
pathArea = (Math.abs(bezierArea) + Math.abs(areaPoly)) * multiplier;
}
totalArea += pathArea;
});
break;
// 2. primitives:
// 2.1 circle an ellipse primitives
case "circle":
case "ellipse":
totalArea = getEllipseArea(el);
break;
// 2.2 polygons
case "polygon":
case "polyline":
totalArea = getPolygonArea(el);
break;
// 2.3 rectancle primitives
case "rect":
totalArea = getRectArea(el);
break;
}
if (decimals > 0) {
totalArea = +totalArea.toFixed(decimals);
}
return totalArea;
}
function getPathArea(pathData) {
let totalArea = 0;
let polyPoints = [];
pathData.forEach(function(com, i) {
let [type, values] = [com.type, com.values];
if (values.length) {
let prevC = i > 0 ? pathData[i - 1] : pathData[0];
let prevCVals = prevC.values;
let prevCValsL = prevCVals.length;
let [x0, y0] = [prevCVals[prevCValsL - 2], prevCVals[prevCValsL - 1]];
// C commands
if (values.length == 6) {
let area = getBezierArea([
x0,
y0,
values[0],
values[1],
values[2],
values[3],
values[4],
values[5]
]);
//push points to calculate inner/remaining polygon area
polyPoints.push([x0, y0], [values[4], values[5]]);
totalArea += area;
}
// L commands
else {
polyPoints.push([x0, y0], [values[0], values[1]]);
}
}
});
let areaPoly = polygonArea(polyPoints);
totalArea = Math.abs(areaPoly) + Math.abs(totalArea);
return totalArea;
}
/**
* James Godfrey-Kittle/@jamesgk : https://github.com/Pomax/BezierInfo-2/issues/238
*/
function getBezierArea(coords) {
let x0 = coords[0];
let y0 = coords[1];
//if is cubic command
if (coords.length == 8) {
let x1 = coords[2];
let y1 = coords[3];
let x2 = coords[4];
let y2 = coords[5];
let x3 = coords[6];
let y3 = coords[7];
let area =
((x0 * (-2 * y1 - y2 + 3 * y3) +
x1 * (2 * y0 - y2 - y3) +
x2 * (y0 + y1 - 2 * y3) +
x3 * (-3 * y0 + y1 + 2 * y2)) *
3) /
20;
return area;
} else {
return 0;
}
}
function polygonArea(points, absolute = true) {
let area = 0;
for (let i = 0; i < points.length; i++) {
const addX = points[i][0];
const addY = points[i === points.length - 1 ? 0 : i + 1][1];
const subX = points[i === points.length - 1 ? 0 : i + 1][0];
const subY = points[i][1];
area += addX * addY * 0.5 - subX * subY * 0.5;
}
if (absolute) {
area = Math.abs(area);
}
return area;
}
function getPolygonArea(el) {
// convert point string to arra of numbers
let points = el
.getAttribute("points")
.split(/,| /)
.filter(Boolean)
.map((val) => {
return parseFloat(val);
});
let polyPoints = [];
for (let i = 0; i < points.length; i += 2) {
polyPoints.push([points[i], points[i + 1]]);
}
let area = polygonArea(polyPoints);
return area;
}
function getRectArea(el) {
let width = el.getAttribute("width");
let height = el.getAttribute("height");
let area = width * height;
return area;
}
function getEllipseArea(el) {
let r = el.getAttribute("r");
let rx = el.getAttribute("rx");
let ry = el.getAttribute("ry");
//if circle – take radius
rx = rx ? rx : r;
ry = ry ? ry : r;
let area = Math.PI * rx * ry;
return area;
}
//path data helpers
function splitSubpaths(pathData) {
let pathDataL = pathData.length;
let subPathArr = [];
let subPathMindex = [];
pathData.forEach(function(com, i) {
let [type, values] = [com["type"], com["values"]];
if (type == "M") {
subPathMindex.push(i);
}
});
//split subPaths
subPathMindex.forEach(function(index, i) {
let end = subPathMindex[i + 1];
let thisSeg = pathData.slice(index, end);
subPathArr.push(thisSeg);
});
return subPathArr;
}
function getSubPathBBoxes(subPaths) {
let ns = "http://www.w3.org/2000/svg";
let svgTmp = document.createElementNS(ns, "svg");
svgTmp.setAttribute("style", "position:absolute; width:0; height:0;");
document.body.appendChild(svgTmp);
let bboxArr = [];
subPaths.forEach(function(pathData) {
let pathTmp = document.createElementNS(ns, "path");
svgTmp.appendChild(pathTmp);
pathTmp.setPathData(pathData);
let bb = pathTmp.getBBox();
bboxArr.push(bb);
});
svgTmp.remove();
return bboxArr;
}
function checkBBoxIntersections(bb, bb1) {
let [x, y, width, height, right, bottom] = [
bb.x,
bb.y,
bb.width,
bb.height,
bb.x + bb.width,
bb.y + bb.height
];
let [x1, y1, width1, height1, right1, bottom1] = [
bb1.x,
bb1.y,
bb1.width,
bb1.height,
bb1.x + bb1.width,
bb1.y + bb1.height
];
let intersects = false;
if (width * height != width1 * height1) {
if (width * height > width1 * height1) {
if (x < x1 && right > right1 && y < y1 && bottom > bottom1) {
intersects = true;
}
}
}
return intersects;
}
svg {
max-height: 20em;
max-width: 100%;
border: 1px solid #ccc;
fill: #ccc;
}
<p><button type="button" onclick="getSingleArea(path0)">Get this area</button></p>
<svg class="svg0" viewBox="300 51.399147033691406 215.8272705078125 98.6994857788086">
<path id="curve" d="M 300 140 C 300 40 505 16 480 113 C544 47 523 235 411 100Z" />
</svg>
<p class="result0"></p>
<svg class="svg1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 25">
<path id="singleCurve" d="M0,12.667h25C25-4.222,0-4.222,0,12.667z" />
<path id="circle-two-quarter" d="M37.5,12.667c0,6.904,5.596,12.5,12.5,12.5c0-6.511,0-12.5,0-12.5l12.5,0c0-6.903-5.597-12.5-12.5-12.5
v12.5L37.5,12.667z" />
<path id="circle-three-quarters" d="M75,12.667c0,6.904,5.596,12.5,12.5,12.5c6.903,0,12.5-5.597,12.5-12.5
c0-6.903-5.597-12.5-12.5-12.5v12.5L75,12.667z" />
<circle id="circle" cx="125" cy="12.667" r="12.5" />
<ellipse id="ellipse" cx="162.5" cy="13.325" rx="12.5" ry="6.25" />
<rect id="rect" x="187.5" y="0.167" width="25" height="25" />
<polygon id="hexagon" points="231.25,23.493 225,12.667 231.25,1.842 243.75,1.842 250,12.667 243.75,23.493 " />
<path id="compound" d="M268.951,10.432c-3.452,0-6.25,2.798-6.25,6.25s2.798,6.25,6.25,6.25s6.25-2.798,6.25-6.25
S272.403,10.432,268.951,10.432z M268.951,19.807c-1.726,0-3.125-1.399-3.125-3.125s1.399-3.125,3.125-3.125
s3.125,1.399,3.125,3.125S270.677,19.807,268.951,19.807z M272.076,4.968c0,1.726-1.399,3.125-3.125,3.125s-3.125-1.399-3.125-3.125
c0-1.726,1.399-3.125,3.125-3.125S272.076,3.242,272.076,4.968z" />
</svg>
<p class="result1"></p>
<p><button type="button" onclick="getAllAreas(areaEls)">Get all areas</button></p>
<!--Dependency: path data polyfill -->
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.3/path-data-polyfill.min.js"></script>
<script>
// 1st example: single path area
let svg0 = document.querySelector('.svg0');
let path0 = svg0.querySelector('path');
let result0 = document.querySelector('.result0');
function getSingleArea(shape) {
let shapeArea = getshapeArea(shape, 3);
result0.textContent = 'area: ' + shapeArea;
}
// 2nd example: multiple shape areas
let svg1 = document.querySelector('.svg1');
let areaEls = svg1.querySelectorAll('path, polygon, circle, ellipse, rect');
let result1 = document.querySelector('.result1');
//benchmark
let [t0, t1] = [0, 0];
function getAllAreas(areaEls) {
let results = []
perfStart();
areaEls.forEach(function(shape, i) {
let type = shape.nodeName.toLowerCase();
let id = shape.id ? '#' + shape.id : '<' + type + '/> [' + i + ']';
let shapeArea = getshapeArea(shape, 3);
let resultString = `<strong>${id}:</strong> ${shapeArea}`;
results.push(resultString);
let title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
title.textContent = `${id}: ${shapeArea}`;
shape.appendChild(title);
});
let totalTime = perfEnd();
result1.innerHTML = results.join('<br />') + '<br /><br /><strong>time: </strong>' + totalTime + 'ms';
}
/**
* helpers for performance testing
*/
function adjustViewBox(svg) {
let bb = svg.getBBox();
let [x, y, width, height] = [bb.x, bb.y, bb.width, bb.height];
svg.setAttribute('viewBox', [x, y, width, height].join(' '));
}
function perfStart() {
t0 = performance.now();
}
function perfEnd(text = '') {
t1 = performance.now();
total = t1 - t0;
return total;
}
</script>
你不能使用高斯神奇鞋带定理的应用程序,通过改变T获取一组数据点,然后将其简单地输入方程式吗?
这里有一个简单的视频演示https://www.youtube.com/watch?v=0KjG8Pg6LGk&ab_channel=Mathologer
在二维平面上移动的点的半径矢量所覆盖的正方形面积为1/2*integral[(x-xc)*dy/dt - (y-yc)*dx/dt]dt。这里xc和yc是原点(中心)的坐标。对于Bezier曲线的推导相当麻烦但是可行。请参见下面的squareAreaQuadr和squareAreaCubic函数。我已经测试和反复测试了这些公式,非常确定没有错误。在SVG坐标平面中,这个签名给出了顺时针旋转的正方形面积。
var xc=0.1, yc=0.2, x0=0.9, y0=0.1, x1=0.9, y1=0.9, x2=0.5, y2=0.5, x3=0.1, y3=0.9
var cubic = document.getElementById("cubic");
cubic.setAttribute("d", "M "+xc*500+" "+yc*500+" L "+x0*500+" "+y0*500+" C "+x1*500+" "+y1*500+" "+x2*500+" "+y2*500+" "+x3*500+" "+y3*500+" L "+xc*500+" "+yc*500);
var center1 = document.getElementById("center1");
center1.setAttribute("cx", xc*500);
center1.setAttribute("cy", yc*500);
function squareAreaCubic(xc, yc, x0, y0, x1, y1, x2, y2, x3, y3)
{
var s;
s = 3/4*( (x0-xc)*(y1-y0) + (x3-xc)*(y3-y2) ) +
1/4*(x3-x0)*(y1+y2-y0-y3) +
1/8*( (x0+x3-2*xc)*(3*y2-3*y1+y0-y3) + (x1+x2-x0-x3)*(y1-y0+y3-y2) ) +
3/40*( (2*x1-x0-x2)*(y1-y0) + (2*x2-x1-x3)*(y3-y2) ) +
1/20*( (2*x1-x0-x2)*(y3-y2) + (2*x2-x1-x3)*(y1-y0) + (x1+x2-x0-x3)*(3*y2-3*y1+y0-y3) ) +
1/40*(x1+x2-x0-x3)*(3*y2-3*y1+y0-y3) -
3/4*( (y0-yc)*(x1-x0) + (y3-yc)*(x3-x2) ) -
1/4*(y3-y0)*(x1+x2-x0-x3) -
1/8*( (y0+y3-2*yc)*(3*x2-3*x1+x0-x3) + (y1+y2-y0-y3)*(x1-x0+x3-x2) ) -
3/40*( (2*y1-y0-y2)*(x1-x0) + (2*y2-y1-y3)*(x3-x2) ) -
1/20*( (2*y1-y0-y2)*(x3-x2) + (2*y2-y1-y3)*(x1-x0) + (y1+y2-y0-y3)*(3*x2-3*x1+x0-x3) ) -
1/40*(y1+y2-y0-y3)*(3*x2-3*x1+x0-x3) ;
return s;
}
var s = squareAreaCubic(xc, yc, x0, y0, x1, y1, x2, y2, x3, y3);
document.getElementById("c").innerHTML = document.getElementById("c").innerHTML + s.toString();
<html>
<body>
<h1>Bezier square area</h1>
<p id="q">Quadratic: S = </p>
<svg height="500" width="500">
<rect width="500" height="500" style="fill:none; stroke-width:2; stroke:black" />
<path id="quadr" fill="lightgray" stroke="red" stroke-width="1" />
<circle id="q_center" r="5" fill="black" />
</svg>
<script>
var xc=0.1, yc=0.2, x0=0.9, y0=0.1, x1=0.9, y1=0.9, x2=0.1, y2=0.9;
var quadr = document.getElementById("quadr");
quadr.setAttribute("d", "M "+xc*500+" "+yc*500+" L "+x0*500+" "+y0*500+" Q "+x1*500+" "+y1*500+" "+x2*500+" "+y2*500+" L "+xc*500+" "+yc*500);
var center = document.getElementById("q_center");
q_center.setAttribute("cx", xc*500);
q_center.setAttribute("cy", yc*500);
function squareAreaQuadr(xc, yc, x0, y0, x1, y1, x2, y2)
{
var s = 1/2*( (x0-xc)*(y1-y0) + (x2-xc)*(y2-y1) - (y0-yc)*(x1-x0) - (y2-yc)*(x2-x1) ) +
1/12*( (x2-x0)*(2*y1-y0-y2) - (y2-y0)*(2*x1-x0-x2) );
return s;
}
var s = squareAreaQuadr(xc, yc, x0, y0, x1, y1, x2, y2);
document.getElementById("q").innerHTML = document.getElementById("q").innerHTML + s.toString();
</script>
<p id="c">Cubic: S = </p>
<svg height="500" width="500">
<rect width="500" height="500" style="fill:none; stroke-width:2; stroke:black" />
<path id="cubic" fill="lightgray" stroke="red" stroke-width="1" />
<circle id="center1" r="5" fill="black" />
</svg>
</body>
</html>