Calc/Place horizontal labels around SVG Donut Graph (NO LIBRARIES) - javascript

I am looking to add 'floating' labels around an SVG Donut Graph, as seen below, using only SVG elements and vanilla JS.
The corresponding lines are a plus, but not necessary.
Building of the dynamic donut/sectors is complete and I have it ported/working correctly within an Angular application. I am using vanilla JS w/Jquery here for ease.
I have scoured StackOverflow and Google to their depths but cannot locate a Question & Answer which provides a compatible solution using an algorithm/code to calculate desired placement. 90% of similar questions reference d3.js while the others reference a different charting library.
Not sure where to begin as I don't think the information I have on the segments is enough to calc placement within the viewBox.
I do know the circumference of the drawn circles are each 100 with a radius of 15.91549430918954. stroke-dasharray and stroke-dashoffset are calculated and set to each segment to build out the colored segments appropriately.
Is it possible to use what data I have to figure this out? Do I need a little more? How would I translate the calculation to x/y coordinates for the viewBox?
Thanks.
$(document).ready(function() {
let data = [500,100,350,600];
let dataTotal = 0;
for (let i = 0; i < data.length; i++) {
dataTotal += data[i];
}
let colors = ['#ce4b99', '#4286f4', '#62f441', '#952ec1'];
let labels = ['A', 'B', 'C', 'D'];
let total = 0;
for (let i = 0; i < data.length; i++) {
let dataPercent = data[i] / dataTotal;
let dataVal = 100 * dataPercent;
var chart = document.getElementById("chart");
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
const node = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
chart.appendChild(group);
group.appendChild(node);
group.appendChild(label);
label.textContent = labels[i];
label.setAttribute('class', 'data-label');
label.setAttribute('x', '20%');
label.setAttribute('y', '20%');
node.setAttribute('stroke-dasharray',`${dataVal} ${100 - dataVal}`);
node.setAttribute('class','donut-segment');
node.setAttribute('fill','transparent');
node.setAttribute('stroke-dashoffset', getOffset(total, i))
total += dataVal;
node.setAttribute('stroke', colors[i]);
node.setAttribute('stroke-width','3');
node.setAttribute('r','15.91549430918954');
node.setAttribute('cx','42');
node.setAttribute('cy','42');
}
});
function getOffset(total, i) {
if (i === 0) return 25;
return ((100 - total) + 25);
}
.chart-contain {
width: 50%;
margin 0 auto;
}
.data-label {
font-size: 4px;
}
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js'></script>
<div class='chart-contain'>
<svg id='chart' width="100%" height="100%" viewBox="0 0 84 84" class="donut">
<circle class="donut-hole" cx="42" cy="42" r="15.91549430918954" fill="transparent"></circle>
<circle class="donut-ring" cx="42" cy="42" r="15.91549430918954" fill="transparent" stroke="#d2d3d4" stroke-width="3">
</circle>
</svg>
</div>

