如何使用 start 和 endAngle 渲染 svg 圆

Posted

技术标签:

【中文标题】如何使用 start 和 endAngle 渲染 svg 圆【英文标题】:How to render a svg circle using start and endAngle 【发布时间】:2017-12-19 01:51:56 【问题描述】:

我已经使用 start 和 endAngle 渲染了 svg 圆。它工作得很好。但是当我渲染完整的圆(startAngle 为 70,endAngle 为 70)时,输出有很大的不同(除了 0、90、180、270)。我为这段代码做错了什么?

            function getPathArc(center, start, end, radius) 
                end -= this.isCompleteAngle(start, end) ? 0.0001 : 0;
                var degree = end - start;
                degree = degree < 0 ? (degree + 360) : degree;
                return this.getCirclePath(
                    center, getLocationFromAngle(start, radius, center),
                    getLocationFromAngle(end, radius, center), radius, (degree < 180) ? 0 : 1
                );
            

            function getCirclePath(center, start, end, radius, clockWise) 
                return 'M ' + start.x + ' ' + start.y + ' A ' + radius + ' ' +
                    radius + ' 0 ' + clockWise + ' 1 ' + end.x + ' ' + end.y;
            

            function getLocationFromAngle(degree, radius, center) 
                var radian = (degree * Math.PI) / 180;
                return 
                    x : Math.cos(radian) * radius + center.x,
                    y : Math.sin(radian) * radius + center.y
                
            

            function isCompleteAngle(startAngle, endAngle) 
                var totalAngle = endAngle - startAngle;
                totalAngle = totalAngle <= 0 ? (totalAngle + 360) : totalAngle;
                return Math.floor(totalAngle / 360) !== 0;
            

示例链接:https://jsfiddle.net/fNPvf/43106/

绿色圆圈被正确渲染,因为 startAngle 和 endAngle 是 90,即使我将角度更改为 0、180、270 和 360,它也会起作用。但实际问题是我使用 70 的红色圆圈,除了那些角度(0、90、180、270、360)之外,问题也会出现。

如何解决这个问题?

【问题讨论】:

【参考方案1】:

圆圈之间的这种差异是由于数值精度的影响。由于 SVG 弧线和浮点运算的工作方式,起点和终点的微小变化可能会在最终弧线中被夸大。弧跨的角度越大,效果就越发挥作用。

要绘制圆弧,渲染器首先需要确定圆心。如果您尝试制作 360 度的弧,您的起点和终点将几乎相同。

考虑下图:

绿点和红点是圆弧的起点和终点。灰点是渲染器为绘制圆弧而计算的圆心。

插图中的圆弧大约代表 359 度。现在想象将红点和绿点靠得更近。确定中心点所需的计算将变得越来越容易受到开始和结束坐标以及浮点算术函数的不准确性的影响。

此外,sin()cos() 函数只是 sin 和 cos 曲线的近似值。请记住,浏览器 javascript 引擎必须平衡速度和准确性。

此外,大多数(如果不是全部)SVG 渲染引擎使用贝塞尔曲线近似弧线。但是贝塞尔曲线不能完美地表示圆弧。

希望您现在可以看到为什么您会得到这样的结果。试图用一条弧线表示大角度是个坏主意。我个人的建议是一个完整的圆至少使用三到四个弧线。

function getPathArc(center, start, end, radius) 
  if (end == start) end += 360;
  var degree = end - start;
  degree = degree < 0 ? (degree + 360) : degree;
  var points = [];
  points.push( getLocationFromAngle(start, radius, center) );
  points.push( getLocationFromAngle(start+degree/3, radius, center) );
  points.push( getLocationFromAngle(start+degree*2/3, radius, center) );
  points.push( getLocationFromAngle(end, radius, center) );
  return this.getCirclePath(points, radius, (degree < 180) ? 0 : 1);

			
function getCirclePath(points, radius, clockWise) 
  return ['M', points[0].x, points[0].y,
          'A', radius, radius, 0, 0, clockWise, points[1].x, points[1].y,
          'A', radius, radius, 0, 0, clockWise, points[2].x, points[2].y,
          'A', radius, radius, 0, 0, clockWise, points[3].x, points[3].y
         ].join(' ');

			
function getLocationFromAngle(degree, radius, center) 
  var radian = (degree * Math.PI) / 180;
  return 
    x : Math.cos(radian) * radius + center.x,
    y : Math.sin(radian) * radius + center.y
  

			
document.getElementById("arc1").setAttribute("d", getPathArc(x:250,y:250, 90, 90, 200));
document.getElementById("arc2").setAttribute("d", getPathArc(x:250,y:250, 70, 70, 200));
<svg  >
  <path id="arc1" fill="none" stroke="green" stroke- />
  <path id="arc2" fill="none" stroke="red" stroke- />
</svg>

【讨论】:

很好的答案,我知道一些 SVG 专家(你)会在这里有所启发。【参考方案2】:

我会建议一个可行的解决方案,但我无法解释其确切机制(编辑:在@LeBeau answer 中解释)

这是你代码中的罪魁祸首:

end -= this.isCompleteAngle(start, end) ? 0.0001 : 0;

我们可以清楚的看到,如果我们在三元为true的时候增加值,也就是增加end变量的减法量,奇异的效果就消失了。让我们展示一下。

用一个很小的减法,只是0.00001,让奇异的效果更加明显:

