如何使用贝塞尔曲线创建圆形?

142

我们有一个起始点(x,y)和一个圆形半径。还存在一个可以从贝塞尔曲线点创建路径的引擎。

如何使用贝塞尔曲线创建圆?


密切相关:几何弧到贝塞尔曲线 - legends2k
这里有一些很棒的信息:https://math.stackexchange.com/q/873224/207316 - PM 2Ring
这个问题应该在数学相关的网站上提问,因为a) 它以一种与语言无关的方式被提出,并且b) 问题中与语言无关的部分纯粹是数学问题,与编程无关 - 假设我们拥有所描述的“引擎”并且知道如何进行必要的API调用,实际的“编程”是微不足道的,唯一有趣的是数学。 - Karl Knechtel
不是贝塞尔曲线,而是通过NURBS(非均匀有理B样条)可以精确地表示圆弧,详见https://www.ibiblio.org/e-notes/Splines/nurbs.html。 - undefined
11个回答

198

如前所述:使用贝塞尔曲线无法精确表示圆形。

为了补充其他答案:对于有n个线段的贝塞尔曲线,控制点到圆的最佳距离是(4/3)*tan(pi/(2n)),其中中心点位于圆上。

公式 n 段

因此,对于4个点,它是 (4/3)*tan(pi/8) = 4*(sqrt(2)-1)/3 = 0.552284749831

4 点案例


7
"最优距离"指的是你正在优化哪种度量标准?正如用三次贝塞尔曲线近似圆形所示,可以通过不同的值来获得可能的最低漂移程度。您是否能提供一些定义在此情况下"最优"的链接或者公式是如何推导出来的呢? - Suma
1
@Suma 这对于某些距离来说并不是最优的。最理想的情况是将曲线的中心放在圆上。如果您设置另一个条件,肯定可以做得更好。 - Kpym
4
好的,我会尝试重新表述:“控制点到圆心的距离应该使曲线的中点位于圆上”。我认为这是一个有效的决策(足够好且易于计算),但我不会称其为最优解(至少没有在说明它在哪方面是最优的情况下)。 - Suma
2
@legends2k 我使用LaTeX和TikZ生成PDF,然后将其转换为PNG。 - Kpym
2
也许对某些人有用,我编写了一个脚本来创建SVG路径命令以使用贝塞尔曲线绘制圆形:http://codereview.stackexchange.com/questions/141491/how-can-i-do-this-small-haskell-script-better-calculating-a-circle-with-bezier。我基于这个答案进行了编写。 - Dave Thomas
显示剩余10条评论

43

包含于comp.graphics.faq

摘要:

问题4.04:如何将贝塞尔曲线拟合到圆上?

有趣的是,贝塞尔曲线可以逼近圆但不能完美地拟合圆。 一种常见的逼近方式是使用四个贝塞尔曲线模拟一个圆,每个曲线的控制点距离起止点的距离为 d=r*4*(sqrt(2)-1)/3(其中r为圆的半径),方向与圆在起止点处的切线相同。这将确保贝塞尔曲线的中间点位于圆上,并且第一阶导数是连续的。
这种逼近的径向误差约为圆的半径的0.0273%。