You may not like my answer because I couldn't use your code. I needed to organise the data differently. However I keep the general idea: I'm using stroke-dasharray and stroke-dashoffset. I would have used paths instead of strokes.
I'm using a radius of 20, but you may change it back. You can also change it to anything else.
In order to draw the text I calculated the starting angle and the ending angle of your arcs. Then I calculated the middle angle. Once I know the middle angle I can draw the text and the lines.
I'm putting the arcs in the #chart group, the text in the #text group and the lines in the #lines group.
const SVG_NS = 'http://www.w3.org/2000/svg';
const XLINK = 'http://www.w3.org/1999/xlink';
let r = 20,cx=42,cy=42;// the attributes for the circle
let text_r = r + 10; // the radius for the text
let perimeter = 2*r*Math.PI;//the perimeter of the circle
donut.setAttributeNS(null, "r", r);
donut.setAttributeNS(null, "cx", cx);
donut.setAttributeNS(null, "cy", cy);
let data = [
{
value:500,
stroke:'#ce4b99',
label: 'A'
},
{
value:100,
stroke:'#4286f4',
label: 'B'
},
{
value:350,
stroke:'#62f441',
label: 'C'
},
{
value:600,
stroke:'#952ec1',
label: 'D'
}
]
let total = 0;
data.map(d =>{
d.temp = total;
total += d.value;
})
data.map(d =>{
d.offset = map(d.temp,0,total,0, perimeter)
d.real_value = map(d.value, 0, total, 0, perimeter);
d.dashArray = `${d.real_value},${perimeter}`;
/// angles
let angleStart = -d.offset/r;
let angleEnd = (d.offset + d.real_value)/r;
d.angleMiddle = (angleEnd - angleStart)/2;
// text
let t = {}
t.props ={
x : cx + text_r*Math.cos(d.angleMiddle),
y : cy + text_r*Math.sin(d.angleMiddle),
}
t.textContent = d.label;
d.text_point = t;
//line
let l = {
x1 : cx + r*Math.cos(d.angleMiddle),
y1 : cy + r*Math.sin(d.angleMiddle),
x2 : cx + .9*text_r*Math.cos(d.angleMiddle),
y2 : cy + .9*text_r*Math.sin(d.angleMiddle),
}
d.line = l;
})
data.map(d=>{// create a new use element
d.use = document.createElementNS(SVG_NS, 'use');
d.use.setAttributeNS(XLINK, 'xlink:href', '#donut');
d.use.setAttributeNS(null, 'stroke', d.stroke);
d.use.setAttributeNS(null, 'stroke-dasharray', d.dashArray);
d.use.setAttributeNS(null, 'stroke-dashoffset', -d.offset);
chart.appendChild(d.use);
drawLine(d.line, lines);
drawText(d.text_point, text);
})
// helpers
function drawLine(o, parent) {
var line = document.createElementNS(SVG_NS, 'line');
for (var name in o) {
if (o.hasOwnProperty(name)) {
line.setAttributeNS(null, name, o[name]);
}
}
parent.appendChild(line);
return line;
}
function drawText(o, parent) {
var text = document.createElementNS(SVG_NS, 'text');
for (var name in o.props) {
if (o.props.hasOwnProperty(name)) {
text.setAttributeNS(null, name, o.props[name]);
}
text.textContent = o.textContent;
}
parent.appendChild(text);
return text;
}
function map(n, a, b, _a, _b) {
let d = b - a;
let _d = _b - _a;
let u = _d / d;
return _a + n * u;
}
svg{border:1px solid;}
#donut{fill:none; stroke-width:5px;}
text{dominant-baseline:central;text-anchor:middle;font-size: 4px;}
line{stroke:black;stroke-width:.1px}
<div class='chart-contain'>
<svg viewBox="0 0 84 84" width="250" class="donut">
<defs>
<circle id="donut" cx="42" cy="42" r="20" ></circle>
</defs>
<g id='text'></g>
<g id='lines'></g>
<g id='chart'></g>
</svg>
</div>
In order to get it exactly like yours you may want to rotate the whole chart -Math.PI/2 and then rotate the text back.

Related

how to plot svgs inside another svg?

let shape = document?.getElementById('path') ;
const random = (min: any, max: any) => Math.floor(Math.random() * (max - min)) + min;
let retArr= [];
const min_x = shape.getBBox().x;
const max_x = shape.getBBox().width + shape.getBBox().x ;
const min_y = shape.getBBox().y;
const max_y = shape.getBBox().height + shape.getBBox().y;
for(let i =0; i<70; i++){
const x:any = random(min_x, max_x);
const y:any = random(min_y, max_y);
retArr.push({x,y});
}
let shape = document?.getElementById('path') ;
let retArr= [];
const min_x = shape.getBBox().x;
const max_x = shape.getBBox().width + shape.getBBox().x ;
const min_y = shape.getBBox().y;
const max_y = shape.getBBox().height + shape.getBBox().y;
for(let i =0; i<70; i++){
const x:any = random(min_x, max_x);
const y:any = random(min_y, max_y);
retArr.push({x,y});
}
I want to insert 70 other svgs into the parent one. All of those should be inside the parent shape. I have tried the above logic but that doesn't seem to work. Many points are falling out of the shape based on the above logic
In the next example I'm using javascript to create 10 shapes (you can do the same for 70).
For clarity I'm using a simple function. You may want to build an object.
All shapes have a random position and are rotated a random angle. You may also try to use a random size. In this case you may want to recalculate the R constant and it may need to go inside the function.
Please read the comments in the code
const SVG_NS = "http://www.w3.org/2000/svg";
const SVG_XLINK = "http://www.w3.org/1999/xlink";
const R = 100 - 12.5; //circle's radius - half bird width
function bird() {
//create a new use element
let use = document.createElementNS(SVG_NS, "use");
use.setAttributeNS(SVG_XLINK, "xlink:href", "#s");
//set the width and the height of the use element
use.setAttribute("width", "21");
use.setAttribute("height", "21");
//set the random rotation. The shape is also translated backward half width so that it centers around 0
let rot = Math.round(Math.random() * 360);
use.setAttribute("transform", `rotate(${rot}) translate(-12.5 -12.5)`);
//calculate and set the x and y attributes for the use element
let a = Math.random() * 2 * Math.PI;
//I use this algorithm to avoid to many points in the center of the circle. Please read more about Disk Point Picking
let r = Math.sqrt(~~(Math.random() * R * R));
let x = r * Math.cos(a);
let y = r * Math.sin(a);
use.setAttribute("x", x);
use.setAttribute("y", y);
svg.appendChild(use);
}
for (let i = 0; i < 10; i++) {
bird();
}
<svg viewBox="-100 -100 200 200" width="200" id="svg">
<circle r="100"/>
<symbol id="s" viewBox="-21 -18.5 42 42">
<path stroke="gold" stroke-width="4" fill="white"
d="M-10,-5
Q0,-30 10,-5
Q30,0 10,5
Q0,40 -10,5
Q-30,0 -10,-5Z"/>
</symbol>
</svg>
Please read more about Disk Point Picking

