以贝塞尔曲线渲染一个SVG椭圆弧
本文翻译自:https://mortoray.com/2017/02/16/rendering-an-svg-elliptical-arc-as-bezier-curves/
我需要绘制椭圆和圆弧,如果没有它们就不能完成一个向量API。但苹果的核心图形库除了轴对齐圆弧外没有其他的东西。安卓有椭圆,但似乎也限制它轴对齐。这意味着我要将椭圆转换成一组贝塞尔曲线,用于这些后端API。
在本文中,另请参阅The smooth sexy curves of a bezier spline (平滑性感的贝塞尔曲线)和Stuffing curves into boxes: calculating the bounds (让方框囊括曲线:计算边界)。
SVG圆弧表示法
SVG的圆弧指令允许绘制任何一种想要的弧形。由于Fuse的旧Path
已经允许SVG路径数据,所以使用这个弧线的定义似乎是合理的。
SVG通过通过它们的端点定义弧:在哪里开始和结束。这些再与椭圆的半径和旋转组合。基于以上参数,就有四个可能的弧。然后还要囊括两个奇怪的标志来选择对应弧。
1 | a radius-x radius-y x-axis-rotation large-arc-flag sweep-flag x y |
这是从绘图角度指定弧的一种好方法。你知道两点相连,你也知道你想要哪个扫掠角。但问题是,这不是一个实际绘制弧线的好办法。它需要转换为一个中心点和要绘制的角度范围。
端点转换到中心点
SVG附录“椭圆弧实现注释”中有一个“从端点到中心点的参数化转换”算法。这很有帮助,因为该圆弧标记法有些不常见,很难在别处找到这个算法。
然而,它也有一些问题。标准中的示例弧实际上错了!它需要修复一下。
平方根
首个问题就是F6.5.2(下图公式)中的平方根:
当试图开方一个负数时,会产生一个虚数,它会变成$NaN$。每当输入的半径不够大,两个端点与椭圆链接时,就会出现这种问题。即使原数是一致的,浮点精度误差也会导致这个值略负。这对开方来说是不可忽略的:sqrt(0)
是$0$,但sqrt(-0.000001)
是$NaN$。
我对它的修复如下。从规范中我们分离出$\sqrt{pq}$如下所示:
问题是当所给的$cr > 1$时,$pq < 0$:
考虑到这个系数,很容易地扩大半径。我们将半径乘以$\sqrt{cr}$。这将准确地将$cr$值减少到1(尽管如此,仍需要注意浮点精度问题),导致$pq == 0$。
完全方程在本文的末尾。我将执行
sqrt(max(0, pq))
;由于精确性,在放大r之后,pq值仍可能为负(实际上在使用中,出现了轻微的负值)。
反余弦
该算法还包括计算两个矢量之间的夹角。虽然已经有了计算的函数,但我还是决定使用他们的方程来确保$±$部分能够按预期工作。
通常情况下,我们需要担心这里的除法,但这个算法中的预过滤确保我们有非零的长度。
但它不能保证arccos
的参数是有效的。由于浮点精度(又是它),该值可能略大于1,或略小于1。这对于arccos
是不可忽视的,这在那些情况下仅返回一个NaN
(准确地说结果是一个虚数)。在我的代码中,我添加了一个clamp
方法来防止它。
clamp
是有效的,因为理论上,方程不能产生范围在-1~1之外的值。这是一个测定过的角度,它有固定的数值范围。我得到的超出范围的值也只是略微超出范围而已。
从弧到贝塞尔曲线
我们可以使用这个中心点表示法把圆弧转换为一组贝塞尔曲线。这涉及到很多东西:椭圆的参数方程,它的倒数,以及一些我没有导出的复杂公式。
值得庆幸的是,L. Maisonobe发表了一篇Drawing an elliptical arc using polylines, quadratic or cubic Bézier curves(使用折线、二次或三次贝塞尔曲线绘制椭圆弧)的论文。我所要做的就是读它并翻译成代码。
参数
首先是获得椭圆的参数方程。椭圆的标准方程如下:
这告诉我们,给定的x, y
点是否位于椭圆上。但这对绘制椭圆不是很有用。取而代之的是,我们想要得到一个参数化的形式。下面是一个函数,传递一个值t
,这表示椭圆上的一个伪角度,然后返回x, y
坐标。这其中包括椭圆半径与X轴的转角(SVG弧必须的)。
1 | static public float2 EllipticArcPoint( float2 c, float2 r, float xAngle, float t ) |
我提到t
是一个伪夹角,它不是一个真正的“角度”。它的角度是基于“如果一个人认为椭圆是一个被拉伸然后旋转的圆”(这是SVG规范中的用词)。
这个方程式的目的是,我们可以在圆弧定义的起始角和终止角之间迭代,以找到椭圆上的点。第二个函数叫做椭圆弧导数。这两个函数让我们计算一个近似于任何弧线的贝塞尔曲线。下表是我们所需的。
其中$\mathit{E}$是椭圆弧点函数,$\mathit{E}’$是椭圆弧导数函数,$\eta_1$和$\eta_2$是我们正在近似计算的弧的起始角和结束角。
我所要做的就是把角度范围细分成小节以获得良好的近似值。我不太理解这篇论文的误差计算,但是我发现Joe Cridge的另一篇论文指出$\pi/2$的分割方式在相当高分辨率的设备上有潜在的一个像素误差。因此,我选择了$\pi/4$来保证平滑,即使是在高像素密度的移动设备上。
一个椭圆
把之前的一切组合在一起,我们就能够从SVG渲染示例了。这是建立在向量API上的,我从上一篇文章开始,对平滑性感的贝塞尔曲线进行了研究。我的工作后端是苹果核心图形,但这个代码也将运行在安卓Canvas和Windows的System.Drawing上。通过自己计算贝塞尔曲线,我们不需要限制后端绘制弧的能力了。
本系列还有一篇文章即将面世。我们仍然需要计算这些图形的边界。这是衍生的另一件奇遇。
我在Fuse上的作品充满了有趣的代码。关注我的Twitter或者Facebook以获得更多的见解和轶事。如果有什么特别的跨平台工具激起你的好奇心,请让我知道。
附录:端点到中心弧转换
这是Uno代码(和本文发布时间一样)用于从SVG弧转换为中心点表示法。
1 | /** |
附录:JavaScript完整实现
本代码来自:StackOverflow
1 | var canvas = document.getElementById("canvas"); |