【问题标题】:Significant error when approximating elliptical arcs with bezier curves on canvas with javascript使用javascript在画布上用贝塞尔曲线逼近椭圆弧时出现重大错误
【发布时间】:2017-10-12 16:23:32
【问题描述】:

我正在尝试在 javascript 中将 svg 路径转换为画布,但是将 svg 路径椭圆弧映射到画布路径确实很困难。其中一种方法是使用多条贝塞尔曲线进行近似。

我已经成功地implemented 用贝塞尔曲线近似椭圆弧,但是近似不是很准确。

我的代码:

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

结果:

红线代表svg路径椭圆弧,黑线代表近似值

如何在画布上准确地绘制任何可能的椭圆弧?

更新:

忘了说算法的原始出处:https://mortoray.com/2017/02/16/rendering-an-svg-elliptical-arc-as-bezier-curves/

【问题讨论】:

  • 您在编写代码时是否关注algorthim in the SVG spec
  • @PaulLeBeau,更新了我的问题以提及我关注的帖子。我通读了整篇文章和 SVG 规范,文章中的算法是按照 SVG 规范编写的。
  • 显然它被破坏了,因为按照定义,贝塞尔曲线中的每个锚点都应该在弧上。但是,对于我的生活,我找不到错误。我检查了所有的数学。
  • 另外值得注意的是,它只发生在那个角度。在其他角度,错误消失了,所以我似乎是 cos() 的东西,而 sin() 应该是,但我检查了一下,它们看起来都很好。

标签: javascript svg


【解决方案1】:

所以这两个错误都很简单:

  • n2 应该声明为n2 = theta + delta;
  • E 和 Ed 函数应使用 rX rY 而不是 rx ry

这就解决了一切。虽然原件显然应该选择将弧分成相等大小的部分,而不是 pi/4 大小的元素,然后附加其余部分。只需找出它需要多少部分,然后将范围划分为相同大小的许多部分,这似乎是一个更优雅的解决方案,而且由于误差随着长度的增加而增加,它也会更准确。

有关工作版本,请参阅:https://jsfiddle.net/Tatarize/4ro0Lm4u/


这不仅仅是在这一方面,它在任何地方都不起作用。您可以看到,根据 phi,它会做很多不同的坏事。那里实际上非常好。但是,其他地方也坏了。

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

原因是n2的声明错误,应该是:

n2 = theta + delta;

https://jsfiddle.net/Tatarize/ba903pss/ 但是,修复索引中的错误后,它显然没有像应有的那样扩大规模。可能是 svg 标准中的弧被按比例放大,因此肯定会有解决方案,而在相关代码中,它们看起来像是被钳制的。

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

"如果 rx, ry 和 φ 是无解的(基本上, 椭圆不够大,无法从 (x1, y1) 到达 (x2, y2)),那么 椭圆均匀地按比例放大,直到只有一个解 (直到椭圆足够大)。”

对此进行测试,因为它确实具有应该对其进行扩展的代码,因此我在调用该代码时将其更改为绿色。当它拧紧时它会变成绿色。所以是的,由于某种原因无法扩展:

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

这意味着某些东西正在使用 rx 而不是缩放的 rX,它是 E 和 Ed 函数:

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

对于ry,这些rx 引用必须阅读rXrY

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

最终修复了最后一个错误,QED。

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


我摆脱了画布,将所有内容移动到 svg 并为其设置动画。

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

【讨论】:

  • 您应该因为如此全面的答案而获奖。说真的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-07
  • 1970-01-01
  • 2012-12-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多