Vue, SVG, after mousehover, mouseleave happens immediately after while still in SVG rect

I am trying to create an SVG NxN square of active locations. https://codepen.io/svsintel/pen/eYpKdaW ( you need to use native debug to see results)
I create a square of NxN rect. I want to know when I am hovering over a rect.
I first create a object of locations
created() {
let sz = this.brdsz;
let brd = new Object();
for (let a = 0; a < sz; a++) {
let l = alph[a];
let x = this.csize * (a + 1);
for (let n = 0; n < sz; n++) {
let k = l + (n + 1).toString();
let y = this.csize * (sz - n) + this.csize;
brd[k] = { id: k, x: x, y: y };
}
}
this.board = brd;
console.log(this.board)
},
Then I use v-for to display the rect for each location.
<div id="app">
<svg ref="svg" class="lineclass" width="400" height="400" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g ref="brd">
<rect v-for="(cell,key) in board"
:key="key"
:id="key"
class="noshow"
:width="csize * 0.8"
:height="csize * 0.8"
:x="cell.x - csize*0.4"
:y="cell.y - csize*0.4"
#mouseover="mover($event)"
#mouseleave="mleave($event)"
#click="click($event)"
#mousemove="mouse_move($event, key)"></rect>
</g>
</svg>
</div>
The problem is when the mouse enters a rect I get a hover event and then a leave event while the mouse is still in the rect.
Because it thinks it is not in the rect, click does not work also.
See codepen https://codepen.io/svsintel/pen/eYpKdaW
By default you only get pointer events from places you've drawn on and fill:none doesn't count as drawing on something. You can fix this with the pointer-events property.
In your CSS add either pointer-events: visible; or perhaps pointer-events: visibleFill; depending on exactly what you want. I.e.
noshow {
fill: none;
stroke: black;
stroke-width: 1;
pointer-events: visible;
}

center of Raphael triangle

