tl;dr summary: Give me the resources or help fix the below code to transform path commands for SVG <path> elements by an arbitrary matrix.
details:
I'm writing a library to convert any arbitrary SVG shape into a <path> element. I have it working when there are no transform="..." elements in the hierarchy, but now I want to bake the local transform of the object into the path data commands themselves.
This is mostly working (code below) when dealing with the simple moveto/lineto commands. However, I'm not sure of the appropriate way to transform the bezier handles or arcTo parameters.
For example, I am able to convert this rounded rectangle to a <path>:
<rect x="10" y="30" rx="10" ry="20" width="80" height="70" />
--> <path d="M20,30 L80,30 A10,20,0,0,1,90,50 L90,80 A10,20,0,0,1,80,100
L20,100 A10,20,0,0,1,10,80 L10,50 A10,20,0,0,1,20,30" />
And I get a valid result when transforming without any round corners:
<rect x="10" y="30" width="80" height="70"
transform="translate(-200,0) scale(1.5) rotate(50)" />
--> <path d="M10,30 L90,30 L90,100 L10,100 L10,30" />
However, transforming only the x/y coords of the elliptical arc commands yields amusing results:
The dotted line is the actual transformed rect, the green fill is my path.
Following is the code I have so far (slightly pared-down). I also have a test page where I'm testing various shapes. Please help me determine how to properly transform the elliptical arc and various other bezier commands given an arbitrary transformation matrix.
function flattenToPaths(el,transform,svg){
if (!svg) svg=el; while(svg && svg.tagName!='svg') svg=svg.parentNode;
var doc = el.ownerDocument;
var svgNS = svg.getAttribute('xmlns');
// Identity transform if nothing passed in
if (!transform) transform= svg.createSVGMatrix();
// Calculate local transform matrix for the object
var localMatrix = svg.createSVGMatrix();
for (var xs=el.transform.baseVal,i=xs.numberOfItems-1;i>=0;--i){
localMatrix = xs.getItem(i).matrix.multiply(localMatrix);
}
// Transform the local transform by whatever was recursively passed in
transform = transform.multiply(localMatrix);
var path = doc.createElementNS(svgNS,'path');
switch(el.tagName){
case 'rect':
path.setAttribute('stroke',el.getAttribute('stroke'));
var x = el.getAttribute('x')*1, y = el.getAttribute('y')*1,
w = el.getAttribute('width')*1, h = el.getAttribute('height')*1,
rx = el.getAttribute('rx')*1, ry = el.getAttribute('ry')*1;
if (rx && !el.hasAttribute('ry')) ry=rx;
else if (ry && !el.hasAttribute('rx')) rx=ry;
if (rx>w/2) rx=w/2;
if (ry>h/2) ry=h/2;
path.setAttribute('d',
'M'+(x+rx)+','+y+
'L'+(x+w-rx)+','+y+
((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+(x+w)+','+(y+ry)) : '') +
'L'+(x+w)+','+(y+h-ry)+
((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+(x+w-rx)+','+(y+h)) : '')+
'L'+(x+rx)+','+(y+h)+
((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+x+','+(y+h-ry)) : '')+
'L'+x+','+(y+ry)+
((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+(x+rx)+','+y) : '')
);
break;
case 'circle':
var cx = el.getAttribute('cx')*1, cy = el.getAttribute('cy')*1,
r = el.getAttribute('r')*1, r0 = r/2+','+r/2;
path.setAttribute('d','M'+cx+','+(cy-r)+' A'+r0+',0,0,0,'+cx+','+(cy+r)+' '+r0+',0,0,0,'+cx+','+(cy-r) );
break;
case 'ellipse':
var cx = el.getAttribute('cx')*1, cy = el.getAttribute('cy')*1,
rx = el.getAttribute('rx')*1, ry = el.getAttribute('ry')*1;
path.setAttribute('d','M'+cx+','+(cy-ry)+' A'+rx+','+ry+',0,0,0,'+cx+','+(cy+ry)+' '+rx+','+ry+',0,0,0,'+cx+','+(cy-ry) );
break;
case 'line':
var x1=el.getAttribute('x1')*1, y1=el.getAttribute('y1')*1,
x2=el.getAttribute('x2')*1, y2=el.getAttribute('y2')*1;
path.setAttribute('d','M'+x1+','+y1+'L'+x2+','+y2);
break;
case 'polyline':
case 'polygon':
for (var i=0,l=[],pts=el.points,len=pts.numberOfItems;i<len;++i){
var p = pts.getItem(i);
l[i] = p.x+','+p.y;
}
path.setAttribute('d',"M"+l.shift()+"L"+l.join(' ') + (el.tagName=='polygon') ? 'z' : '');
break;
case 'path':
path = el.cloneNode(false);
break;
}
// Convert local space by the transform matrix
var x,y;
var pt = svg.createSVGPoint();
var setXY = function(x,y,xN,yN){
pt.x = x; pt.y = y;
pt = pt.matrixTransform(transform);
if (xN) seg[xN] = pt.x;
if (yN) seg[yN] = pt.y;
};
// Extract rotation and scale from the transform
var rotation = Math.atan2(transform.b,transform.d)*180/Math.PI;
var sx = Math.sqrt(transform.a*transform.a+transform.c*transform.c);
var sy = Math.sqrt(transform.b*transform.b+transform.d*transform.d);
// FIXME: Must translate any Horizontal or Vertical lineto commands into absolute moveto
for (var segs=path.pathSegList,c=segs.numberOfItems,i=0;i<c;++i){
var seg = segs.getItem(i);
// Odd-numbered path segments are all relative
// http://www.w3.org/TR/SVG/paths.html#InterfaceSVGPathSeg
var isRelative = (seg.pathSegType%2==1);
var hasX = seg.x != null;
var hasY = seg.y != null;
if (hasX) x = isRelative ? x+seg.x : seg.x;
if (hasY) y = isRelative ? y+seg.y : seg.y;
if (hasX || hasY) setXY( x, y, hasX && 'x', hasY && 'y' );
if (seg.x1 != null) setXY( seg.x1, seg.y1, 'x1', 'y1' );
if (seg.x2 != null) setXY( seg.x2, seg.y2, 'x2', 'y2' );
if (seg.angle != null){
seg.angle += rotation;
seg.r1 *= sx; // FIXME; only works for uniform scale
seg.r2 *= sy; // FIXME; only works for uniform scale
}
}
return path;
}
I have made a general SVG flattener flatten.js, that supports all shapes and path commands:
https://gist.github.com/timo22345/9413158
Basic usage: flatten(document.getElementById('svg'));
What it does: Flattens elements (converts elements to paths and flattens transformations).
If the argument element (whose id is above 'svg') has children, or it's descendants has children,
these children elements are flattened also.
What can be flattened: entire SVG document, individual shapes (path, circle, ellipse etc.) and groups. Nested groups are handled automatically.
How about attributes? All attributes are copied. Only arguments that are not valid in path element, are dropped (eg. r, rx, ry, cx, cy), but they are not needed anymore. Also transform attribute is dropped, because transformations are flattened to path commands.
If you want to modify path coordinates using non-affine methods (eg. perspective distort),
you can convert all segments to cubic curves using:
flatten(document.getElementById('svg'), true);
There are also arguments 'toAbsolute' (convert coordinates to absolute) and 'dec',
number of digits after decimal separator.
Extreme path and shape tester: https://jsfiddle.net/fjm9423q/embedded/result/
Basic usage example: http://jsfiddle.net/nrjvmqur/embedded/result/
CONS: text element is not working. It could be my next goal.
If every object (circles etc) are converted first to paths, then taking transforms into account is rather easy. I made a testbed ( http://jsbin.com/oqojan/73 ) where you can test the functionality. The testbed creates random path commands and applies random transforms to paths and then flattens transforms. Of course in reality the path commands and transforms are not random, but for testing accuracy it is fine.
There is a function flatten_transformations(), which makes the main task:
function flatten_transformations(path_elem, normalize_path, to_relative, dec) {
// Rounding coordinates to dec decimals
if (dec || dec === 0) {
if (dec > 15) dec = 15;
else if (dec < 0) dec = 0;
}
else dec = false;
function r(num) {
if (dec !== false) return Math.round(num * Math.pow(10, dec)) / Math.pow(10, dec);
else return num;
}
// For arc parameter rounding
var arc_dec = (dec !== false) ? 6 : false;
arc_dec = (dec && dec > 6) ? dec : arc_dec;
function ra(num) {
if (arc_dec !== false) return Math.round(num * Math.pow(10, arc_dec)) / Math.pow(10, arc_dec);
else return num;
}
var arr;
//var pathDOM = path_elem.node;
var pathDOM = path_elem;
var d = pathDOM.getAttribute("d").trim();
// If you want to retain current path commans, set normalize_path to false
if (!normalize_path) { // Set to false to prevent possible re-normalization.
arr = Raphael.parsePathString(d); // str to array
arr = Raphael._pathToAbsolute(arr); // mahvstcsqz -> uppercase
}
// If you want to modify path data using nonAffine methods,
// set normalize_path to true
else arr = Raphael.path2curve(d); // mahvstcsqz -> MC
var svgDOM = pathDOM.ownerSVGElement;
// Get the relation matrix that converts path coordinates
// to SVGroot's coordinate space
var matrix = pathDOM.getTransformToElement(svgDOM);
// The following code can bake transformations
// both normalized and non-normalized data
// Coordinates have to be Absolute in the following
var i = 0,
j, m = arr.length,
letter = "",
x = 0,
y = 0,
point, newcoords = [],
pt = svgDOM.createSVGPoint(),
subpath_start = {};
subpath_start.x = "";
subpath_start.y = "";
for (; i < m; i++) {
letter = arr[i][0].toUpperCase();
newcoords[i] = [];
newcoords[i][0] = arr[i][0];
if (letter == "A") {
x = arr[i][6];
y = arr[i][7];
pt.x = arr[i][6];
pt.y = arr[i][7];
newcoords[i] = arc_transform(arr[i][4], arr[i][5], arr[i][6], arr[i][4], arr[i][5], pt, matrix);
// rounding arc parameters
// x,y are rounded normally
// other parameters at least to 5 decimals
// because they affect more than x,y rounding
newcoords[i][7] = ra(newcoords[i][8]); //rx
newcoords[i][9] = ra(newcoords[i][10]); //ry
newcoords[i][11] = ra(newcoords[i][12]); //x-axis-rotation
newcoords[i][6] = r(newcoords[i][6]); //x
newcoords[i][7] = r(newcoords[i][7]); //y
}
else if (letter != "Z") {
// parse other segs than Z and A
for (j = 1; j < arr[i].length; j = j + 2) {
if (letter == "V") y = arr[i][j];
else if (letter == "H") x = arr[i][j];
else {
x = arr[i][j];
y = arr[i][j + 1];
}
pt.x = x;
pt.y = y;
point = pt.matrixTransform(matrix);
newcoords[i][j] = r(point.x);
newcoords[i][j + 1] = r(point.y);
}
}
if ((letter != "Z" && subpath_start.x == "") || letter == "M") {
subpath_start.x = x;
subpath_start.y = y;
}
if (letter == "Z") {
x = subpath_start.x;
y = subpath_start.y;
}
if (letter == "V" || letter == "H") newcoords[i][0] = "L";
}
if (to_relative) newcoords = Raphael.pathToRelative(newcoords);
newcoords = newcoords.flatten().join(" ").replace(/\s*([A-Z])\s*/gi, "$1").replace(/\s*([-])/gi, "$1");
return newcoords;
} // function flatten_transformations
// Helper tool to piece together Raphael's paths into strings again
Array.prototype.flatten || (Array.prototype.flatten = function() {
return this.reduce(function(a, b) {
return a.concat('function' === typeof b.flatten ? b.flatten() : b);
}, []);
});
The code uses Raphael.pathToRelative(), Raphael._pathToAbsolute() and Raphael.path2curve(). The Raphael.path2curve() is bugfixed version.
If flatten_transformations() is called using argument normalize_path=true, then all commands are converted to Cubics and everything is fine. And the code can be simplified by removing if (letter == "A") { ... } and also removing handling of H, V and Z. The simplified version can be something like this.
But because someone may want to only bake transformations and not to make All Segs -> Cubics normalization, I added there a possibility to this. So, if you want to flatten transformations with normalize_path=false, this means that Elliptical Arc parameters have to be flattened also and it's not possible to handle them by simply applying matrix to coordinates. Two radiis (rx ry), x-axis-rotation, large-arc-flag and sweep-flag have to handle separately. So the following function can flatten transformations of Arcs. The matrix parameter is a relation matrix which comes from is used already in flatten_transformations().
// Origin: http://devmaster.net/forums/topic/4947-transforming-an-ellipse/
function arc_transform(a_rh, a_rv, a_offsetrot, large_arc_flag, sweep_flag, endpoint, matrix, svgDOM) {
function NEARZERO(B) {
if (Math.abs(B) < 0.0000000000000001) return true;
else return false;
}
var rh, rv, rot;
var m = []; // matrix representation of transformed ellipse
var s, c; // sin and cos helpers (the former offset rotation)
var A, B, C; // ellipse implicit equation:
var ac, A2, C2; // helpers for angle and halfaxis-extraction.
rh = a_rh;
rv = a_rv;
a_offsetrot = a_offsetrot * (Math.PI / 180); // deg->rad
rot = a_offsetrot;
s = parseFloat(Math.sin(rot));
c = parseFloat(Math.cos(rot));
// build ellipse representation matrix (unit circle transformation).
// the 2x2 matrix multiplication with the upper 2x2 of a_mat is inlined.
m[0] = matrix.a * +rh * c + matrix.c * rh * s;
m[1] = matrix.b * +rh * c + matrix.d * rh * s;
m[2] = matrix.a * -rv * s + matrix.c * rv * c;
m[3] = matrix.b * -rv * s + matrix.d * rv * c;
// to implict equation (centered)
A = (m[0] * m[0]) + (m[2] * m[2]);
C = (m[1] * m[1]) + (m[3] * m[3]);
B = (m[0] * m[1] + m[2] * m[3]) * 2.0;
// precalculate distance A to C
ac = A - C;
// convert implicit equation to angle and halfaxis:
if (NEARZERO(B)) {
a_offsetrot = 0;
A2 = A;
C2 = C;
} else {
if (NEARZERO(ac)) {
A2 = A + B * 0.5;
C2 = A - B * 0.5;
a_offsetrot = Math.PI / 4.0;
} else {
// Precalculate radical:
var K = 1 + B * B / (ac * ac);
// Clamp (precision issues might need this.. not likely, but better save than sorry)
if (K < 0) K = 0;
else K = Math.sqrt(K);
A2 = 0.5 * (A + C + K * ac);
C2 = 0.5 * (A + C - K * ac);
a_offsetrot = 0.5 * Math.atan2(B, ac);
}
}
// This can get slightly below zero due to rounding issues.
// it's save to clamp to zero in this case (this yields a zero length halfaxis)
if (A2 < 0) A2 = 0;
else A2 = Math.sqrt(A2);
if (C2 < 0) C2 = 0;
else C2 = Math.sqrt(C2);
// now A2 and C2 are half-axis:
if (ac <= 0) {
a_rv = A2;
a_rh = C2;
} else {
a_rv = C2;
a_rh = A2;
}
// If the transformation matrix contain a mirror-component
// winding order of the ellise needs to be changed.
if ((matrix.a * matrix.d) - (matrix.b * matrix.c) < 0) {
if (!sweep_flag) sweep_flag = 1;
else sweep_flag = 0;
}
// Finally, transform arc endpoint. This takes care about the
// translational part which we ignored at the whole math-showdown above.
endpoint = endpoint.matrixTransform(matrix);
// Radians back to degrees
a_offsetrot = a_offsetrot * 180 / Math.PI;
var r = ["A", a_rh, a_rv, a_offsetrot, large_arc_flag, sweep_flag, endpoint.x, endpoint.y];
return r;
}
OLD EXAMPLE:
I made an example that has a path with segments M Q A A Q M, which has transformations applied. The path is inside g that also has trans applied. And to make very sure this g is inside another g which has different transformations applied. And the code can:
A) First normalize those all path segments (thanks to Raphaël's path2curve, to which I made a bug fix, and after this fix all possible path segment combinations worked finally: http://jsbin.com/oqojan/42. The original Raphaël 2.1.0 has buggy behavior as you can see here, if not click paths few times to generate new curves.)
B) Then flatten transformations using native functions getTransformToElement(), createSVGPoint() and matrixTransform().
The only one that lacks is the way to convert Circles, Rectangles and Polygons to path commands, but as far as I know, you have an excellent code for it.
This is an updated log of any forward progress I am making as an 'answer', to help inform others; if I somehow solve the problem on my own, I'll just accept this.
Update 1: I've got the absolute arcto command working perfectly except in cases of non-uniform scale. Here were the additions:
// Extract rotation and scale from the transform
var rotation = Math.atan2(transform.b,transform.d)*180/Math.PI;
var sx = Math.sqrt(transform.a*transform.a+transform.c*transform.c);
var sy = Math.sqrt(transform.b*transform.b+transform.d*transform.d);
//inside the processing of segments
if (seg.angle != null){
seg.angle += rotation;
// FIXME; only works for uniform scale
seg.r1 *= sx;
seg.r2 *= sy;
}
Thanks to this answer for a simpler extraction method than I was using, and for the math for extracting non-uniform scale.
As long as you translate all coordinates to absolute coordinates, all béziers will work just fine; there is nothing magical about the their handles. As for the elliptical arc commands, the only general solution (handling non-uniform scaling, as you point out, which the arc command can not represent, in the general case) is to first convert them to their bézier approximations.
https://github.com/johan/svg-js-utils/blob/df605f3e21cc7fcd2d604eb318fb2466fd6d63a7/paths.js#L56..L113 (uses absolutizePath in the same file, a straight port of your Convert SVG Path to Absolute Commands hack) does the former, but not yet the latter.
How to best approximate a geometrical arc with a Bezier curve? links the math for converting arcs to béziers (one bézier segment per 0 < α <= π/2 arc segment); this paper shows the equations at the end of the page (its prettier pdf rendition has it at the end of section 3.4.1).
Related
I'm trying to create an algorithm that detects discontinuities (like vertical asymptotes) within functions between an interval for the purpose of plotting graphs without these discontinuous connecting lines. Also, I only want to evaluate within the interval so bracketing methods like bisection seems good for that.
EDIT
https://en.wikipedia.org/wiki/Classification_of_discontinuities
I realize now there are a few different kinds of discontinuities. I'm mostly interested in jump discontinuities for graphical purposes.
I'm using a bisection method as I've noticed that discontinuities occur where the slope tends to infinity or becomes vertical, so why not narrow in on those sections where the slope keeps increasing and getting steeper and steeper. The point where the slope is a vertical line, that's where the discontinuity exists.
Approach
Currently, my approach is as follows. If you subdivide the interval using a midpoint into 2 sections and compare which section has the steepest slope, then that section with the steepest slope becomes the new subinterval for the next evaluation.
Termination
This repeats until it converges by either slope becoming undefined (reaching infinity) or the left side or the right side of the interval equaling the middle (I think this is because the floating-point decimal runs out of precision and cannot divide any further)
(1.5707963267948966 + 1.5707963267948968) * .5 = 1.5707963267948966
Example
function - floor(x)
(blue = start leftX and rightX, purple = midpoint, green = 2nd iteration midpoints points, red = slope lines per iteration)
As you can see from the image, each bisection narrows into the discontinuity and the slope keeps getting steeper until it becomes a vertical line at the discontinuity point at x=1.
To my surprise this approach seems to work for step functions like floor(x) and tan(x), but it's not that great for 1/x as it takes too many iterations (I'm thinking of creating a hybrid method where I use either illinois or ridders method on the inverse of 1/x as it those tend to find the root in just one iteration).
Javascript Code
/* Math function to test on */
function fn(x) {
//return (Math.pow(Math.tan(x), 3));
return 1/x;
//return Math.floor(x);
//return x*((x-1-0.001)/(x-1));
}
function slope(x1, y1, x2, y2) {
return (y2 - y1) / (x2 - x1);
}
function findDiscontinuity(leftX, rightX, fn) {
while (true) {
let leftY = fn(leftX);
let rightY = fn(rightX);
let middleX = (leftX + rightX) / 2;
let middleY = fn(middleX);
let leftSlope = Math.abs(slope(leftX, leftY, middleX, middleY));
let rightSlope = Math.abs(slope(middleX, middleY, rightX, rightY));
if (!isFinite(leftSlope) || !isFinite(rightSlope)) return middleX;
if (middleX === leftX || middleX === rightX) return middleX;
if (leftSlope > rightSlope) {
rightX = middleX;
rightY = middleY;
} else {
leftX = middleX;
leftY = middleY;
}
}
}
Problem 1 - Improving detection
For the function x*((x-1-0.001)/(x-1)), the current algorithm has a hard time detecting the discontinuity at x=1 unless I make the interval really small. As an alternative, I could also add most subdivisions but I think the real problem is using slopes as they trick the algorithm into choosing the incorrect subinterval (as demonstrated in the image below), so this approach is not robust enough. Maybe there are some statistical methods that can help determine a more probable interval to select. Maybe something like least squares for measuring the differences and maybe applying weights or biases!
But I don't want the calculations to get too heavy and 5 points of evaluation are the max I would go with per iteration.
EDIT
After looking at problem 1 again, where it selects the wrong (left-hand side) subinterval. I noticed that the only difference between the subintervals was the green midpoint distance from their slope line. So taking inspiration from linear regression, I get the squared distance from the slope line to the midpoints [a, fa] and [b, fb] corresponding to their (left/right) subintervals. And which subinterval has the greatest change/deviation is the one chosen for further subdivision, that is, the greater of the two residuals.
This further improvement resolves problem 1. Although, it now takes around 593 iterations to find the discontinuity for 1/x. So I've created a hybrid function that uses ridders method to find the roots quicker for some functions and then fallback to this new approach. I have given up on slopes as they don't provide enough accurate information.
Problem 2 - Jump Threshold
I'm not sure how to incorporate a jump threshold and what to use for that calculation, don't think slopes would help.
Also, if the line thickness for the graph is 2px and 2 lines of a step function were on top of each other then you wouldn't be able to see the gap of 2px between those lines. So the minimum jump gap would be calculated as such
jumpThreshold = height / (ymax-ymin) = cartesian distance per pixel
minJumpGap = jumpThreshold * 2
But I don't know where to go from here! And once again, maybe there are statistical methods that can help to determine the change in function so that the algorithm can terminate quickly if there's no indication of a discontinuity.
Overall, any help or advice in improving what I got already would be much appreciated!
EDIT
As the above images explains, the more divergent the midpoints are the greater the need for more subdivisions for further inspection for that subinterval. While, if the points mostly follow a straight line trend where the midpoints barely deviate then should exit early. So now it makes sense to use the jumpThreshold in this context.
Maybe there's further analysis that could be done like measuring the curvature of the points in the interval to see whether to terminate early and further optimize this method. Zig zag points or sudden dips would be the most promising. And maybe after a certain number of intervals, keep widening the jumpThreshold as for a discontinuity you expect the residual distance to rapidly increase towards infinity!
Updated code
let ymax = 5, ymin = -5; /* just for example */
let height = 500; /* 500px screen height */
let jumpThreshold = Math.pow(.5 * (ymax - ymin) / height, 2); /* fraction(half) of a pixel! */
/* Math function to test on */
function fn(x) {
//return (Math.pow(Math.tan(x), 3));
return 1 / x;
//return Math.floor(x);
//return x * ((x - 1 - 0.001) / (x - 1));
//return x*x;
}
function findDiscontinuity(leftX, rightX, jumpThreshold, fn) {
/* try 5 interations of ridders method */
/* usually this approach can find the exact reciprocal root of a discountinuity
* in 1 iteration for functions like 1/x compared to the bisection method below */
let iterations = 5;
let root = inverseRidderMethod(leftX, rightX, iterations, fn);
let limit = fn(root);
if (Math.abs(limit) > 1e+16) {
if (root >= leftX && root <= rightX) return root;
return NaN;
}
root = discontinuityBisection(leftX, rightX, jumpThreshold, fn);
return root;
}
function discontinuityBisection(leftX, rightX, jumpThreshold, fn) {
while (true) {
let leftY = fn(leftX);
let rightY = fn(rightX);
let middleX = (leftX + rightX) * .5;
let middleY = fn(middleX);
let a = (leftX + middleX) * .5;
let fa = fn(a);
let b = (middleX + rightX) * .5;
let fb = fn(b);
let leftResidual = Math.pow(fa - (leftY + middleY) * .5, 2);
let rightResidual = Math.pow(fb - (middleY + rightY) * .5, 2);
/* if both subinterval midpoints (fa,fb) barely deviate from their slope lines
* i.e. they're under the jumpThreshold, then return NaN,
* indicating no discountinuity with the current threshold,
* both subintervals are mostly straight */
if (leftResidual < jumpThreshold && rightResidual < jumpThreshold) return NaN;
if (!isFinite(fa) || a === leftX || a === middleX) return a;
if (!isFinite(fb) || b === middleX || b === rightX) return b;
if (leftResidual > rightResidual) {
/* left hand-side subinterval */
rightX = middleX;
middleX = a;
} else {
/* right hand-side subinterval */
leftX = middleX;
middleX = b;
}
}
}
function inverseRidderMethod(min, max, iterations, fn) {
/* Modified version of RiddersSolver from Apache Commons Math
* http://commons.apache.org/
* https://www.apache.org/licenses/LICENSE-2.0.txt
*/
let x1 = min;
let y1 = 1 / fn(x1);
let x2 = max;
let y2 = 1 / fn(x2);
// check for zeros before verifying bracketing
if (y1 == 0) {
return min;
}
if (y2 == 0) {
return max;
}
let functionValueAccuracy = 1e-55;
let relativeAccuracy = 1e-16;
let oldx = Number.POSITIVE_INFINITY;
let i = 0;
while (i < iterations) {
// calculate the new root approximation
let x3 = 0.5 * (x1 + x2);
let y3 = 1 / fn(x3);
if (!isFinite(y3)) return NaN;
if (Math.abs(y3) <= functionValueAccuracy) {
return x3;
}
let delta = 1 - (y1 * y2) / (y3 * y3); // delta > 1 due to bracketing
let correction = (signum(y2) * signum(y3)) * (x3 - x1) / Math.sqrt(delta);
let x = x3 - correction; // correction != 0
if (!isFinite(x)) return NaN;
let y = 1 / fn(x);
// check for convergence
let tolerance = Math.max(relativeAccuracy * Math.abs(x), 1e-16);
if (Math.abs(x - oldx) <= tolerance) {
return x;
}
if (Math.abs(y) <= functionValueAccuracy) {
return x;
}
// prepare the new interval for the next iteration
// Ridders' method guarantees x1 < x < x2
if (correction > 0.0) { // x1 < x < x3
if (signum(y1) + signum(y) == 0.0) {
x2 = x;
y2 = y;
} else {
x1 = x;
x2 = x3;
y1 = y;
y2 = y3;
}
} else { // x3 < x < x2
if (signum(y2) + signum(y) == 0.0) {
x1 = x;
y1 = y;
} else {
x1 = x3;
x2 = x;
y1 = y3;
y2 = y;
}
}
oldx = x;
}
}
function signum(a) {
return (a < 0.0) ? -1.0 : ((a > 0.0) ? 1.0 : a);
}
/* TEST */
console.log(findDiscontinuity(.5, .6, jumpThreshold, fn));
Python Code
I don't mind if the solution is provided in Javascript or Python
import math
def fn(x):
try:
# return (math.pow(math.tan(x), 3))
# return 1 / x
# return math.floor(x)
return x * ((x - 1 - 0.001) / (x - 1))
except ZeroDivisionError:
return float('Inf')
def slope(x1, y1, x2, y2):
try:
return (y2 - y1) / (x2 - x1)
except ZeroDivisionError:
return float('Inf')
def find_discontinuity(leftX, rightX, fn):
while True:
leftY = fn(leftX)
rightY = fn(rightX)
middleX = (leftX + rightX) / 2
middleY = fn(middleX)
leftSlope = abs(slope(leftX, leftY, middleX, middleY))
rightSlope = abs(slope(middleX, middleY, rightX, rightY))
if not math.isfinite(leftSlope) or not math.isfinite(rightSlope):
return middleX
if middleX == leftX or middleX == rightX:
return middleX
if leftSlope > rightSlope:
rightX = middleX
rightY = middleY
else:
leftX = middleX
leftY = middleY
Summary
This question is in JavaScript, but an answer in any language, pseudo-code, or just the maths would be great!
I have been trying to implement the Separating-Axis-Theorem to accomplish the following:
Detecting an intersection between a convex polygon and a circle.
Finding out a translation that can be applied to the circle to resolve the intersection, so that the circle is barely touching the polygon but no longer inside.
Determining the axis of the collision (details at the end of the question).
I have successfully completed the first bullet point and you can see my javascript code at the end of the question. I am having difficulties with the other parts.
Resolving the intersection
There are plenty of examples online on how to resolve the intersection in the direction with the smallest/shortest overlap of the circle. You can see in my code at the end that I already have this calculated.
However this does not suit my needs. I must resolve the collision in the opposite direction of the circle's trajectory (assume I already have the circle's trajectory and would like to pass it into my function as a unit-vector or angle, whichever suits).
You can see the difference between the shortest resolution and the intended resolution in the below image:
How can I calculate the minimum translation vector for resolving the intersection inside my test_CIRCLE_POLY function, but that is to be applied in a specific direction, the opposite of the circle's trajectory?
My ideas/attempts:
My first idea was to add an additional axis to the axes that must be tested in the SAT algorithm, and this axis would be perpendicular to the circle's trajectory. I would then resolve based on the overlap when projecting onto this axis. This would sort of work, but would resolve way to far in most situations. It won't result in the minimum translation. So this won't be satisfactory.
My second idea was to continue to use magnitude of the shortest overlap, but change the direction to be the opposite of the circle's trajectory. This looks promising, but there are probably many edge-cases that I haven't accounted for. Maybe this is a nice place to start.
Determining side/axis of collision
I've figured out a way to determine which sides of the polygon the circle is colliding with. For each tested axis of the polygon, I would simply check for overlap. If there is overlap, that side is colliding.
This solution will not be acceptable once again, as I would like to figure out only one side depending on the circle's trajectory.
My intended solution would tell me, in the example image below, that axis A is the axis of collision, and not axis B. This is because once the intersection is resolved, axis A is the axis corresponding to the side of the polygon that is just barely touching the circle.
My ideas/attempts:
Currently I assume the axis of collision is that perpendicular to the MTV (minimum translation vector). This is currently incorrect, but should be the correct axis once I've updated the intersection resolution process in the first half of the question. So that part should be tackled first.
Alternatively I've considered creating a line from the circle's previous position and their current position + radius, and checking which sides intersect with this line. However, there's still ambiguity, because on occasion there will be more than one side intersecting with the line.
My code so far
function test_CIRCLE_POLY(circle, poly, circleTrajectory) {
// circleTrajectory is currently not being used
let axesToTest = [];
let shortestOverlap = +Infinity;
let shortestOverlapAxis;
// Figure out polygon axes that must be checked
for (let i = 0; i < poly.vertices.length; i++) {
let vertex1 = poly.vertices[i];
let vertex2 = poly.vertices[i + 1] || poly.vertices[0]; // neighbouring vertex
let axis = vertex1.sub(vertex2).perp_norm();
axesToTest.push(axis);
}
// Figure out circle axis that must be checked
let closestVertex;
let closestVertexDistSqr = +Infinity;
for (let vertex of poly.vertices) {
let distSqr = circle.center.sub(vertex).magSqr();
if (distSqr < closestVertexDistSqr) {
closestVertexDistSqr = distSqr;
closestVertex = vertex;
}
}
let axis = closestVertex.sub(circle.center).norm();
axesToTest.push(axis);
// Test for overlap
for (let axis of axesToTest) {
let circleProj = proj_CIRCLE(circle, axis);
let polyProj = proj_POLY(poly, axis);
let overlap = getLineOverlap(circleProj.min, circleProj.max, polyProj.min, polyProj.max);
if (overlap === 0) {
// guaranteed no intersection
return { intersecting: false };
}
if (Math.abs(overlap) < Math.abs(shortestOverlap)) {
shortestOverlap = overlap;
shortestOverlapAxis = axis;
}
}
return {
intersecting: true,
resolutionVector: shortestOverlapAxis.mul(-shortestOverlap),
// this resolution vector is not satisfactory, I need the shortest resolution with a given direction, which would be an angle passed into this function from the trajectory of the circle
collisionAxis: shortestOverlapAxis.perp(),
// this axis is incorrect, I need the axis to be based on the trajectory of the circle which I would pass into this function as an angle
};
}
function proj_POLY(poly, axis) {
let min = +Infinity;
let max = -Infinity;
for (let vertex of poly.vertices) {
let proj = vertex.projNorm_mag(axis);
min = Math.min(proj, min);
max = Math.max(proj, max);
}
return { min, max };
}
function proj_CIRCLE(circle, axis) {
let proj = circle.center.projNorm_mag(axis);
let min = proj - circle.radius;
let max = proj + circle.radius;
return { min, max };
}
// Check for overlap of two 1 dimensional lines
function getLineOverlap(min1, max1, min2, max2) {
let min = Math.max(min1, min2);
let max = Math.min(max1, max2);
// if negative, no overlap
let result = Math.max(max - min, 0);
// add positive/negative sign depending on direction of overlap
return result * ((min1 < min2) ? 1 : -1);
};
I am assuming the polygon is convex and that the circle is moving along a straight line (at least for a some possibly small interval of time) and is not following some curved trajectory. If it is following a curved trajectory, then things get harder. In the case of curved trajectories, the basic ideas could be kept, but the actual point of collision (the point of collision resolution for the circle) might be harder to calculate. Still, I am outlining an idea, which could be extended to that case too. Plus, it could be adopted as a main approach for collision detection between a circle and a convex polygon.
I have not considered all possible cases, which may include special or extreme situations, but at least it gives you a direction to explore.
Transform in your mind the collision between the circle and the polygon into a collision between the center of the circle (a point) and a version of the polygon thickened by the circle's radius r, i.e. (i) each edge of the polygon is offset (translated) outwards by radius r along a vector perpendicular to it and pointing outside of the polygon, (ii) the vertices become circular arcs of radius r, centered at the polygons vertices and connecting the endpoints of the appropriate neighboring offset edges (basically, put circles of radius r at the vertices of the polygon and take their convex hull).
Now, the current position of the circle's center is C = [ C[0], C[1] ] and it has been moving along a straight line with direction vector V = [ V[0], V[1] ] pointing along the direction of motion (or if you prefer, think of V as the velocity of the circle at the moment when you have detected the collision). Then, there is an axis (or let's say a ray - a directed half-line) defined by the vector equation X = C - t * V, where t >= 0 (this axis is pointing to the past trajectory). Basically, this is the half-line that passes through the center point C and is aligned with/parallel to the vector V. Now, the point of resolution, i.e. the point where you want to move your circle to is the point where the axis X = C - t * V intersects the boundary of the thickened polygon.
So you have to check (1) first axis intersection for edges and then (2) axis intersection with circular arcs pertaining to the vertices of the original polygon.
Assume the polygon is given by an array of vertices P = [ P[0], P[1], ..., P[N], P[0] ] oriented counterclockwise.
(1) For each edge P[i-1]P[i] of the original polygon, relevant to your collision (these could be the two neighboring edges meeting at the vertex based on which the collision is detected, or it could be actually all edges in the case of the circle moving with very high speed and you have detected the collision very late, so that the actual collision did not even happen there, I leave this up to you, because you know better the details of your situation) do the following. You have as input data:
C = [ C[0], C[1] ]
V = [ V[0], V[1] ]
P[i-1] = [ P[i-1][0], P[i-1][1] ]
P[i] = [ P[i][0], P[i][1] ]
Do:
Normal = [ P[i-1][1] - P[i][1], P[i][0] - P[i-1][0] ];
Normal = Normal / sqrt((P[i-1][1] - P[i][1])^2 + ( P[i][0] - P[i-1][0] )^2);
// you may have calculated these already
Q_0[0] = P[i-1][0] + r*Normal[0];
Q_0[1] = P[i-1][1] + r*Normal[1];
Q_1[0] = P[i][0]+ r*Normal[0];
Q_1[1] = P[i][1]+ r*Normal[1];
Solve for s, t the linear system of equations (the equation for intersecting ):
Q_0[0] + s*(Q_1[0] - Q_0[0]) = C[0] - t*V[0];
Q_0[1] + s*(Q_1[1] - Q_0[1]) = C[1] - t*V[1];
if 0<= s <= 1 and t >= 0, you are done, and your point of resolution is
R[0] = C[0] - t*V[0];
R[1] = C[1] - t*V[1];
else
(2) For the each vertex P[i] relevant to your collision, do the following:
solve for t the quadratic equation (there is an explicit formula)
norm(P[i] - C + t*V )^2 = r^2
or expanded:
(V[0]^2 + V[1]^2) * t^2 + 2 * ( (P[i][0] - C[0])*V[0] + (P[i][1] - C[1])*V[1] )*t + ( P[i][0] - C[0])^2 + (P[i][1] - C[1])^2 ) - r^2 = 0
or if you prefer in a more code-like way:
a = V[0]^2 + V[1]^2;
b = (P[i][0] - C[0])*V[0] + (P[i][1] - C[1])*V[1];
c = (P[i][0] - C[0])^2 + (P[i][1] - C[1])^2 - r^2;
D = b^2 - a*c;
if D < 0 there is no collision with the vertex
i.e. no intersection between the line X = C - t*V
and the circle of radius r centered at P[i]
else
D = sqrt(D);
t1 = ( - b - D) / a;
t2 = ( - b + D) / a;
where t2 >= t1
Then your point of resolution is
R[0] = C[0] - t2*V[0];
R[1] = C[1] - t2*V[1];
Circle polygon intercept
If the ball is moving and if you can ensure that the ball always starts outside the polygon then the solution is rather simple.
We will call the ball and its movement the ball line. It starts at the ball's current location and end at the position the ball will be at the next frame.
To solve you find the nearest intercept to the start of the ball line.
There are two types of intercept.
Line segment (ball line) with Line segment (polygon edge)
Line segment (ball line) with circle (circle at each (convex only) polygon corner)
The example code has a Lines2 object that contains the two relevant intercept functions. The intercepts are returned as a Vec2containing two unit distances. The intercept functions are for lines (infinite length) not line sgements. If there is no intercept then the return is undefined.
For the line intercepts Line2.unitInterceptsLine(line, result = new Vec2()) the unit values (in result) are the unit distance along each line from the start. negative values are behind the start.
To take in account of the ball radius each polygon edge is offset the ball radius along its normal. It is important that the polygon edges have a consistent direction. In the example the normal is to the right of the line and the polygon points are in a clockwise direction.
For the line segment / circle intercepts Line2.unitInterceptsCircle(center, radius, result = new Vec2()) the unit values (in result) are the unit distance along the line where it intercepts the circle. result.x will always contain the closest intercept (assuming you start outside the circle). If there is an intercept there ways always be two, even if they are at the same point.
Example
The example contains all that is needed
The objects of interest are ball and poly
ball defines the ball and its movement. There is also code to draw it for the example
poly holds the points of the polygon. Converts the points to offset lines depending on the ball radius. It is optimized to that it only calculates the lines if the ball radius changes.
The function poly.movingBallIntercept is the function that does all the work. It take a ball object and an optional results vector.
It returns the position as a Vec2 of the ball if it contacts the polygon.
It does this by finding the smallest unit distance to the offset lines, and point (as circle) and uses that unit distance to position the result.
Note that if the ball is inside the polygon the intercepts with the corners is reversed. The function Line2.unitInterceptsCircle does provide 2 unit distance where the line enters and exits the circle. However you need to know if you are inside or outside to know which one to use. The example assumes you are outside the polygon.
Instructions
Move the mouse to change the balls path.
Click to set the balls starting position.
Math.EPSILON = 1e-6;
Math.isSmall = val => Math.abs(val) < Math.EPSILON;
Math.isUnit = u => !(u < 0 || u > 1);
Math.TAU = Math.PI * 2;
/* export {Vec2, Line2} */ // this should be a module
var temp;
function Vec2(x = 0, y = (temp = x, x === 0 ? (x = 0 , 0) : (x = x.x, temp.y))) {
this.x = x;
this.y = y;
}
Vec2.prototype = {
init(x, y = (temp = x, x = x.x, temp.y)) { this.x = x; this.y = y; return this }, // assumes x is a Vec2 if y is undefined
copy() { return new Vec2(this) },
equal(v) { return (this.x - v.x) === 0 && (this.y - v.y) === 0 },
isUnits() { return Math.isUnit(this.x) && Math.isUnit(this.y) },
add(v, res = this) { res.x = this.x + v.x; res.y = this.y + v.y; return res },
sub(v, res = this) { res.x = this.x - v.x; res.y = this.y - v.y; return res },
scale(val, res = this) { res.x = this.x * val; res.y = this.y * val; return res },
invScale(val, res = this) { res.x = this.x / val; res.y = this.y / val; return res },
dot(v) { return this.x * v.x + this.y * v.y },
uDot(v, div) { return (this.x * v.x + this.y * v.y) / div },
cross(v) { return this.x * v.y - this.y * v.x },
uCross(v, div) { return (this.x * v.y - this.y * v.x) / div },
get length() { return this.lengthSqr ** 0.5 },
set length(l) { this.scale(l / this.length) },
get lengthSqr() { return this.x * this.x + this.y * this.y },
rot90CW(res = this) {
const y = this.x;
res.x = -this.y;
res.y = y;
return res;
},
};
const wV1 = new Vec2(), wV2 = new Vec2(), wV3 = new Vec2(); // pre allocated work vectors used by Line2 functions
function Line2(p1 = new Vec2(), p2 = (temp = p1, p1 = p1.p1 ? p1.p1 : p1, temp.p2 ? temp.p2 : new Vec2())) {
this.p1 = p1;
this.p2 = p2;
}
Line2.prototype = {
init(p1, p2 = (temp = p1, p1 = p1.p1, temp.p2)) { this.p1.init(p1); this.p2.init(p2) },
copy() { return new Line2(this) },
asVec(res = new Vec2()) { return this.p2.sub(this.p1, res) },
unitDistOn(u, res = new Vec2()) { return this.p2.sub(this.p1, res).scale(u).add(this.p1) },
translate(vec, res = this) {
this.p1.add(vec, res.p1);
this.p2.add(vec, res.p2);
return res;
},
translateNormal(amount, res = this) {
this.asVec(wV1).rot90CW().length = -amount;
this.translate(wV1, res);
return res;
},
unitInterceptsLine(line, res = new Vec2()) { // segments
this.asVec(wV1);
line.asVec(wV2);
const c = wV1.cross(wV2);
if (Math.isSmall(c)) { return }
wV3.init(this.p1).sub(line.p1);
res.init(wV1.uCross(wV3, c), wV2.uCross(wV3, c));
return res;
},
unitInterceptsCircle(point, radius, res = new Vec2()) {
this.asVec(wV1);
var b = -2 * this.p1.sub(point, wV2).dot(wV1);
const c = 2 * wV1.lengthSqr;
const d = (b * b - 2 * c * (wV2.lengthSqr - radius * radius)) ** 0.5
if (isNaN(d)) { return }
return res.init((b - d) / c, (b + d) / c);
},
};
/* END of file */ // Vec2 and Line2 module
/* import {vec2, Line2} from "whateverfilename.jsm" */ // Should import vec2 and line2
const POLY_SCALE = 0.5;
const ball = {
pos: new Vec2(-150,0),
delta: new Vec2(10, 10),
radius: 20,
drawPath(ctx) {
ctx.beginPath();
ctx.arc(this.pos.x, this.pos.y, this.radius, 0, Math.TAU);
ctx.stroke();
},
}
const poly = {
bRadius: 0,
lines: [],
set ballRadius(radius) {
const len = this.points.length
this.bRadius = ball.radius;
i = 0;
while (i < len) {
let line = this.lines[i];
if (line) { line.init(this.points[i], this.points[(i + 1) % len]) }
else { line = new Line2(new Vec2(this.points[i]), new Vec2(this.points[(i + 1) % len])) }
this.lines[i++] = line.translateNormal(radius);
}
this.lines.length = i;
},
points: [
new Vec2(-200, -150).scale(POLY_SCALE),
new Vec2(200, -100).scale(POLY_SCALE),
new Vec2(100, 0).scale(POLY_SCALE),
new Vec2(200, 100).scale(POLY_SCALE),
new Vec2(-200, 75).scale(POLY_SCALE),
new Vec2(-150, -50).scale(POLY_SCALE),
],
drawBallLines(ctx) {
if (this.lines.length) {
const r = this.bRadius;
ctx.beginPath();
for (const l of this.lines) {
ctx.moveTo(l.p1.x, l.p1.y);
ctx.lineTo(l.p2.x, l.p2.y);
}
for (const p of this.points) {
ctx.moveTo(p.x + r, p.y);
ctx.arc(p.x, p.y, r, 0, Math.TAU);
}
ctx.stroke()
}
},
drawPath(ctx) {
ctx.beginPath();
for (const p of this.points) { ctx.lineTo(p.x, p.y) }
ctx.closePath();
ctx.stroke();
},
movingBallIntercept(ball, res = new Vec2()) {
if (this.bRadius !== ball.radius) { this.ballRadius = ball.radius }
var i = 0, nearest = Infinity, nearestGeom, units = new Vec2();
const ballT = new Line2(ball.pos, ball.pos.add(ball.delta, new Vec2()));
for (const p of this.points) {
const res = ballT.unitInterceptsCircle(p, ball.radius, units);
if (res && units.x < nearest && Math.isUnit(units.x)) { // assumes ball started outside poly so only need first point
nearest = units.x;
nearestGeom = ballT;
}
}
for (const line of this.lines) {
const res = line.unitInterceptsLine(ballT, units);
if (res && units.x < nearest && units.isUnits()) { // first unit.x is for unit dist on line
nearest = units.x;
nearestGeom = ballT;
}
}
if (nearestGeom) { return ballT.unitDistOn(nearest, res) }
return;
},
}
const ctx = canvas.getContext("2d");
var w = canvas.width, cw = w / 2;
var h = canvas.height, ch = h / 2
requestAnimationFrame(mainLoop);
// line and point for displaying mouse interaction. point holds the result if any
const line = new Line2(ball.pos, ball.pos.add(ball.delta, new Vec2())), point = new Vec2();
function mainLoop() {
ctx.setTransform(1,0,0,1,0,0); // reset transform
if(w !== innerWidth || h !== innerHeight){
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
}else{
ctx.clearRect(0,0,w,h);
}
ctx.setTransform(1,0,0,1,cw,ch); // center to canvas
if (mouse.button) { ball.pos.init(mouse.x - cw, mouse.y - ch) }
line.p2.init(mouse.x - cw, mouse.y - ch);
line.p2.sub(line.p1, ball.delta);
ctx.lineWidth = 1;
ctx.strokeStyle = "#000"
poly.drawPath(ctx)
ctx.strokeStyle = "#F804"
poly.drawBallLines(ctx);
ctx.strokeStyle = "#F00"
ctx.beginPath();
ctx.arc(ball.pos.x, ball.pos.y, ball.radius, 0, Math.TAU);
ctx.moveTo(line.p1.x, line.p1.y);
ctx.lineTo(line.p2.x, line.p2.y);
ctx.stroke();
ctx.strokeStyle = "#00f"
ctx.lineWidth = 2;
ctx.beginPath();
if (poly.movingBallIntercept(ball, point)) {
ctx.arc(point.x, point.y, ball.radius, 0, Math.TAU);
} else {
ctx.arc(line.p2.x, line.p2.y, ball.radius, 0, Math.TAU);
}
ctx.stroke();
requestAnimationFrame(mainLoop);
}
const mouse = {x:0, y:0, button: false};
function mouseEvents(e) {
const bounds = canvas.getBoundingClientRect();
mouse.x = e.pageX - bounds.left - scrollX;
mouse.y = e.pageY - bounds.top - scrollY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["mousedown","mouseup","mousemove"].forEach(name => document.addEventListener(name,mouseEvents));
#canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
Click to position ball. Move mouse to test trajectory
Vec2 and Line2
To make it easier a vector library will help. For the example I wrote a quick Vec2 and Line2 object (Note only functions used in the example have been tested, Note The object are designed for performance, inexperienced coders should avoid using these objects and opt for a more standard vector and line library)
It's probably not what you're looking for, but here's a way to do it (if you're not looking for perfect precision) :
You can try to approximate the position instead of calculating it.
The way you set up your code has a big advantage : You have the last position of the circle before the collision. Thanks to that, you can just "iterate" through the trajectory and try to find a position that is closest to the intersection position.
I'll assume you already have a function that tells you if a circle is intersecting with the polygon.
Code (C++) :
// What we need :
Vector startPos; // Last position of the circle before the collision
Vector currentPos; // Current, unwanted position
Vector dir; // Direction (a unit vector) of the circle's velocity
float distance = compute_distance(startPos, currentPos); // The distance from startPos to currentPos.
Polygon polygon; // The polygon
Circle circle; // The circle.
unsigned int iterations_count = 10; // The number of iterations that will be done. The higher this number, the more precise the resolution.
// The algorithm :
float currentDistance = distance / 2.f; // We start at the half of the distance.
Circle temp_copy; // A copy of the real circle to "play" with.
for (int i = 0; i < iterations_count; ++i) {
temp_copy.pos = startPos + currentDistance * dir;
if (checkForCollision(temp_copy, polygon)) {
currentDistance -= currentDistance / 2.f; // We go towards startPos by the half of the current distance.
}
else {
currentDistance += currentDistance / 2.f; // We go towards currentPos by the half of the current distance.
}
}
// currentDistance now contains the distance between startPos and the intersection point
// And this is where you should place your circle :
Vector intersectionPoint = startPos + currentDistance * dir;
I haven't tested this code so I hope there's no big mistake in there. It's also not optimized and there are a few problems with this approach (the intersection point could end up inside the polygon) so it still needs to be improved but I think you get the idea.
The other (big, depending on what you're doing) problem with this is that it's an approximation and not a perfect answer.
Hope this helps !
I'm not sure if I understood the scenario correctly, but an efficient straight forward use case would be, to only use a square bounding box of your circle first, calculating intersection of that square with your polygone is extremely fast, much much faster, than using the circle. Once you detect an intersection of that square and the polygone, you need to think or to write which precision is mostly suitable for your scenarion. If you need a better precision, than at this state, you can go on as this from here:
From the 90° angle of your sqare intersection, you draw a 45° degree line until it touches your circle, at this point, where it touches, you draw a new square, but this time, the square is embedded into the circle, let it run now, until this new square intersects the polygon, once it intersects, you have a guaranteed circle intersection. Depending on your needed precision, you can simply play around like this.
I'm not sure what your next problem is from here? If it has to be only the inverse of the circles trajectory, than it simply must be that reverse, I'm really not sure what I'm missing here.
I have CSS style for a layer:
.element {
-webkit-transform: rotate(7.5deg);
-moz-transform: rotate(7.5deg);
-ms-transform: rotate(7.5deg);
-o-transform: rotate(7.5deg);
transform: rotate(7.5deg);
}
Is there a way to get curent rotation value through jQuery?
I tried this
$('.element').css("-moz-transform")
The result is matrix(0.991445, 0.130526, -0.130526, 0.991445, 0px, 0px) which doesn't tell me a lot. What I'm looking to get is 7.5.
Here's my solution using jQuery.
This returns a numerical value corresponding to the rotation applied to any HTML element.
function getRotationDegrees(obj) {
var matrix = obj.css("-webkit-transform") ||
obj.css("-moz-transform") ||
obj.css("-ms-transform") ||
obj.css("-o-transform") ||
obj.css("transform");
if(matrix !== 'none') {
var values = matrix.split('(')[1].split(')')[0].split(',');
var a = values[0];
var b = values[1];
var angle = Math.round(Math.atan2(b, a) * (180/Math.PI));
} else { var angle = 0; }
return (angle < 0) ? angle + 360 : angle;
}
angle1 = getRotationDegrees($('#myDiv'));
angle2 = getRotationDegrees($('.mySpan a:last-child'));
etc...
I've found a bug/features in the Twist's code: the function return negative angles.
So I've add a simple line of code before returning the angle:
if(angle < 0) angle +=360;
Than the results will be:
function getRotationDegrees(obj) {
var matrix = obj.css("-webkit-transform") ||
obj.css("-moz-transform") ||
obj.css("-ms-transform") ||
obj.css("-o-transform") ||
obj.css("transform");
if(matrix !== 'none') {
var values = matrix.split('(')[1].split(')')[0].split(',');
var a = values[0];
var b = values[1];
var angle = Math.round(Math.atan2(b, a) * (180/Math.PI));
} else { var angle = 0; }
if(angle < 0) angle +=360;
return angle;
}
My Solution (using jQuery):
$.fn.rotationInfo = function() {
var el = $(this),
tr = el.css("-webkit-transform") || el.css("-moz-transform") || el.css("-ms-transform") || el.css("-o-transform") || '',
info = {rad: 0, deg: 0};
if (tr = tr.match('matrix\\((.*)\\)')) {
tr = tr[1].split(',');
if(typeof tr[0] != 'undefined' && typeof tr[1] != 'undefined') {
info.rad = Math.atan2(tr[1], tr[0]);
info.deg = parseFloat((info.rad * 180 / Math.PI).toFixed(1));
}
}
return info;
};
Usage:
$(element).rotationInfo(); // {deg: 7.5, rad: 0.13089969389957515}
$(element).rotationInfo().deg; // 7.5
Here is a plug-in version of Twist's function. Also, the conditional if(matrix !== 'none') did not work for me. So I have added type-checking:
(function ($) {
$.fn.rotationDegrees = function () {
var matrix = this.css("-webkit-transform") ||
this.css("-moz-transform") ||
this.css("-ms-transform") ||
this.css("-o-transform") ||
this.css("transform");
if(typeof matrix === 'string' && matrix !== 'none') {
var values = matrix.split('(')[1].split(')')[0].split(',');
var a = values[0];
var b = values[1];
var angle = Math.round(Math.atan2(b, a) * (180/Math.PI));
} else { var angle = 0; }
return angle;
};
}(jQuery));
Use as follows:
var rotation = $('img').rotationDegrees();
The CSS tranform property will always return a matrix value, as rotate, skew, scale etc. is just shorthand for doing things easier, and not having to calculate the matrix value everytime, however the matrix is calculated by the browser and applied as a matrix, and when that is done it can no longer return the rotated degree by angle without recalculating the matrix back again.
To make such calcualtions easier there is a javascript library called Sylvester that was created for the purpose of easy matrix calculation, try looking at that to get the rotation degree from the matrix value.
Also, if you where to write a rotate function in javascript to translate rotational degrees to a matrix, it would probably look something like this (this uses sylvester for the last calculation) :
var Transform = {
rotate: function(deg) {
var rad = parseFloat(deg) * (Math.PI/180),
cos_theta = Math.cos(rad),
sin_theta = Math.sin(rad);
var a = cos_theta,
b = sin_theta,
c = -sin_theta,
d = cos_theta;
return $M([
[a, c, 0],
[b, d, 0],
[0, 0, 1]
]);
}
};
Now all you really have to do is reverse enginer that function and you're golden :-)
This script is very helpful
https://github.com/zachstronaut/jquery-css-transform
I have make a fiddle with this working code to get rotateX Y Z on a 3D , or rotateZ for a 2D transform. Thanks to mihn for the base code that i have little updated with actual jquery 2.2.3.
I currently use this solution for my own projects.
https://jsfiddle.net/bragon95/49a4h6e9/
//
//Thanks: Adapted on base code from mihn http://stackoverflow.com/a/20371725
//
function getcsstransform(obj)
{
var isIE = /(MSIE|Trident\/|Edge\/)/i.test(navigator.userAgent);
var TType="undefined",
rotateX = 0,
rotateY = 0,
rotateZ = 0;
var matrix = obj.css("-webkit-transform") ||
obj.css("-moz-transform") ||
obj.css("-ms-transform") ||
obj.css("-o-transform") ||
obj.css("transform");
if (matrix!==undefined && matrix !== 'none')
{
// if matrix is 2d matrix
TType="2D";
if (matrix.indexOf('matrix(') >= 0)
{
var values = matrix.split('(')[1].split(')')[0];
if (isIE) //case IE
{
angle = parseFloat(values.replace('deg', STR_EMPTY));
}else
{
values = values.split(',');
var a = values[0];
var b = values[1];
var rotateZ = Math.round(Math.atan2(b, a) * (180 / Math.PI));
}
}else
{
// matrix is matrix3d
TType="3D";
var values = matrix.split('(')[1].split(')')[0].split(',');
var sinB = parseFloat(values[8]);
var b = Math.round(Math.asin(sinB) * 180 / Math.PI);
var cosB = Math.cos(b * Math.PI / 180);
var matrixVal10 = parseFloat(values[9]);
var a = Math.round(Math.asin(-matrixVal10 / cosB) * 180 / Math.PI);
var matrixVal1 = parseFloat(values[0]);
var c = Math.round(Math.acos(matrixVal1 / cosB) * 180 / Math.PI);
rotateX = a;
rotateY = b;
rotateZ = c;
}
}
return { TType: TType, rotateX: rotateX, rotateY: rotateY, rotateZ: rotateZ };
};
mAngle = getcsstransform($("#Objet3D"));
if (mAngle.TType=="2D")
{
$("#Result").html("Transform 2D [rotateZ=" + mAngle.rotateZ + "°]");
}else
{
$("#Result").html("Transform 3D [rotateX=" + mAngle.rotateX + "°|rotateY=" + mAngle.rotateY + "°|rotateZ=" + mAngle.rotateZ + "°]");
}
If you do this in the way you described, any this is the only place where you actually modify transform of the object, then since your browser can not be all 4 kinds of browsers at the same time, some of the prefixed values you assigned are still exactly as you assigned them.
So for example if you use webkit, then this.css('-o-transform') will still return 'rotate(7.5deg)', so it is just a matter of matching it against /rotate\((.*)deg\)/.
This worked fine for me : I always assign 5 css styles, and read back all five styles, hoping that at least one of them will be untouched. I am not sure if this works if the styles are set in CSS (not in JS) though.
Also you could replace var angle = Math.round(Math.atan2(b, a) * (180/Math.PI)); to var angle = Math.round(Math.acos(a) * (180/Math.PI));
Since I constantly need to use jQuery together with TweenMax and since TweenMax already took care of all the parsing of various types of transformation strings as well as compatibility issues, I wrote a tiny jquery plugin here (more of a wrap up of gsap's) that could directly access these values like this:
$('#ele').transform('rotationX') // returns 0
$('#ele').transform('x') // returns value of translate-x
The list of properties you could get/set, along with their initial properties:
perspective: 0
rotation: 0
rotationX: 0
rotationY: 0
scaleX: 1
scaleY: 1
scaleZ: 1
skewX: 0
skewY: 0
x: 0
y: 0
z: 0
zOrigin: 0
Paste from my other answer, hope this helps.
If you're willing to use in-line styling for just this transformation, then you can use jQuery to get the contents of the style tag:
parseInt($( /*TODO*/ ).attr('style').split('rotate(')[1].split('deg)')[0]);
So, i'm trying to implement hough transform, this version is 1-dimensional (its for all dims reduced to 1 dim optimization) version based on the minor properties.
Enclosed is my code, with a sample image... input and output.
Obvious question is what am i doing wrong. I've tripled check my logic and code and it looks good also my parameters. But obviously i'm missing on something.
Notice that the red pixels are supposed to be ellipses centers , while the blue pixels are edges to be removed (belong to the ellipse that conform to the mathematical equations).
also, i'm not interested in openCV / matlab / ocatve / etc.. usage (nothing against them).
Thank you very much!
var fs = require("fs"),
Canvas = require("canvas"),
Image = Canvas.Image;
var LEAST_REQUIRED_DISTANCE = 40, // LEAST required distance between 2 points , lets say smallest ellipse minor
LEAST_REQUIRED_ELLIPSES = 6, // number of found ellipse
arr_accum = [],
arr_edges = [],
edges_canvas,
xy,
x1y1,
x2y2,
x0,
y0,
a,
alpha,
d,
b,
max_votes,
cos_tau,
sin_tau_sqr,
f,
new_x0,
new_y0,
any_minor_dist,
max_minor,
i,
found_minor_in_accum,
arr_edges_len,
hough_file = 'sample_me2.jpg',
edges_canvas = drawImgToCanvasSync(hough_file); // make sure everything is black and white!
arr_edges = getEdgesArr(edges_canvas);
arr_edges_len = arr_edges.length;
var hough_canvas_img_data = edges_canvas.getContext('2d').getImageData(0, 0, edges_canvas.width,edges_canvas.height);
for(x1y1 = 0; x1y1 < arr_edges_len ; x1y1++){
if (arr_edges[x1y1].x === -1) { continue; }
for(x2y2 = 0 ; x2y2 < arr_edges_len; x2y2++){
if ((arr_edges[x2y2].x === -1) ||
(arr_edges[x2y2].x === arr_edges[x1y1].x && arr_edges[x2y2].y === arr_edges[x1y1].y)) { continue; }
if (distance(arr_edges[x1y1],arr_edges[x2y2]) > LEAST_REQUIRED_DISTANCE){
x0 = (arr_edges[x1y1].x + arr_edges[x2y2].x) / 2;
y0 = (arr_edges[x1y1].y + arr_edges[x2y2].y) / 2;
a = Math.sqrt((arr_edges[x1y1].x - arr_edges[x2y2].x) * (arr_edges[x1y1].x - arr_edges[x2y2].x) + (arr_edges[x1y1].y - arr_edges[x2y2].y) * (arr_edges[x1y1].y - arr_edges[x2y2].y)) / 2;
alpha = Math.atan((arr_edges[x2y2].y - arr_edges[x1y1].y) / (arr_edges[x2y2].x - arr_edges[x1y1].x));
for(xy = 0 ; xy < arr_edges_len; xy++){
if ((arr_edges[xy].x === -1) ||
(arr_edges[xy].x === arr_edges[x2y2].x && arr_edges[xy].y === arr_edges[x2y2].y) ||
(arr_edges[xy].x === arr_edges[x1y1].x && arr_edges[xy].y === arr_edges[x1y1].y)) { continue; }
d = distance({x: x0, y: y0},arr_edges[xy]);
if (d > LEAST_REQUIRED_DISTANCE){
f = distance(arr_edges[xy],arr_edges[x2y2]); // focus
cos_tau = (a * a + d * d - f * f) / (2 * a * d);
sin_tau_sqr = (1 - cos_tau * cos_tau);//Math.sqrt(1 - cos_tau * cos_tau); // getting sin out of cos
b = (a * a * d * d * sin_tau_sqr ) / (a * a - d * d * cos_tau * cos_tau);
b = Math.sqrt(b);
b = parseInt(b.toFixed(0));
d = parseInt(d.toFixed(0));
if (b > 0){
found_minor_in_accum = arr_accum.hasOwnProperty(b);
if (!found_minor_in_accum){
arr_accum[b] = {f: f, cos_tau: cos_tau, sin_tau_sqr: sin_tau_sqr, b: b, d: d, xy: xy, xy_point: JSON.stringify(arr_edges[xy]), x0: x0, y0: y0, accum: 0};
}
else{
arr_accum[b].accum++;
}
}// b
}// if2 - LEAST_REQUIRED_DISTANCE
}// for xy
max_votes = getMaxMinor(arr_accum);
// ONE ellipse has been detected
if (max_votes != null &&
(max_votes.max_votes > LEAST_REQUIRED_ELLIPSES)){
// output ellipse details
new_x0 = parseInt(arr_accum[max_votes.index].x0.toFixed(0)),
new_y0 = parseInt(arr_accum[max_votes.index].y0.toFixed(0));
setPixel(hough_canvas_img_data,new_x0,new_y0,255,0,0,255); // Red centers
// remove the pixels on the detected ellipse from edge pixel array
for (i=0; i < arr_edges.length; i++){
any_minor_dist = distance({x:new_x0, y: new_y0}, arr_edges[i]);
any_minor_dist = parseInt(any_minor_dist.toFixed(0));
max_minor = b;//Math.max(b,arr_accum[max_votes.index].d); // between the max and the min
// coloring in blue the edges we don't need
if (any_minor_dist <= max_minor){
setPixel(hough_canvas_img_data,arr_edges[i].x,arr_edges[i].y,0,0,255,255);
arr_edges[i] = {x: -1, y: -1};
}// if
}// for
}// if - LEAST_REQUIRED_ELLIPSES
// clear accumulated array
arr_accum = [];
}// if1 - LEAST_REQUIRED_DISTANCE
}// for x2y2
}// for xy
edges_canvas.getContext('2d').putImageData(hough_canvas_img_data, 0, 0);
writeCanvasToFile(edges_canvas, __dirname + '/hough.jpg', function() {
});
function getMaxMinor(accum_in){
var max_votes = -1,
max_votes_idx,
i,
accum_len = accum_in.length;
for(i in accum_in){
if (accum_in[i].accum > max_votes){
max_votes = accum_in[i].accum;
max_votes_idx = i;
} // if
}
if (max_votes > 0){
return {max_votes: max_votes, index: max_votes_idx};
}
return null;
}
function distance(point_a,point_b){
return Math.sqrt((point_a.x - point_b.x) * (point_a.x - point_b.x) + (point_a.y - point_b.y) * (point_a.y - point_b.y));
}
function getEdgesArr(canvas_in){
var x,
y,
width = canvas_in.width,
height = canvas_in.height,
pixel,
edges = [],
ctx = canvas_in.getContext('2d'),
img_data = ctx.getImageData(0, 0, width, height);
for(x = 0; x < width; x++){
for(y = 0; y < height; y++){
pixel = getPixel(img_data, x,y);
if (pixel.r !== 0 &&
pixel.g !== 0 &&
pixel.b !== 0 ){
edges.push({x: x, y: y});
}
} // for
}// for
return edges
} // getEdgesArr
function drawImgToCanvasSync(file) {
var data = fs.readFileSync(file)
var canvas = dataToCanvas(data);
return canvas;
}
function dataToCanvas(imagedata) {
img = new Canvas.Image();
img.src = new Buffer(imagedata, 'binary');
var canvas = new Canvas(img.width, img.height);
var ctx = canvas.getContext('2d');
ctx.patternQuality = "best";
ctx.drawImage(img, 0, 0, img.width, img.height,
0, 0, img.width, img.height);
return canvas;
}
function writeCanvasToFile(canvas, file, callback) {
var out = fs.createWriteStream(file)
var stream = canvas.createPNGStream();
stream.on('data', function(chunk) {
out.write(chunk);
});
stream.on('end', function() {
callback();
});
}
function setPixel(imageData, x, y, r, g, b, a) {
index = (x + y * imageData.width) * 4;
imageData.data[index+0] = r;
imageData.data[index+1] = g;
imageData.data[index+2] = b;
imageData.data[index+3] = a;
}
function getPixel(imageData, x, y) {
index = (x + y * imageData.width) * 4;
return {
r: imageData.data[index+0],
g: imageData.data[index+1],
b: imageData.data[index+2],
a: imageData.data[index+3]
}
}
It seems you try to implement the algorithm of Yonghong Xie; Qiang Ji (2002). A new efficient ellipse detection method 2. p. 957.
Ellipse removal suffers from several bugs
In your code, you perform the removal of found ellipse (step 12 of the original paper's algorithm) by resetting coordinates to {-1, -1}.
You need to add:
`if (arr_edges[x1y1].x === -1) break;`
at the end of the x2y2 block. Otherwise, the loop will consider -1, -1 as a white point.
More importantly, your algorithm consists in erasing every point which distance to the center is smaller than b. b supposedly is the minor axis half-length (per the original algorithm). But in your code, variable b actually is the latest (and not most frequent) half-length, and you erase points with a distance lower than b (instead of greater, since it's the minor axis). In other words, you clear all points inside a circle with a distance lower than latest computed axis.
Your sample image can actually be processed with a clearing of all points inside a circle with a distance lower than selected major axis with:
max_minor = arr_accum[max_votes.index].d;
Indeed, you don't have overlapping ellipses and they are spread enough. Please consider a better algorithm for overlapping or closer ellipses.
The algorithm mixes major and minor axes
Step 6 of the paper reads:
For each third pixel (x, y), if the distance between (x, y) and (x0,
y0) is greater than the required least distance for a pair of pixels
to be considered then carry out the following steps from (7) to (9).
This clearly is an approximation. If you do so, you will end up considering points further than the minor axis half length, and eventually on the major axis (with axes swapped). You should make sure the distance between the considered point and the tested ellipse center is smaller than currently considered major axis half-length (condition should be d <= a). This will help with the ellipse erasing part of the algorithm.
Also, if you also compare with the least distance for a pair of pixels, as per the original paper, 40 is too large for the smaller ellipse in your picture. The comment in your code is wrong, it should be at maximum half the smallest ellipse minor axis half-length.
LEAST_REQUIRED_ELLIPSES is too small
This parameter is also misnamed. It is the minimum number of votes an ellipse should get to be considered valid. Each vote corresponds to a pixel. So a value of 6 means that only 6+2 pixels make an ellipse. Since pixels coordinates are integers and you have more than 1 ellipse in your picture, the algorithm might detect ellipses that are not, and eventually clear edges (especially when combined with the buggy ellipse erasing algorithm). Based on tests, a value of 100 will find four of the five ellipses of your picture, while 80 will find them all. Smaller values will not find the proper centers of the ellipses.
Sample image is not black & white
Despite the comment, sample image is not exactly black and white. You should convert it or apply some threshold (e.g. RGB values greater than 10 instead of simply different form 0).
Diff of minimum changes to make it work is available here:
https://gist.github.com/pguyot/26149fec29ffa47f0cfb/revisions
Finally, please note that parseInt(x.toFixed(0)) could be rewritten Math.floor(x), and you probably want to not truncate all floats like this, but rather round them, and proceed where needed: the algorithm to erase the ellipse from the picture would benefit from non truncated values for the center coordinates. This code definitely could be improved further, for example it currently computes the distance between points x1y1 and x2y2 twice.
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>