Significant error when approximating elliptical arcs with bezier curves on canvas with javascript

Viewed 156

I'm trying to convert svg path to canvas in javascript, however it's really hard to map svg path elliptical arcs to canvas path. One of the ways is to approximate using multiple bezier curves.

I have successfully implemented the approximation of elliptical arcs with bezier curves however the approximation isn't very accurate.

My code:

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()

With the result:

Elliptical arc and its approximation

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

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

Update:

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

1 Answers
Related