如何使用贝塞尔曲线最佳逼近几何圆弧?

时间:2009-04-09 12:51:59

标签: math graphics geometry

使用Bezier曲线近似在2D中绘制圆弧时,如果您有圆心,起点和终点角以及半径,如何计算两个控制点?

9 个答案:

答案 0 :(得分:17)

这在StackOverflow帖子中不易解释,特别是因为向您证明它将涉及许多详细步骤。但是,您所描述的是一个常见问题,并且有许多详尽的解释。见 here here ;我非常喜欢#2并且之前使用过它。

答案 1 :(得分:12)

这是一个有8年历史的问题,但是我最近一直在努力解决这个问题,所以我想我会分享我的想法。我花了很多时间尝试使用来自this text的解决方案(9)并且在我做了一些谷歌搜索之前无法得到任何合理的数字并且了解到,显然,方程式中存在一些拼写错误。根据{{​​3}}中列出的修正,给定弧的起点和终点(分别为[x1,y1]和[x4,y4])和圆心([xc,yc]),可以得出三次贝塞尔曲线([x2,y2]和[x3,y3])的控制点如下:

ax = x1 – xc
ay = y1 – yc
bx = x4 – xc
by = y4 – yc
q1 = ax * ax + ay * ay
q2 = q1 + ax * bx + ay * by
k2 = 4/3 * (√(2 * q1 * q2) – q2) / (ax * by – ay * bx)


x2 = xc + ax – k2 * ay
y2 = yc + ay + k2 * ax
x3 = xc + bx + k2 * by                                 
y3 = yc + by – k2 * bx

希望这可以帮助除我以外的人!

答案 2 :(得分:5)

Wolfram MathWorld中有Mathematica代码:Bézier Curve Approximation of an Arc,它可以帮助你入门。

另见:

答案 3 :(得分:4)

Raphael 2.1.0支持Arc-> Cubic(path2curve-function),修复了S和T路径规范化中的错误后,它似乎现在正常工作。我更新了 * 随机路径生成器 * ,以便它只生成弧,因此可以轻松测试所有可能的路径组合:

http://jsbin.com/oqojan/53/

测试,如果某条路径失败,我很乐意收到报告。

编辑:刚才意识到这是3岁的线程......

答案 4 :(得分:4)

“近似”Cubic Bezier Curve by Circular Arcs

中提供了一个很好的解释

长话短说:使用Bezier曲线可以实现1.96×10 ^ -4的最小误差,这对于大多数应用来说都是相当不错的。

对于正象限弧,请使用以下几点:

p0 = [0, radius]

p1 = [radius * K, radius]  

p2 = [radius, radius * K]

p3 = [radius, 0]

其中K是所谓的“幻数”,这是一个非有理数。它可以近似如下:

K = 0.5522847498

答案 5 :(得分:3)

我正在回答这个老问题(这应该属于数学,所以写公式会很糟糕)并进行一些演示。

假设 P0 P3 是弧的初始和最终点, P1 P2 控件Bézier曲线的点, x 是角度除以2的度量。假设 x 小于 pi / 2。

PM 段的中点 P0P3 PH 弧的中点。为了逼近弧线,我们希望Bézier曲线从 P0 开始,通过 PH ,以 P3 结束,并与弧相切在 P0 P3

(点击“运行代码片段”显示图。诅咒仍然不支持SVG。)

