我该如何计算���塞尔曲线的面积?

34

假设有以下路径(例如),描述了SVG立方贝塞尔曲线:

M 300,140 C 300,40, 500,40, 500,140

并且假设连接端点300,140500,140的直线(封闭曲线下的区域),是否可以计算所围面积?

有人能提供一个公式(或JavaScript)来完成这个任务吗?


1
你可能会在http://math.stackexchange.com/上获得更快、更好的答案。 - Ramesh
1
期待看到这个问题的好答案 :) - mihai
如果您能明确以下期望,那将会很好:a) 曲线穿过连接线(变为“负数”,如字母‘u’),b) 曲线有一个环(例如草书字母‘e’),c) 曲线在每个x值上有多个y值(例如大写字母‘S’)。 - Phrogz
另请参阅https://github.com/Pomax/BezierInfo-2/issues/238。特别是第二个公式(1/20 * (...))是y * dx的线积分,即曲线与x轴之间的有符号面积,因此如果您使用格林定理来计算由一系列三阶贝塞尔曲线段界定的封闭区域的面积,这就是您想要的内容。 - undefined
9个回答

55

将路径转换为任意精度的多边形, 然后 计算多边形的面积

交互演示: 通过细分计算路径面积(已损坏)

                      Screenshot of Demo

在其核心,上述演示使用函数自适应将路径细分为多边形计算多边形面积
// 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

在其核心,上述演示使用函数来用多边形逼近路径计算多边形的面积

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

2
这个解决方案非常适合我的目的,并且它让我免于重新学习大学微积分!我已经编写了一个计算多边形面积的函数。但我希望当时就想到将曲线转换;现在看来这很显然。感谢@Phrogz... - MikeW
2
这是一个不错的快速而粗暴的暴力解决方案。但如果你想要更精确的精度,这就不太适合:尝试将额外的样本设置为100,并制作一个紧密的曲线(在某个地方具有高曲率区域的曲线)。你会发现在这部分中没有足够的样本。解决方案是将样本大小从100增加到200或500,但当有数十条曲线时,这会使代码变慢。更好、更快、更精确的解决方案是采用自适应曲线分割,它在曲线的紧密部分产生更多的样本,在松散部分产生较少的样本。 - Timo Kähkönen
3
@Timo,你说服了我。 我编辑了我的答案,并添加了一个新函数,该函数自适应地细分<path>以产生一个多边形。 请注意,这种新技术可能无法处理某些自交路径。 (不幸的样本放置导致自交多边形的出现可能导致近似零面积,从而使细分算法认为其工作已完成。) - Phrogz
1
实际计算面积的神奇公式 area += (p2.x+p1.x) * (p2.y-p1.y) 被称为鞋带公式:https://en.wikipedia.org/wiki/Shoelace_formula - BallpointBen
2
我不知道之前的情况,但是getPointAtLength现在没有被弃用。不过,很遗憾,演示已经失效了,正如我在回答中编辑时所指出的那样。(这又是一个将演示限制在内部代码片段并附加到答案中的例子。) - ashleedawg
显示剩余3条评论

14
我犹豫着是发表评论还是完整回复。但是,简单的“area bezier curve”谷歌搜索结果显示前三个链接(第一个链接是这篇帖子):

http://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.org

3
我点击了你给的链接,它经过了好几次重定向后到了一个购买手表的地方。我不确定它是否正常运行。 - RamenChef
2
@nbonneel - “复制文本以防网站崩溃…”…谢谢!这个网站确实崩溃了。我希望每个人在分享外部信息时都能多花一秒钟!在这种情况下,有一个存档页面,但格式混乱,而且并非所有页面都被归档…无论如何,这就是我一直在寻找的信息,再次感谢! - ashleedawg
1
我认为这是最好的答案。这不是一个近似值。 - kamae

