Given the following path (for example) which describes a SVG cubic bezier curve: "M300,140C300,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?
These are vector equations. In other words, we can put x and y instead of P to get corresponding coordinates. For instance, the 3-point curve is formed by points (x,y) calculated as: x = (1−t)2x1 + 2(1−t)tx2 + t2x.
To find any point P along a line, use the formula: P = (1-t)P0 + (t)P1 , where t is the percentage along the line the point lies and P0 is the start point and P1 is the end point. Knowing this, we can now solve for the unknown control point.
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)
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)
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; }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With