<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 20 80 80">
    <style>text{font-size:40%;font-style:italic;text-anchor:middle}tspan{font-size:50%;font-style:normal}</style>
    <rect x="10" y="20" width="80" height="80" fill="none" stroke="gray"></rect>
    <path stroke="gray" stroke-dasharray="3,2" fill="none" d="M25,30 62.6,31.62 80,65 22.19,95.13 25,30 80,65 M22.19,95.13 62.6,31.62"></path>
    <path stroke="black" fill="none" d="M25,30A65.19 65.19 0 0 1 80,65"></path>
    <circle r="1" fill="red" cx="25" cy="30"></circle>
    <circle r="1" fill="green" cx="80" cy="65"></circle>
    <circle r="1" fill="magenta" cx="22.19" cy="95.13"></circle>
    <circle r="1" fill="darkgreen" cx="52.5" cy="47.5"></circle>
    <circle r="1" fill="yellow" cx="57.19" cy="40.13"></circle>
    <circle r="1" fill="maroon" cx="62.6" cy="31.62"></circle>
    <circle r="1" fill="orange" cx="48.27" cy="31"></circle>
    <circle r="1" fill="teal" cx="69.24" cy="44.35"></circle>
    <text x="25" y="28">P<tspan>0</tspan></text>
    <text x="48.27" y="29">P<tspan>1</tspan></text>
    <text x="71.24" y="42.35">P<tspan>2</tspan></text>
    <text x="83" y="63">P<tspan>3</tspan></text>
    <text x="62.6" y="29.62">P<tspan>E</tspan></text>
    <text x="59.19" y="47.13">P<tspan>H</tspan></text>
    <text x="54.5" y="54.5">P<tspan>M</tspan></text>
</svg>

PE P0 P3 中的弧相切的线的交点。为了使曲线与弧相切, P1 必须位于 P0PE 段上,而 P2 必须位于 P3PE上。设 k 为比率 P0P1 / P0PE (也等于 P3P2 / P3PE ):

  

