如何计算贝塞尔曲线的面积?

时间:2012-04-06 05:52:28

标签: javascript svg

给出以下描述SVG三次贝塞尔曲线的路径(例如):“M300,140C300,40,500,40,500,140”, 并假设连接端点300,140至500,140的直线(关闭曲线下方的区域),是否可以计算如此封闭的区域?

有人可以建议使用公式(或JavaScript)来完成此任务吗?

6 个答案:

答案 0 :(得分:49)

Convert the path to a polygon任意精度,然后calculate the area of the polygon

互动演示:Area of Path via Subdivision

Screenshot of Demo

上述演示的核心是adaptively subdividing path into a polygoncomputing the area of a polygon

的功能
// 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>

互动演示:http://phrogz.net/svg/area_of_path.xhtml

Screenshot of Demo

上述演示的核心是approximating a path with a polygoncomputing the area of a 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;
}

答案 1 :(得分:10)

我犹豫是要发表评论或完整回复。但是,简单的谷歌搜索“区域贝塞尔曲线”导致前三个链接(第一个是同一个帖子),在:

http://objectmix.com/graphics/133553-area-closed-bezier-curve.html

使用发散定理提供封闭形式的解决方案。我很惊讶OP没有找到这个链接。

如果网站出现故障,请复制文本,并将回复的作者Kalle Rutanen记入帐户:

  

一个有趣的问题。对于2D中的任何分段可微曲线,   以下一般程序为您提供曲线内的区域/   系列曲线。对于多项式曲线(贝塞尔曲线),您将获得   封闭式解决方案。

     

设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用于积分,'用于区分,点用于点   产品。必须将集成拼接到相应的部件   到平滑的曲线段。

     

现在举例。取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

     

为序列中的每条曲线计算并加起来。总和   是由曲线包围的区域(假设曲线形成一个循环)。

     

如果曲线只包含一条贝塞尔曲线,那么它必须是x3 =   x0和y3 = y0,区域为:

     

面积:= 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.org

答案 2 :(得分:2)

首先,我对bezier曲线并不那么熟悉,但我知道它们是连续的函数。如果确保三次曲线不与自身相交,则可以在给定的封闭域([ab])上将其以闭合形式(我的意思是使用分析积分)进行积分,并减去由末端形成的三角形区域。连接直线和X轴。如果与贝塞尔曲线和末端连接直线相交,您可以分成几个部分并尝试以一致的方式分别计算每个区域。

对我来说,合适的搜索词是“连续功能整合”“积分”“功能区域”“微积分”

当然,您可以从贝塞尔曲线fn生成离散数据,并获得离散的X-Y数据并近似计算积分。

Descriptive drawing

答案 3 :(得分:2)

我喜欢Phrogz接受的答案中的解决方案,但我也看得更远,并找到了使用CompoundPath类和area属性对Paper.js执行相同操作的方法。 See my Paper.js demo

使用阈值0时,结果(表面积= 11856)与Phrogz's demo完全相同,但处理速度似乎更快!我知道加载Paper.js只是为了计算表面积是有点过头了,但是如果你正在考虑实现一个框架或者想调查Paper.js是如何做的那样......

答案 4 :(得分:1)

我遇到了同样的问题,但是我没有使用JavaScript,所以无法使用@Phrogz的可接受答案。另外,根据Mozilla,弃用了已接受答案中的SVGPathElement.getPointAtLength()

在描述带有点(x0/y0)(x1/y1)(x2/y2)(x3/y3)(其中(x0/y0)是起点,{{1} })),您可以使用参数化形式:

enter image description here (来源:Wikipedia

,其中 B (t)是贝塞尔曲线上的点,而 P i 是贝塞尔曲线定义的点点(请参见上文, P 0 是起点,...)。 t 是0≤ t ≤1的运行变量。

这种形式可以很容易地近似贝塞尔曲线:使用 t = i / n 可以生成任意数量的点。 i> 。 (请注意,您必须添加起点和终点)。结果是一个多边形。然后,您可以使用shoelace formular(就像@Phrogz在他的解决方案中所做的那样)来计算面积。请注意,对于鞋带配方师,点的顺序很重要。通过使用 t 作为参数,顺序将总是正确。

enter image description here

为了匹配这个问题,这里是一个示例,也是用javascript编写的。这可以被其他语言采用。它不使用任何javascript(或svg)特定的命令(图纸除外)。

这里的结果是300的面积。图中的长度为30,高度为10。“波”是对称的,它们相互抵消。所以这个区域是正确的。

(x3/y3)
// only for the demo to show the points and the bezier curve
var svg = document.getElementById("svg");
var bezier_points = getBezierPathPoints(svg);
// in this example there is only one bezier curve
bezier_points = bezier_points[0];
var approx_points = getBezierApproxPoints(bezier_points, 10);

// add corners to have a closed arae
approx_points.unshift([0, 0]);
approx_points.push([30, 0]);

// add the circles
var doc = svg.ownerDocument;
for(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);
}

console.log("The area is ", polyArea(approx_points));

/**
 *  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){
  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 THIS example path.
 *
 *  @param svg: SVGElement, the svg
 *
 *  @return object, the bezier curve
 */
function getBezierPathPoints(svg){
  var path = svg.getElementById("path");
  var path_segments = path.pathSegList;
  var points = [];
  
  var x = 0;
  var y = 0;
  for(index in path_segments){
    if(path_segments[index]["pathSegTypeAsLetter"] == "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;
}

/**
 *  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);
}

答案 5 :(得分:0)

<html>
<body>
<!--Square area covered by radius vector of a point moving in 2D plane is 1/2*integral[(x-xc)*dy/dt - (y-yc)*dx/dt]dt .
Here xc and yc are coordinates of the origin point (center).
Derivation for the case of Bezier curves is rather cumbersome but possible.
See functions squareAreaQuadr and squareAreaCubic below.
I have tested and retested these formulae, rather sure, that there are no mistakes.
This signature gives positive square area for clockwise rotation in SVG coordinates plane.-->
<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>

<script>
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();
</script>

</body>
</html>