function getPathArc(center, start, end, radius) 
  end -= this.isCompleteAngle(start, end) ? 0.00001 : 0;
  var degree = end - start;
  degree = degree < 0 ? (degree + 360) : degree;
  return this.getCirclePath(
    center, getLocationFromAngle(start, radius, center),
    getLocationFromAngle(end, radius, center), radius, (degree < 180) ? 0 : 1
  );


function getCirclePath(center, start, end, radius, clockWise) 

  return 'M ' + start.x + ' ' + start.y + ' A ' + radius + ' ' +
    radius + ' 0 ' + clockWise + ' 1 ' + end.x + ' ' + end.y;


function getLocationFromAngle(degree, radius, center) 
  var radian = (degree * Math.PI) / 180;
  return 
    x: Math.cos(radian) * radius + center.x,
    y: Math.sin(radian) * radius + center.y
  


function isCompleteAngle(startAngle, endAngle) 
  var totalAngle = endAngle - startAngle;
  totalAngle = totalAngle <= 0 ? (totalAngle + 360) : totalAngle;
  return Math.floor(totalAngle / 360) !== 0;


window.onload = function() 
  document.getElementById("arc1").setAttribute("d", getPathArc(
    x: 250,
    y: 250
  , 90, 90, 200));
  document.getElementById("arc2").setAttribute("d", getPathArc(
    x: 250,
    y: 250
  , 70, 70, 200));
;
<svg  >
  <path id="arc1" fill="none" stroke="green" stroke- />
  <path id="arc2" fill="none" stroke="red" stroke- />
</svg>

现在有了更大的减法,0.01

function getPathArc(center, start, end, radius) 
  end -= this.isCompleteAngle(start, end) ? 0.01 : 0;
  var degree = end - start;
  degree = degree < 0 ? (degree + 360) : degree;
  return this.getCirclePath(
    center, getLocationFromAngle(start, radius, center),
    getLocationFromAngle(end, radius, center), radius, (degree < 180) ? 0 : 1
  );


function getCirclePath(center, start, end, radius, clockWise) 

  return 'M ' + start.x + ' ' + start.y + ' A ' + radius + ' ' +
    radius + ' 0 ' + clockWise + ' 1 ' + end.x + ' ' + end.y;


function getLocationFromAngle(degree, radius, center) 
  var radian = (degree * Math.PI) / 180;
  return 
    x: Math.cos(radian) * radius + center.x,
    y: Math.sin(radian) * radius + center.y
  


function isCompleteAngle(startAngle, endAngle) 
  var totalAngle = endAngle - startAngle;
  totalAngle = totalAngle <= 0 ? (totalAngle + 360) : totalAngle;
  return Math.floor(totalAngle / 360) !== 0;


window.onload = function() 
  document.getElementById("arc1").setAttribute("d", getPathArc(
    x: 250,
    y: 250
  , 90, 90, 200));
  document.getElementById("arc2").setAttribute("d", getPathArc(
    x: 250,
    y: 250
  , 70, 70, 200));
;
<svg  >
  <path id="arc1" fill="none" stroke="green" stroke- />
  <path id="arc2" fill="none" stroke="red" stroke- />
</svg>

你看到了吗?如果减法较大,则两条弧重叠。

因此,解决方案是增加end -= someValue的差异。

进一步调查:

在调查此问题时,我console.loged 每个变量。它们几乎相同。

然后,我在两个 sn-ps 中复制了这两个元素。令我惊讶的是,它们几乎相同。看看,这是第一个sn-p中的两条路径:

<path id="arc1" fill="none" stroke="green" stroke- 
    d="M 250 450 A 200 200 0 1 1 250.0349065848627 449.9999969538258"></path>
<path id="arc2" fill="none" stroke="red" stroke- 
    d="M 318.40402866513375 437.93852415718163 A 200 200 0 1 1 318.40406146659313 437.9385122184236"></path>

现在第二个sn-p产生的两条路径:

<path id="arc1" fill="none" stroke="green" stroke- 
    d="M 250 450 A 200 200 0 1 1 250.0000349065851 449.99999999999693"></path>
<path id="arc2" fill="none" stroke="red" stroke- 
    d="M 318.40402866513375 437.93852415718163 A 200 200 0 1 1 318.4368290834931 437.92658253955653"></path>

如您所见,除了 x 轴的开始/结束值之外,红色路径几乎相同。好像越相似,效果就越大。

为了测试这一点,让我们看看如果值完全相同(黑色圆圈标记原点)会发生什么:

<svg  >
  <path id="arc1" fill="none" stroke="green" stroke- d="M 250 450 A 200 200 0 1 1 250.0349065848627 449.9999969538258"></path>
  <path id="arc2" fill="none" stroke="red" stroke- d="M 318.4368290834931 437.93852415718163 A 200 200 0 1 1 318.4368290834931 437.92658253955653"></path>
  <circle cx="318.4368290834931" cy="437.93852415718163" r="4"></circle>
</svg>

正如我之前所说,我无法解释这一点。但我相信这里的 SVG 专家之一很快就会。

【讨论】:

@Gerado 感谢老兄的宝贵意见。

以上是关于如何使用 start 和 endAngle 渲染 svg 圆的主要内容,如果未能解决你的问题,请参考以下文章

如何最好地使用批量数据json在rails中渲染?

将远程数据渲染到网格的正确程序说明

C++入门——实现“旋转蛇”错觉

unity3D 如何渲染出图,一张JPG的就行~

[Unity] Awake 与 Start 的使用, 为什么将 GetComponent 逻辑放到 Awake 中

[Unity] Awake 与 Start 的使用, 为什么将 GetComponent 逻辑放到 Awake 中