P1 =(1 - k P0 + k PE < / p>      

P2 =(1 - k P3 + k PE < / p>

我们还有以下(做一些比例):

  

PM =( P0 + P3 )/ 2

     

PH = PM / cos( x )= PM sec( x )=( P0 + P3 )秒( x )/ 2

     

PE = PH / cos( x )= PM sec( x )^ 2 =( P0 + P3 )秒( x )^ 2/2

为了简化我们的计算,我认为所有的矢量点都是基于中心的,但最终它并不重要。

通用的4点Bézier曲线由公式

给出
  

C t )= t ^ 3 P3 + 3(1 - t < / em>) t ^ 2 P2 + 3(1 - t )^ 2 t P1 +(1 - t )^ 3 P0

我们必须 C (1/2)= PH ,所以

  

C (1/2)=( P0 + 3 P1 + 3 P2 + P3 )/ 8

     

=(( P0 + P3 )+ 3(1 - k P0 + 3 k PE + 3(1 - k P3 + 3 k PE )/ 8

     

=(( P0 + P3 )+ 3(1 - k )( P0 + P3 )+ 6 k PE )/ 8

     

=( P0 + P3 )(1 + 3(1 - k )+ 3 k 秒( x )^ 2)/ 8

所以,这是我们的等式(乘以8)来找到 k

  

8 C (1/2)= 8 PH

     

=&GT; ( P0 + P3 )(4 - 3 k + 3 k 秒( x )^ 2)= 4( P0 + P3 )秒( x

让我们摆脱向量( P0 + P3 ),我们得到:

  

4 - 3 k + 3 k sec( x )^ 2 = 4秒( x

     

=&GT; 3 k (秒( x )^ 2 - 1)= 4(秒( x ) - 1)

     

=&GT; k = 4/3(秒( x )+ 1)

现在您知道放置控制点的位置了。万岁!

如果你有 x = pi / 4,你会得到 k = 0.552 ......你可能已经看到了这个值

在处理椭圆弧时,您所要做的就是相应地缩放点的坐标。

如果你需要处理更大的角度,我建议将它们分成更多的曲线。这实际上是一些软件在绘制弧时所做的事情,因为计算贝塞尔曲线有时比使用正弦和余弦更快。

答案 6 :(得分:2)

我以general solution for any elliptical arc作为立方贝塞尔曲线取得了成功。它甚至包括配方中的起始角和终止角,因此不需要额外的旋转(这对于非圆形椭圆来说是个问题)。

答案 7 :(得分:1)

我最近偶然发现了这个问题。我以模块的形式从这里提到的文章中编译了一个解决方案。

它接受起始角度,结束角度,中心和半径作为输入。

它非常接近小弧(&lt; = PI / 2)。如果你需要近似从PI / 2到2 * PI的某些弧,你可以总是将它们分成几部分&lt; PI / 2,计算相应的曲线并在之后加入它们。

此解决方案是开始和结束角度顺序无关 - 它总是选择次要弧。

因此,您可以获得在绝对坐标中定义三次贝塞尔曲线所需的全部四个点。

我认为最好在代码和评论中解释:

'use strict';

module.exports = function (angleStart, angleEnd, center, radius) {
    // assuming angleStart and angleEnd are in degrees
    const angleStartRadians = angleStart * Math.PI / 180;
    const angleEndRadians = angleEnd * Math.PI / 180;

    // Finding the coordinates of the control points in a simplified case where the center of the circle is at [0,0]
    const relControlPoints = getRelativeControlPoints(angleStartRadians, angleEndRadians, radius);

    return {
        pointStart: getPointAtAngle(angleStartRadians, center, radius),
        pointEnd: getPointAtAngle(angleEndRadians, center, radius),
        // To get the absolute control point coordinates we just translate by the center coordinates
        controlPoint1: {
            x: center.x + relControlPoints[0].x,
            y: center.y + relControlPoints[0].y
        },
        controlPoint2: {
            x: center.x + relControlPoints[1].x,
            y: center.y + relControlPoints[1].y
        }
    };
};

function getRelativeControlPoints(angleStart, angleEnd, radius) {
    // factor is the commonly reffered parameter K in the articles about arc to cubic bezier approximation 
    const factor = getApproximationFactor(angleStart, angleEnd);

    // Distance from [0, 0] to each of the control points. Basically this is the hypotenuse of the triangle [0,0], a control point and the projection of the point on Ox
    const distToCtrPoint = Math.sqrt(radius * radius * (1 + factor * factor));
    // Angle between the hypotenuse and Ox for control point 1.
    const angle1 = angleStart + Math.atan(factor);
    // Angle between the hypotenuse and Ox for control point 2.
    const angle2 = angleEnd - Math.atan(factor);

    return [
        {
            x: Math.cos(angle1) * distToCtrPoint,
            y: Math.sin(angle1) * distToCtrPoint
        },
        {
            x: Math.cos(angle2) * distToCtrPoint,
            y: Math.sin(angle2) * distToCtrPoint
        }
    ];
}

function getPointAtAngle(angle, center, radius) {
    return {
        x: center.x + radius * Math.cos(angle),
        y: center.y + radius * Math.sin(angle)
    };
}

// Calculating K as done in https://pomax.github.io/bezierinfo/#circles_cubic
function getApproximationFactor(angleStart, angleEnd) {
    let arc = angleEnd - angleStart;

    // Always choose the smaller arc
    if (Math.abs(arc) > Math.PI) {
        arc -= Math.PI * 2;
        arc %= Math.PI * 2;
    }
    return (4 / 3) * Math.tan(arc / 4);
}

答案 8 :(得分:0)

Swift 基于@ k88lawrence answer

的解决方案

适用于圆弧<= PI / 2

func controls(center: CGPoint, start: CGPoint, end: CGPoint) -> (CGPoint, CGPoint) {
    let ax = start.x - center.x
    let ay = start.y - center.y
    let bx = end.x - center.x
    let by = end.y - center.y
    let q1 = (ax * ax) + (ay * ay)
    let q2 = q1 + (ax * bx) + (ay * by)
    let k2 = 4 / 3 * (sqrt(2 * q1 * q2) - q2) / ((ax * by) - (ay * bx))
    let control1 = CGPoint(x: center.x + ax - (k2 * ay), y: center.y + ay + (k2 * ax))
    let control2 = CGPoint(x: center.x + bx + (k2 * by), y: center.y + by - (k2 * bx))
    return (control1, control2)
}