Let's say I need to put a text in the middle of the area of a triangle.
I can calculate the coordinates of the triangle's center using getBBox():
var triangle = "M0,0 L100,0 100,50 z";
var BBox = paper.path(triangle).getBBox();
var middle;
middle.x = BBox.x + BBox.width/2;
middle.y = BBox.y + BBox.height/2;
This results in the coordinates (50, 25) which are always on the long side of the triangle.
How can I make sure the calculated "middle" is inside the triangle?
The correct coordinates should be approximately: (75, 25).
The code should of course be independent of this particular example, it should work for any kind of shape.
I've done some more research in the topic, and following an advice from another list I got here:
https://en.wikipedia.org/wiki/Centroid
There is an algorithm there to calculate the centroid of an irregular polygon, which I have translated into this code:
function getCentroid(path) {
var x = new Array(11);
var y = new Array(11);
var asum = 0, cxsum = 0, cysum = 0;
var totlength = path.getTotalLength();
for (var i = 0; i < 11; i++) {
var location = path.getPointAtLength(i*totlength/10);
x[i] = location.x;
y[i] = location.y;
if (i > 0) {
asum += x[i - 1]*y[i] - x[i]*y[i - 1];
cxsum += (x[i - 1] + x[i])*(x[i - 1]*y[i] - x[i]*y[i - 1]);
cysum += (y[i - 1] + y[i])*(x[i - 1]*y[i] - x[i]*y[i - 1]);
}
}
return({x: (1/(3*asum))*cxsum, y: (1/(3*asum))*cysum});
}
It's basically an approximation of any path by 10 points (the 11th is equal to the starting point), and the function returns, for that triangle, the coordinates:
Object {x: 65.32077336966377, y: 16.33111549955705}
I've tested it with many other shapes, and it works pretty good.
Hope it helps somebody.
This snippet will calculate the center of any polygon by averaging the vertices.
var paper = Raphael(0,0, 320, 200);
var triangle = "M0,0 L100,0 100,50 z";
var tri = paper.path(triangle);
tri.attr('fill', 'blue');
var center = raphaelPathCenter( tri );
var circle = paper.circle( center.x, center.y, 5);
circle.attr("fill", "#f00");
circle.attr("stroke", "#fff");
function raphaelPathCenter( path ) {
path.getBBox(); // forces path to be traced so realPath is not null.
var vertices = parseSVGVertices( path.realPath );
var center = vertices.reduce( function(prev,cur) {
return { x: prev.x + cur.x, y: prev.y + cur.y }
}, {x:0, y:0} );
center.x /= vertices.length;
center.y /= vertices.length;
return center;
}
function parseSVGVertices( svgPath )
{
var vertices = [];
for ( var i = 0; i < svgPath.length; i ++ )
{
var vertex = svgPath[i];
if ( "ML".indexOf( vertex[0] ) > -1 ) // check SVG command
vertices.push( { x: vertex[1], y: vertex[2] } );
}
return vertices;
}
<script src="https://raw.githubusercontent.com/DmitryBaranovskiy/raphael/master/raphael-min.js"></script>
<canvas id='canvas'></canvas>
<pre id='output'></pre>
However there are a few more triangle centers to choose from.

Transform SVG path to canvas html5