4
I had the same problem but I am not using javascript so I cannot use the accepted answer of @Phrogz. In addition the SVGPathElement.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)是终点)时,可以使用参数化形式: Parametrized form of a cubic bezier curve (来源:Wikipedia

在Bézier曲线上,B(t)表示点,Pi表示Bézier曲线定义点(参见上文,P0是起始点,...)。 t是运行变量,其中0≤t≤1。

这种形式使得近似Bézier曲线非常容易:您可以使用t=i/npoints生成尽可能多的点。(请注意,您必须添加起点和终点)。结果是一个多边形。然后,您可以使用shoelace formular(如@Phrogz在其解决方案中所做的那样)来计算面积。请注意,对于鞋带公式,点的顺序很重要。通过使用t作为参数,顺序将始终是正确的。

Interactive demo preview

为了配合这个问题,这里提供了一个交互式示例的代码片段,也是用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>


3
"Deprecated"这个词有些误导人,实际上它只是被移动到了父接口中,所以您可以继续使用它而不必担心。下面的注释解释了这一点。 - Robert Longson

3

我喜欢Phrogz提供的被采纳答案中的解决方案,但我也进一步寻找了一种使用Paper.js的CompoundPath类和area属性实现相同效果的方法。请查看我的Paper.js演示

结果(表面积=11856)与使用阈值0时{{link1:Phrogz的演示}}完全相同,但处理速度似乎更快!我知道仅为计算表面积加载Paper.js有些过度,但如果您正在考虑实施框架或想要调查Paper.js如何实现它...

3

受 James Godfrey-Kittle 在 this bezierInfo thread: add section: area under a bézier curve 中的建议启发,我将此概念包装成了一个 js 助手函数,它可以获取 svg 的 <path> 和其他元素的区域。 它基于与 @nbonneel's answer 建议相同的公式。

主要步骤:

  1. 解析并将路径的 d 属性标准化为一系列绝对和三次贝塞尔曲线命令的数组。为此,我使用了 Jarek Foksa's path-data polyfill。该填充程序允许我们通过其 getPathData({normalize:true}) 选项从任何路径检索绝对坐标。这样就不必担心相对、三次贝塞尔或速记命令。

enter image description here

  • 计算每个曲线段(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() 方法的多边形近似 - 计算相对较快。

    1. 将剩余多边形的面积添加到贝塞尔曲线区域 (p0)。此步骤还将使用鞋带公式。

    示例 1:半径为50的半圆(SVG用户单位)

    我们可以轻松检查计算是否正确,因为预期结果应该是:

    π·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>

    示例2:获取基本形状和复合路径的区域

    为了实现更加灵活的帮助函数,我们可以包括像<circle><ellipse><polygon>基本元素,并跳过对这些元素类型的bézier计算。

    复合路径 - 就像字母Oi这样的形状需要计算每个子路径的区域。如果一个子路径在另一个形状(例如字母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 : '&lt;' + type + '/&gt; [' + 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>

    Codepen示例


    2
    这可能是此页面上最好的答案。它真的值得更多的赞! - Zearin

    2
    首先,我不太熟悉贝塞尔曲线,但我知道它们是连续的函数。如果您确保您的三次曲线不相交,您可以在给定的封闭域([a-b])上使用解析积分来计算它,并减去由末端连接直线和X轴形成的三角形面积。如果与贝塞尔曲线和末端连接的直线相交,则可以将其分成部分,并以一致的方式尝试单独计算每个区域。
    对我来说,适合的搜索词是“连续函数积分”、“积分”、“函数下面的面积”、“微积分”。
    当然,您可以从贝塞尔曲线fn生成离散数据并获得离散的X-Y数据,从而近似计算积分。
    请参考以下描述性图示:

    1
    抱歉挖掘旧答案,但我理解这里的难点在于将曲线表示为y = f(x)函数以计算积分。Bézier曲线通常由一对参数函数x = X(t)和y = Y(t)表示,至少在我的理解中,这不利于计算定积分,该定积分等于您绘制的红色和绿色线段内部的面积。因此,尽管从技术上讲您所写的是正确的,但这里的挑战在于首先计算y = f(x)。 - Armen Michaeli

    0

    我可以建议一种数值方法来完成这个任务。 从一般情况开始。 三次贝塞尔曲线方程 您可以将其展开,最终得到 这个。 您可以代入您的坐标并简化,然后使用这个公式进行积分。

    这应该给出曲线和x轴之间的面积。然后,您可以使用标准积分减去线下的面积,这应该给出所包含的面积。

    积分公式(图3)的来源和更多信息:https://math.libretexts.org/Courses/University_of_California_Davis/UCD_Mat_21C%3A_Multivariate_Calculus/10%3A_Parametric_Equations_and_Polar_Coordinates/10.2%3A_Calculus_with_Parametric_Curves#:~:text=The%20area%20between%20a%20parametric,%E2%80%B2(t)dt


    0

    2
    你的回答对于多边形很有用,但对于曲线不适用。 - Chiara Ani
    1
    如果正确应用,它可以工作。 在我的情况下,我通过改变T在Excel中从曲线上获得了1000个点。 - Herbert Schreib

    -1

    在二维平面上移动的点的半径矢量所覆盖的正方形面积为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>


    2
    请添加一些描述而不仅仅放置代码。 - R4444
    短描述包含在我放置的代码中,但你是对的,它不完整。我会尝试发布更长的解释,但需要一些时间。 - jsakars
    请尝试在Web浏览器上运行此代码,这将澄清您的想法。 - jsakars

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接