Michael Goldapp,“用三次多项式近似圆弧”计算几何设计辅助 (#8 1991 pp.227-238)

Tor Dokken和Morten Daehlen,“连续曲率贝塞尔曲线的良好逼近圆”的计算几何设计辅助 (#7 1990 pp. 33-41)。http://www.sciencedirect.com/science/article/pii/016783969090019N(需付费阅读)

还可参见免费文章:http://spencermortensen.com/articles/bezier-circle/

浏览器和Canvas元素。

请注意,一些浏览器在canvas绘制圆弧时使用贝塞尔曲线,Chrome使用(目前)四个扇区的方法,而Safari使用八个扇区的方法,这种差异仅在高分辨率下才能察觉到,因为只有0.0273%,并且只在平行和不同相位的弧线绘制时真正可见,您会注意到弧线会从真正的圆形波动。当弧线围绕其径向中心旋转时,效果也更加明显,通常半径为600像素时会有所差异。

某些绘图API没有真正的弧形渲染功能,因此它们也使用贝塞尔曲线。例如Flash平台没有弧形绘制API,所以任何提供弧线的框架通常都使用相同的贝塞尔曲线方法。

请注意,浏览器中的SVG引擎可能使用不同的绘图方法。

其他平台

无论你试图使用哪个平台,最好检查一下弧形绘制是如何完成的,这样你就可以预测出类似的视觉错误并进行适应。


谢谢,我会替换它。 - ocodo

41
问题的答案很好,所以没有什么可补充的。受此启发,我开始进行实验,通过四条贝塞尔曲线逐步减少到一条来直观地验证解决方案。令人惊奇地是,我发现用三条贝塞尔曲线绘制的圆形对我而言已经足够好看了,但构造方法有点棘手。实际上,我使用Inkscape在红色3像素宽度的圆(由Inkscape生成)上放置黑色1像素宽度的贝塞尔近似曲线。为了说明问题,我添加了蓝色线条和面板,显示出贝塞尔曲线的边界框。

请自行查看我的结果:

1 条曲线图表(看起来像一个挤在角落里的水滴,只是为了完整):enter image description here

2 条曲线图表:enter image description here

3 条曲线图表:enter image description here

4 条曲线图表:enter image description here

(我想在这里放SVG或PDF文件,但不支持)


1
到目前为止,SVG 可以作为 HTML 代码片段包含在内。例如,请参见此答案:https://dev59.com/EWw05IYBdhLWcg3wcRYj#32162431 - T S
1
@T S:当我试图用我拥有的SVG替换图形时,我意识到我在今年年初被盗的USB存储器中丢失了它们。如果时间允许,我会尽快重新创建它们。但是,如果SVG可以作为XML代码添加(而不是显示为图形),那么在这里就没有太多意义了。 - U. Windl
如果您的浏览器支持SVG,则图像将在单击“运行代码片段”后立即呈现(显然,在stackoverflow的移动版本上没有该按钮...)。请参见我链接的答案。 - T S
1
@TS:对于较长的文件来说,我认为这太丑了。 - U. Windl

20

对于只是寻找代码的人: 四个部分的解决方案

https://jsfiddle.net/nooorz24/2u9forep/12/

var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");

function drawBezierOvalQuarter(centerX, centerY, sizeX, sizeY) {
    ctx.beginPath();
    ctx.moveTo(
        centerX - (sizeX),
        centerY - (0)
    );
    ctx.bezierCurveTo(
        centerX - (sizeX),
        centerY - (0.552 * sizeY),
        centerX - (0.552 * sizeX),
        centerY - (sizeY),
        centerX - (0),
        centerY - (sizeY)
    );
    ctx.stroke();
}

function drawBezierOval(centerX, centerY, sizeX, sizeY) {
    drawBezierOvalQuarter(centerX, centerY, -sizeX, sizeY);
    drawBezierOvalQuarter(centerX, centerY, sizeX, sizeY);
    drawBezierOvalQuarter(centerX, centerY, sizeX, -sizeY);
    drawBezierOvalQuarter(centerX, centerY, -sizeX, -sizeY);
}

function drawBezierCircle(centerX, centerY, size) {
    drawBezierOval(centerX, centerY, size, size)
}

drawBezierCircle(200, 200, 64)
<canvas id="myCanvas" width="400" height="400" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.</canvas>

这样可以绘制由4个贝塞尔曲线组成的圆。 用JS编写,但可以轻松转换为任何其他语言。
注意:
如果不是必须使用SVG路径来绘制圆形,请不要使用贝塞尔曲线。在路径中,您可以使用“Arc”创建两个半圆。 使用SVG的弧路径绘制圆形

非常有帮助,谢谢!为了使这4个部分按顺序排列,需要做哪些更改?我需要沿着路径编写文本,但现在它散落在这4个部分周围。 - Alexa
上面的代码似乎缺少象限的内部部分,它只是关闭了圆角外形。我认为在 bezierCurveTo() 之后再加上另一个 ctx.lineTo(centerX, centerY); 就可以解决这个问题。 - drott
好的,现在请提供起始弧到结束弧的代码。 - undefined

15
已经有很多答案了,但我找到了一篇非常好的小型在线文章,其中提供了一个非常好的立方贝塞尔圆近似值。对于单位圆而言,c = 0.55191502449,其中c是沿着切线到截距点的距离。
作为一个单象限的单位圆,两个中间坐标是控制点。(0,1),(c,1),(1,c),(1,0) 径向误差仅为0.019608%,因此我只需要将其添加到答案列表中即可。
该文章可以在这里找到:用三次贝塞尔曲线近似圆形

9
你读过 Stackoverflow 的 Mike 'Pomax' Kamermans 所写的这篇精彩论文,介绍贝塞尔曲线吗?非常值得一读! :-) - markE
1
@markE 非常感谢您提供的链接,这是我见过的关于这个主题最好的论文之一。迫不及待地想仔细研究一下... :D 谢谢... - Blindman67
1
当圆的半径超过2551像素时,图形将出现0.019608%的误差,而不是那令人不安的0.027253%。在1835像素处,出现了2个像素错误的情况下,图形引擎会更改像素,导致一个半像素的错误(即0.5个像素)。 - Tatarize
@Tatarize 这篇文章没有说明如何测量误差,它说最大径向漂移?我认为误差在曲线0<= t <= 1上最小,以匹配象限0 <= pheta <= Pi/2,在t = 0 = 1/2 = 1时等于pheta = 0 = Pi/4 = Pi/4,误差为0.019608%,t = ~0.1822和t = ~ 0.8177处的最大误差为0.019608%(符号?),但在这些点上t不等于pheta,误差是否包括角度漂移?4个像素可能正确,也可能不正确。误差可能是方差,因此对于r = 2551,误差<2pix。有很多需要调查的问题。 - Blindman67
我非常确定,通过查看错误曲线,给定的调整只是将点向下移动足够多,以使最大误差在弧线上方等于弧线下方的最大误差。也就是说,我们将曲线向下改变一点,这样所有的误差都不是正数。这个调整意味着我们要穿过弧线4次,有4个最大误差点。当原始规定的线在t=.25和t=.75处有2个点时。通过调整,它应该在t=.125、t=.375、t=.625和t=.875处。这假设我们使用实心像素而不是抗锯齿,后者会在14px处发生变化。 - Tatarize

11

这是不可能的。贝塞尔曲线是三次曲线(至少是最常用的)。由于圆包含平方根,因此无法使用三次曲线精确表示圆的方程。因此,您必须进行逼近。

为了做到这一点,您需要将圆分成n个部分(例如四分之一、八分之一)。对于每个部分,您使用第一个和最后一个点作为贝塞尔曲线的起点和终点。贝塞尔多边形需要两个额外的点。为了快速计算,我会针对每个部分的极端点取切线,并选择两条切线的交点作为这两个额外的点(以便基本上您的贝塞尔多边形是一个三角形)。增加n的数量以适应您的精度。


4
只要使用无限数量长度为零的贝塞尔曲线,就可以实现。这基本上是无限数量的点,或者更确切地说是弧形曲线。 - Tatarize
一个圆不能用一个三次曲线完全表达,因为一个圆在它的方程中包含了平方根。这种措辞暗示了立方曲线有一些特殊之处 - 它是否适用于其他任何贝塞尔曲线的程度?(据我理解,不适用。) - Karl Knechtel

8

4

我不确定是否应该开新问题,因为这是关于近似值的问题,但我对获得任何次数的Bezier曲线控制点的通用公式感兴趣,并且我相信它适用于此问题。

我在网上找到的所有解决方案都只适用于三次曲线或需要付费,或者我甚至无法理解(我数学不太好)。

所以我决定自己解决这个问题。我正在研究控制点与给定角度的圆心距离,到目前为止我发现:

enter image description here

其中N是单个曲线的控制点数量,α是圆弧角度。

对于二次曲线,它可以简化为l ≈ r + r * PI*0.1 * pow(α/90, 2) PI*0.1是一个猜测 - 我没有计算出完美的值,但它非常接近。 这对于具有1-2个控制点的曲线效果相当不错,对于三次曲线,半径误差约为0.2%。对于更高阶的曲线,精度损失是明显的。对于具有3个控制点的曲线,曲线看起来类似于二次曲线,所以显然我漏掉了什么,但我无法弄清楚,并且这种方法通常适合我的需求。

这里是演示


你用什么软件创建这个图像? - Qian Sijianhao
1
我的演示屏幕截图 + Win 7的数学书写面板(或者翻译成其他名称)+ MS Paint - Paweł Audionysos

1

如果您需要@NoOorZ24答案的纯JS版本。这将返回一个SVG路径:

function drawBezierOvalQuarter(centerX, centerY, sizeX, sizeY) {
  return `
  M ${centerX - sizeX} ${centerY}
  C ${centerX - sizeX} ${centerY - 0.552 * sizeY},
    ${centerX - 0.552 * sizeX} ${centerY - sizeY},
    ${centerX} ${centerY - sizeY}
  `;
}

function drawBezierOval(centerX, centerY, sizeX, sizeY) {
  return (
    drawBezierOvalQuarter(centerX, centerY, -sizeX, sizeY) +
    drawBezierOvalQuarter(centerX, centerY, sizeX, sizeY) +
    drawBezierOvalQuarter(centerX, centerY, sizeX, -sizeY) +
    drawBezierOvalQuarter(centerX, centerY, -sizeX, -sizeY)
  );
}

-1

很抱歉从死亡区域中找回这篇文章,但我发现这篇文章和this页面一起对创建可伸缩公式有很大帮助。

基本上,你可以使用一个非常简单的公式创建一个接近圆形的形状,该公式允许您使用超过4个贝塞尔曲线:距离=半径*步角/3

其中距离是贝塞尔控制点与弧线最近端之间的距离,半径是圆的半径,而步角则是表示由两个π/(曲线数量)表示的弧末端之间的角度。

因此,为了一次性完成它:距离=半径*2π/(曲线数量)/3


3
这不是圆的最佳近似。最好的近似公式是 Distance = (4/3)*tan(pi/2n)。对于大量的弧,它几乎相同,因为 tan(pi/2)~pi/2n,但例如对于 n=4(这是最常用的情况),你的公式给出了 Distance=0.5235...,但最优的公式是 Distance=0.5522...(所以你有约5%的误差)。 - Kpym

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接