Here I have some code that make svg path:
http://jsbin.com/gazecasagi/1/edit?html,output
<html>
<head>
<script>
function draw() {
var polygons = [[{"X":22,"Y":59.45},{"X":136,"Y":66},{"X":170,"Y":99},{"X":171,"Y":114},{"X":183,"Y":125},{"X":218,"Y":144},{"X":218,"Y":165},{"X":226,"Y":193},{"X":254,"Y":195},{"X":283,"Y":195},{"X":292,"Y":202},{"X":325,"Y":213},{"X":341,"Y":134},{"X":397,"Y":245},{"X":417,"Y":548}]];
var scale = 1000;
reverse_copy(polygons);
polygons = scaleup(polygons, scale);
var cpr = new ClipperLib.Clipper();
var delta = 20;
var joinType = ClipperLib.JoinType.jtRound;
var miterLimit = 2;
var AutoFix = true;
var svg, offsetted_polygon,
cont = document.getElementById('svgcontainer');
offsetted_polygon = cpr.OffsetPolygons(polygons, delta * scale, joinType, miterLimit, AutoFix);
//console.log(JSON.stringify(offsetted_polygon));
// Draw red offset polygon
svg = '<svg style="margin-top:10px;margin-right:10px;margin-bottom:10px;background-color:#dddddd" width="800" height="600">';
svg += '<path stroke="red" fill="red" stroke-width="2" stroke-opacity="0.6" fill-opacity="0.2" d="' + polys2path(offsetted_polygon, scale) + '"/>';
//Draw blue polyline
svg += '<path stroke="blue" stroke-width="1" d="' + polys2path(polygons, scale) + '"/>';
svg += '</svg>';
cont.innerHTML += svg;
}
// helper function to scale up polygon coordinates
function scaleup(poly, scale) {
var i, j;
if (!scale) scale = 1;
for(i = 0; i < poly.length; i++) {
for(j = 0; j < poly[i].length; j++) {
poly[i][j].X *= scale;
poly[i][j].Y *= scale;
}
}
return poly;
}
// converts polygons to SVG path string
function polys2path (poly, scale) {
var path = "", i, j;
if (!scale) scale = 1;
for(i = 0; i < poly.length; i++) {
for(j = 0; j < poly[i].length; j++){
if (!j) path += "M";
else path += "L";
path += (poly[i][j].X / scale) + ", " + (poly[i][j].Y / scale);
}
path += "Z";
}
return path;
}
function reverse_copy(poly) {
// Make reverse copy of polygons = convert polyline to a 'flat' polygon ...
var k, klen = poly.length, len, j;
for (k = 0; k < klen; k++) {
len = poly[k].length;
poly[k].length = len * 2 - 2;
for (j = 1; j <= len - 2; j++) {
poly[k][len - 1 + j] = {
X: poly[k][len - 1 - j].X,
Y: poly[k][len - 1 - j].Y
}
}
}
}
</script>
</head>
<body onload="draw()">
<div id="svgcontainer"></div>
</body>
</html>
Is there a simple way to tranform SVG path to Canvas. I need this becouse I need to show this example on mobile devices and Canvas have a better performance than canvas on mobile devices.
This code I need to transform to CANVAS:
// Draw red offset polygon
svg = '<svg style="margin-top:10px;margin-right:10px;margin-bottom:10px;background-color:#dddddd" width="800" height="600">';
svg += '<path stroke="red" fill="red" stroke-width="2" stroke-opacity="0.6" fill-opacity="0.2" d="' + polys2path(offsetted_polygon, scale) + '"/>';
//Draw blue polyline
svg += '<path stroke="blue" stroke-width="1" d="' + polys2path(polygons, scale) + '"/>';
svg += '</svg>';
How I can transform SVG path to simple CANVAS path?
You can use canvg library to convert svg to canvas.
You should include all its necessary js files to your page and then use it something like this:
canvg(document.getElementById('canvasElement'), '<svg>...</svg>')
Of course, the fastest way to present your complex polyline is to convert it into an image.
A fully optimized canvas version of your complex polyline would involve a canvas path:
Create a closed path of your red outline using lines with Bezier curves for joins. You can use context.lineTo and context.quadraticCurveTo + context.bezierCurveTo to define the path. The resulting path is commonly called a spline.
Stroke the path with red.
Fill the path with pink.
Draw the blue line.
This is not hard to do but does involve some trigonometry (mainly finding points tangent to the vectors of your polyline).
Here's an alternative that uses shadowing to mimic your complex SVG polyline:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var pts = [{x:22,y:59.45},{x:136,y:66},{x:170,y:99},{x:171,y:114},{x:183,y:125},{x:218,y:144},{x:218,y:165},{x:226,y:193},{x:254,y:195},{x:283,y:195},{x:292,y:202},{x:325,y:213},{x:341,y:134},{x:397,y:245},{x:417,y:548}];
mimicSvg(pts);
function mimicSvg(pts){
// make caps & joins round
ctx.lineCap='round';
ctx.lineJoin='round';
// draw the outside line with red shadow
ctx.shadowColor='red';
ctx.shadowBlur='2';
ctx.lineWidth=25;
// draw multiple times to darken shadow
drawPolyline(pts);
drawPolyline(pts);
drawPolyline(pts);
// stop shadowing
ctx.shadowColor='transparent';
// refill the outside line with pink
ctx.strokeStyle='pink';
drawPolyline(pts);
// draw the inside line
ctx.lineWidth=2;
ctx.strokeStyle='blue';
drawPolyline(pts);
}
function drawPolyline(pts){
ctx.beginPath();
ctx.moveTo(pts[0].x,pts[0].y);
for(var i=1;i<pts.length;i++){
ctx.lineTo(pts[i].x,pts[i].y);
}
ctx.stroke();
}
body{ background-color: ivory; padding:10px; }
#canvas{border:1px solid red;}
<canvas id="canvas" width=500 height=600></canvas>

