Significant error in approximating elliptic arcs with Bezier curves on canvas with javascript

I am trying to convert an SVG path to canvas in javascript, however it is very difficult to map the elliptical arcs of the SVG path to the canvas path. One way is to approximate the use of several Bezier curves.

I have successfully implemented the approximation of elliptic arcs with Bezier curves, however, the approximation is not very accurate.

My code is:

var canvas = document.getElementById("canvas"); var ctx = canvas.getContext("2d"); 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,200 ctx.moveTo(100,200) // a25,100 -30 0,1 50,-25 var rx = 25, ry=100 ,phi = -30 * Math.PI / 180, fa = 0, fs = 1, x = 100, y = 200, x1 = x + 50, y1 = y - 25; var cps = generateBezierPoints(rx, ry, phi, fa, fs, x, y, x1, y1); var limit = 4; 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() 

As a result:

Elliptical arc and its approximation

The red line represents the elliptical arc of the svg path, and the black line represents the approximation

How can I accurately draw any possible elliptical arc on canvas?

Update:

Forgot to mention the source of the algorithm: https://mortoray.com/2017/02/16/rendering-an-svg-elliptical-arc-as-bezier-curves/

+5
source share
1 answer

So both errors:

  • n2 must be declared n2 = theta + delta;
  • Functions E and Ed should use rX rY , not rX rY .

And that fixes it all. Although the original should obviously have preferred to divide the arcs into parts of equal size, rather than into elements of size pi / 4, and then add the rest. Just find out how many parts he will need, and then divide the range so that many parts of equal size seem to be a much more elegant solution, and since the error increases with increasing, it will also be more accurate.

See: https://jsfiddle.net/Tatarize/4ro0Lm4u/ for the working version.


This is not just in the sense that it does not work anywhere. You can see that depending on phi, he does a lot of different bad things. This is really amazingly good. But, broken and everywhere.

https://jsfiddle.net/Tatarize/dm7yqypb/

The reason is because declaration n2 is incorrect and must read:

 n2 = theta + delta; 

https://jsfiddle.net/Tatarize/ba903pss/ But, fixing the error in indexing, it obviously does not scale there as it should. It is possible that arcs in the svg standard are scaled so that, of course, there can be a solution, whereas in the corresponding code they seem to be clamped.

https://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters

"If rx, ry and φ are such that there is no solution (basically, the ellipse is not large enough to reach (x1, y1) to (x2, y2)), then the ellipse scales evenly until there is exactly one solution (until the ellipse won't be big enough).

Testing this, since it really has code that needs to scale it, I changed it to green when this code received a call. And it turns green when it is screwed. So yes, this is the inability to scale for some reason:

https://jsfiddle.net/Tatarize/tptroxho/

This means that something uses rx, not scaled rX, and the functions E and Ed:

 var enx = cx + rx * Math.cos(phi) * Math.cos(n) - ry * Math.sin(phi) * Math.sin(n); 

These rX links should read rX and rY for rY .

 var enx = cx + rx * Math.cos(phi) * Math.cos(n) - ry * Math.sin(phi) * Math.sin(n); 

Which finally fixes the last error, QED.

https://jsfiddle.net/Tatarize/4ro0Lm4u/


I got rid of the canvas, moved everything to svg and animated it.

 var svgNS = "http://www.w3.org/2000/svg"; var svg = document.getElementById("svg"); var arcgroup = document.getElementById("arcgroup"); var curvegroup = document.getElementById("curvegroup"); function doArc() { while (arcgroup.firstChild) { arcgroup.removeChild(arcgroup.firstChild); } //clear old svg data. --> var d = document.createElementNS(svgNS, "path"); //var path = "M100,200 a25,100 -30 0,1 50,-25" var path = "M" + x + "," + y + "a" + rx + " " + ry + " " + phi + " " + fa + " " + fs + " " + " " + x1 + " " + y1; d.setAttributeNS(null, "d", path); arcgroup.appendChild(d); } function doCurve() { var cps = generateBezierPoints(rx, ry, phi * Math.PI / 180, fa, fs, x, y, x + x1, y + y1); while (curvegroup.firstChild) { curvegroup.removeChild(curvegroup.firstChild); } //clear old svg data. --> var d = document.createElementNS(svgNS, "path"); var limit = 4; var path = "M" + x + "," + y; for (var i = 0; i < limit && i < cps.length; i++) { if (i < limit - 1) { path += "C" + cps[i].cpx1 + " " + cps[i].cpy1 + " " + cps[i].cpx2 + " " + cps[i].cpy2 + " " + cps[i].en2.x + " " + cps[i].en2.y; } else { path += "C" + cps[i].cpx1 + " " + cps[i].cpy1 + " " + cps[i].cpx2 + " " + cps[i].cpy2 + " " + (x + x1) + " " + (y + y1); } } d.setAttributeNS(null, "d", path); d.setAttributeNS(null, "stroke", "#000"); curvegroup.appendChild(d); } setInterval(phiClock, 50); function phiClock() { phi += 1; doCurve(); doArc(); } doCurve(); doArc(); 
+2
source

All Articles