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 3
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.
When describing a Bézier curve with the points (x0/y0)
, (x1/y1)
, (x2/y2)
and (x3/y3)
(where (x0/y0)
is the start point and (x3/y3)
the end point) you can use the parametrized form:
(source: Wikipedia)
with B(t) being the point on the Bézier curve and Pi the Bézier curve defining point (see above, P0 is the starting point, ...). t is the running variable with 0 ≤ t ≤ 1.
This form makes it very easy to approximate a Bézier curve: You can generate as much points as you want by using t = i / npoints. (Note that you have to add the start and the end point). The result is a polygon. You can then use the shoelace formular (like @Phrogz did in his solution) to calculate the area. Note that for the shoelace formular the order of the points is important. By using t as the parameter the order will always be correct.
To match the question here is an interactive example in the code snippet, also written in javascript. This can be adopted to other languages. It does not use any javascript (or svg) specific commands (except for the drawings). Note that this requires a browser which supports HTML5 to work.
/**
* 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);
})();
Approximating with
points, area is