How can I calculate the area of a bezier curve?

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?
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;
}
I hesitated to just make a comment or a full reply. But a simple Google search of "area bezier curve" results in the first three links (the first one being this same post), in :
http://objectmix.com/graphics/133553-area-closed-bezier-curve.html (archived)
that provides the closed form solution, using the divergence theorem. I am surprised that this link has not been found by the OP.
Copying the text in case the website goes down, and crediting the author of the reply Kalle Rutanen:
An interesting problem. For any piecewise differentiable curve in 2D,
the following general procedure gives you the area inside the curve /
series of curves. For polynomial curves (Bezier curves), you will get
closed form solutions.
Let g(t) be a piecewise differentiable curve, with 0 <= t <= 1. g(t)
is oriented clockwise and g(1) = g(0).
Let F(x, y) = [x, y] / 2
Then div(F(x, y)) = 1 where div is for divergence.
Now the divergence theorem gives you the area inside the closed curve
g (t) as a line integral along the curve:
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)
where int is for integration, ' for differentiation and dot for dot
product. The integration has to be pieced to the parts corresponding
to the smooth curve segments.
Now for examples. Take the Bezier degree 3 and one such curve with
control points (x0, y0), (x1, y1), (x2, y2), (x3, y3). The integral
over this curve is:
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
Calculate this for each curve in the sequence and add them up. The sum
is the area enclosed by the curves (assuming the curves form a loop).
If the curve consists of just one Bezier curve, then it must be x3 =
x0 and y3 = y0, and the area is:
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
Hope I did not do mistakes.
--
Kalle Rutanen
http://kaba.hilvi.org
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);
})();
<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>
I like the solution in the accepted answer by Phrogz, but I also looked a little further and found a way to do the same with Paper.js using the CompoundPath class and area property. See my Paper.js demo.
The result (surface area = 11856) is the exact same as with Phrogz's demo when using threshold 0, but the processing seems a lot quicker! I know it's overkill to load Paper.js just to calculate the surface area, but if you are considering implementing a framework or feel like investigating how Paper.js does it...
Firstly, I am not so familiar with Bézier curves, but I know that they are continuous functions. If you ensure that your cubic curve does not intersect itself, you may integrate it in closed form (I mean by using analytic integrals) on the given enclosing domain ([a-b]) and subtract the area of triangle that is formed by the the end joining straight line and the X axis. In case of intersection with the Bézier curve and end joining straight line, you may divide into sections and try to calculate each area separately in a consistent manner..
For me suitable search terms are "continuous function integration" "integrals" "area under a function" "calculus"
Of course you may generate discrete data from your Bézier curve fn and obtain discrete X-Y data and calculate the integral approximately.
Couldn't you use an application of Gauss's magic shoelace theorem by getting a set of data points by changing T, then simply inputting that into the equation?
Here's a simple video demo https://www.youtube.com/watch?v=0KjG8Pg6LGk&ab_channel=Mathologer
And then here's the wiki https://en.wikipedia.org/wiki/Shoelace_formula
I can suggest a formula to do this numerically.
Starting with the general.
Cubic Bezier Equation
You can expand it out and you will end up with
this.
You can sub in your coordinates and simplify, then integrate with this formula.
This should give you the area between the curve and the x-axis. You can then subtract the area under the line,using standard integration, and this should give you the area enclosed.
Credit for the integration formula (image 3) and further info: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.
Inspired by James Godfrey-Kittle's suggestion in this bézierInfo thread: add section: area under a bézier curve I've wrapped this concept in a js helper function, that will get svg <path> and other elements' areas.
It's based on the same formula as suggested in #nbonneel's answer.
The main steps:
Parse and normalize a path's d attribute to an array of absolute and cubic commands. For this task, I'm using Jarek Foksa's path-data polyfill. The polyfill allows us to retrieve absolute coordinates from any path by its getPathData({normalize:true}) option. This way we don't have to bother about relative, cubic or shorthand commands.
Calculate the area for each curve segment (b0 and 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 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;
}
}
x0, y0 are the last coordinates of the command preceding the current C command. x1, y1, x2, y2, x3, y3 are the current pathdata values.
Since we don't need a polygon approximation based on the rather expensive getPointAtLength() method – the calculation is comparatively fast.
Add the remaining polygon's area to the bézier areas (p0). This step will also use the shoelace formula.
Example 1: semi circle with a radius of 50 (svg user units)
We can easily check, if the calculation works, since the expected result should be:
π·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>
Example 2: get areas of primitives and compound paths
For a more versatile helper function, we can include primitives like <circle>, <ellipse>, <polygon> etc. and skip the bézier calculation for these element types.
Compound paths – so shapes like the letters O or i will require to calculate the areas for each sub path. If a sub path is within the boundaries of another shape like the letter O, we also need to subtract inner shapes from the total area.
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 : '<' + type + '/> [' + 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 example
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.
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>

Categories

Resources