Search code examples
javascriptsvggeometryarea

How can I calculate the area of a bezier curve?


Given the following path (for example) which describes a SVG cubic bezier curve:

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

And assuming a straight line connecting the end points 300,140 to 500,140 (closing the area under the curve), is it possible to calculate the area so enclosed?

Can anyone suggest a formula (or JavaScript) to accomplish this?


Solution

  • Convert the path to a polygon of arbitrary precision, and then calculate the area of the polygon.

    Interactive Demo: Area of Path via Subdivision (broken)

                          Screenshot of Demo

    At its core the above demo uses functions for adaptively subdividing path into a polygon and computing 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);
    }
    

    Following is the original answer, which uses a different (non-adaptive) technique for converting the <path> to a <polygon>.

    Interactive Demo: http://phrogz.net/svg/area_of_path.xhtml (broken)

                      Screenshot of Demo

    At its core the above demo uses functions for approximating a path with a polygon and computing 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;
    }