Svg approximation of elliptical arc in canvas with javascript
I am trying to create an elliptical arc by approximating a Bezier curve as in post https://mortoray.com/2017/02/16/rendering-an-svg-elliptical-arc-as-bezier-curves/
However, my implementation doesn't seem to give the correct result. (The red line is SVG and the black line is the path to the canvas)
This is my code
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
// M100,350
// a45,35 -30 0,1 50,-25
canvas.width = document.body.clientWidth;
canvas.height = document.body.clientHeight;
ctx.strokeWidth = 2;
ctx.strokeStyle = "#000000";
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max)
}
function svgAngle(ux, uy, vx, vy ) {
var dot = ux*vx + uy*vy;
var len = Math.sqrt(ux*ux + uy*uy) * Math.sqrt(vx*vx + vy*vy);
var ang = Math.acos( clamp(dot / len,-1,1) );
if ( (ux*vy - uy*vx) < 0)
ang = -ang;
return ang;
}
function generateBezierPoints(rx, ry, phi, flagA, flagS, x1, y1, x2, y2) {
var rX = Math.abs(rx);
var rY = Math.abs(ry);
var dx2 = (x1 - x2)/2;
var dy2 = (y1 - y2)/2;
var x1p = Math.cos(phi)*dx2 + Math.sin(phi)*dy2;
var y1p = -Math.sin(phi)*dx2 + Math.cos(phi)*dy2;
var rxs = rX * rX;
var rys = rY * rY;
var x1ps = x1p * x1p;
var y1ps = y1p * y1p;
var cr = x1ps/rxs + y1ps/rys;
if (cr > 1) {
var s = Math.sqrt(cr);
rX = s * rX;
rY = s * rY;
rxs = rX * rX;
rys = rY * rY;
}
var dq = (rxs * y1ps + rys * x1ps);
var pq = (rxs*rys - dq) / dq;
var q = Math.sqrt( Math.max(0,pq) );
if (flagA === flagS)
q = -q;
var cxp = q * rX * y1p / rY;
var cyp = - q * rY * x1p / rX;
var cx = Math.cos(phi)*cxp - Math.sin(phi)*cyp + (x1 + x2)/2;
var cy = Math.sin(phi)*cxp + Math.cos(phi)*cyp + (y1 + y2)/2;
var theta = svgAngle( 1,0, (x1p-cxp) / rX, (y1p - cyp)/rY );
var delta = svgAngle(
(x1p - cxp)/rX, (y1p - cyp)/rY,
(-x1p - cxp)/rX, (-y1p-cyp)/rY);
delta = delta - Math.PI * 2 * Math.floor(delta / (Math.PI * 2));
if (!flagS)
delta -= 2 * Math.PI;
var n1 = theta, n2 = delta;
// E(n)
// cx +acosθcosη−bsinθsinη
// cy +asinθcosη+bcosθsinη
function E(n) {
var enx = cx + rx * Math.cos(phi) * Math.cos(n) - ry * Math.sin(phi) * Math.sin(n);
var eny = cy + rx * Math.sin(phi) * Math.cos(n) + ry * Math.cos(phi) * Math.sin(n);
return {x: enx,y: eny};
}
// E'(n)
// −acosθsinη−bsinθcosη
// −asinθsinη+bcosθcosη
function Ed(n) {
var ednx = -1 * rx * Math.cos(phi) * Math.sin(n) - ry * Math.sin(phi) * Math.cos(n);
var edny = -1 * rx * Math.sin(phi) * Math.sin(n) + ry * Math.cos(phi) * Math.cos(n);
return {x: ednx, y: edny};
}
var en1 = E(n1);
var en2 = E(n2);
var edn1 = Ed(n1);
var edn2 = Ed(n2);
var alpha = Math.sin(n2 - n1) * (Math.sqrt(4 + 3 * Math.pow(Math.tan((n2 - n1)/2), 2)) - 1)/3;
console.log(en1, en2);
return {
cpx1: en1.x + alpha*edn1.x,
cpy1: en1.y + alpha*edn1.y,
cpx2: en2.x - alpha*edn2.x,
cpy2: en2.y - alpha*edn2.y
};
}
// M100,100
ctx.moveTo(100,100)
// a45,35 -30 0,1 50,-25
cp = generateBezierPoints(
45,35, // Radii
-30 * Math.PI / 180, // xAngle
0, // Large arc flag
1, // Sweep flag
100,100, // Endpoint1
100 + 50, 100 - 25 // Endpoint2
);
ctx.bezierCurveTo(cp.cpx1,cp.cpy1,cp.cpx2,cp.cpy2,150,75);
ctx.stroke()
I need help figuring out where I'm going wrong
UPDATE:
I went through the mail a couple more times and there is one piece of the post that I don't quite understand, which may also be missing in my implementation.
All I had to do was split the angle range into small sections to get a good approximation. I didn't quite understand the error calculations in the docs, but found another article stating that π / 2 divisions give a potential error in one pixel at a sufficiently high resolution. So I chose π / 4 to provide smooth animation even for partial arcs on high density mobile devices.
I don't understand what the author means by splitting angles ...
source to share
So apparently an elliptical arc cannot be approximated by a single Bezier curve, so it takes multiple Bezier curves, dividing the two angles into ranges.
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
// M100,350
// a45,35 -30 0,1 50,-25
canvas.width = document.body.clientWidth;
canvas.height = document.body.clientHeight;
ctx.strokeWidth = 2;
ctx.strokeStyle = "#000000";
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max)
}
function svgAngle(ux, uy, vx, vy ) {
var dot = ux*vx + uy*vy;
var len = Math.sqrt(ux*ux + uy*uy) * Math.sqrt(vx*vx + vy*vy);
var ang = Math.acos( clamp(dot / len,-1,1) );
if ( (ux*vy - uy*vx) < 0)
ang = -ang;
return ang;
}
function generateBezierPoints(rx, ry, phi, flagA, flagS, x1, y1, x2, y2) {
var rX = Math.abs(rx);
var rY = Math.abs(ry);
var dx2 = (x1 - x2)/2;
var dy2 = (y1 - y2)/2;
var x1p = Math.cos(phi)*dx2 + Math.sin(phi)*dy2;
var y1p = -Math.sin(phi)*dx2 + Math.cos(phi)*dy2;
var rxs = rX * rX;
var rys = rY * rY;
var x1ps = x1p * x1p;
var y1ps = y1p * y1p;
var cr = x1ps/rxs + y1ps/rys;
if (cr > 1) {
var s = Math.sqrt(cr);
rX = s * rX;
rY = s * rY;
rxs = rX * rX;
rys = rY * rY;
}
var dq = (rxs * y1ps + rys * x1ps);
var pq = (rxs*rys - dq) / dq;
var q = Math.sqrt( Math.max(0,pq) );
if (flagA === flagS)
q = -q;
var cxp = q * rX * y1p / rY;
var cyp = - q * rY * x1p / rX;
var cx = Math.cos(phi)*cxp - Math.sin(phi)*cyp + (x1 + x2)/2;
var cy = Math.sin(phi)*cxp + Math.cos(phi)*cyp + (y1 + y2)/2;
var theta = svgAngle( 1,0, (x1p-cxp) / rX, (y1p - cyp)/rY );
var delta = svgAngle(
(x1p - cxp)/rX, (y1p - cyp)/rY,
(-x1p - cxp)/rX, (-y1p-cyp)/rY);
delta = delta - Math.PI * 2 * Math.floor(delta / (Math.PI * 2));
if (!flagS)
delta -= 2 * Math.PI;
var n1 = theta, n2 = delta;
// E(n)
// cx +acosθcosη−bsinθsinη
// cy +asinθcosη+bcosθsinη
function E(n) {
var enx = cx + rx * Math.cos(phi) * Math.cos(n) - ry * Math.sin(phi) * Math.sin(n);
var eny = cy + rx * Math.sin(phi) * Math.cos(n) + ry * Math.cos(phi) * Math.sin(n);
return {x: enx,y: eny};
}
// E'(n)
// −acosθsinη−bsinθcosη
// −asinθsinη+bcosθcosη
function Ed(n) {
var ednx = -1 * rx * Math.cos(phi) * Math.sin(n) - ry * Math.sin(phi) * Math.cos(n);
var edny = -1 * rx * Math.sin(phi) * Math.sin(n) + ry * Math.cos(phi) * Math.cos(n);
return {x: ednx, y: edny};
}
var n = [];
n.push(n1);
var interval = Math.PI/4;
while(n[n.length - 1] + interval < n2)
n.push(n[n.length - 1] + interval)
n.push(n2);
function getCP(n1, n2) {
var en1 = E(n1);
var en2 = E(n2);
var edn1 = Ed(n1);
var edn2 = Ed(n2);
var alpha = Math.sin(n2 - n1) * (Math.sqrt(4 + 3 * Math.pow(Math.tan((n2 - n1)/2), 2)) - 1)/3;
console.log(en1, en2);
return {
cpx1: en1.x + alpha*edn1.x,
cpy1: en1.y + alpha*edn1.y,
cpx2: en2.x - alpha*edn2.x,
cpy2: en2.y - alpha*edn2.y,
en1: en1,
en2: en2
};
}
var cps = []
for(var i = 0; i < n.length - 1; i++) {
cps.push(getCP(n[i],n[i+1]));
}
return cps;
}
// M100,100
ctx.moveTo(100,100)
// a45,35 -30 0,1 50,-25
var rx = 45, ry=35,phi = -30 * Math.PI / 180, fa = 0, fs = 1, x = 100, y = 100, x1 = x + 50, y1 = y - 25;
var cps = generateBezierPoints(rx, ry, phi, fa, fs, x, y, x1, y1);
var limit = 2;
for(var i = 0; i < limit && i < cps.length; i++) {
ctx.bezierCurveTo(cps[i].cpx1, cps[i].cpy1,
cps[i].cpx2, cps[i].cpy2,
i < limit - 1 ? cps[i].en2.x : x1, i < limit - 1 ? cps[i].en2.y : y1);
}
ctx.stroke()
source to share