How to best approximate a geometrical arc with a Bezier curve?
Asked Answered
B

9

45

When drawing an Arc in 2D, using a Bezier Curve approximation, how does one calculate the two control points given that you have a center point of a circle, a start and end angle and a radius?

Beeves answered 9/4, 2009 at 12:51 Comment(3)
Is it correct to assume that you're trying to approximate a circular arc?Savior
math.stackexchange.com/questions/11698/…Cyrano
The best way is to be rational bezier curves with a bernstein weighted value such that it exactly matches the curved section.Federate
A
20

This isn't easily explained in a StackOverflow post, particularly since proving it to you will involve a number of detailed steps. However, what you're describing is a common question and there's a number of thorough explanations. See here and here; I like #2 very much and have used it before.

Artemus answered 9/4, 2009 at 13:7 Comment(4)
typo near the end of #2: "x3=x1" should be "x3=x0".Crescint
I have a slightly more generic explanation over at pomax.github.io/bezierinfo/#circles_cubic, which covers the second link's conclusion, while explaining what the coordinates would be for any arc length rather than for arcs with angles up to a quarter circle.Philter
@Mike'Pomax'Kamermans I love how that article throws a bunch of formulas and the source code is completely different and only seem to work for a circle of a radius of 1.Quartus
The circle radius is irrelevant, you just scale the value by however much your own circle's radius is. Got a circle with radius 100? Use 55.1785 instead of 0.551785.Philter
T
38

This is an 8-year-old question, but one that I recently struggled with, so I thought I'd share what I came up with. I spent a lot of time trying to use solution (9) from this article by Aleksas Riškus and couldn't get any sensible numbers out of it until I did some Googling and learned that, apparently, there were some typos in the equations. Per the corrections listed in this blog post, given the start and end points of the arc ([x1, y1] and [x4, y4], respectively) and the the center of the circle ([xc, yc]), one can derive the control points for a cubic Bézier curve ([x2, y2] and [x3, y3]) as follows:

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

Hope this helps someone other than me!

Titled answered 29/6, 2017 at 15:29 Comment(4)
Works perfect for arcs <= PI / 2 (90º)Stickpin
A note for Java and Processing users: k2 calculate must be with floats, therefore write 4./3 or 4f/3 for the correct result.Reformism
This works great. Do you know of a solution that does the opposite?Unreflecting
This formula really looks like coming from Aleksas Riškus article, also cited in @Nic answer.Venipuncture
A
20

This isn't easily explained in a StackOverflow post, particularly since proving it to you will involve a number of detailed steps. However, what you're describing is a common question and there's a number of thorough explanations. See here and here; I like #2 very much and have used it before.

Artemus answered 9/4, 2009 at 13:7 Comment(4)
typo near the end of #2: "x3=x1" should be "x3=x0".Crescint
I have a slightly more generic explanation over at pomax.github.io/bezierinfo/#circles_cubic, which covers the second link's conclusion, while explaining what the coordinates would be for any arc length rather than for arcs with angles up to a quarter circle.Philter
@Mike'Pomax'Kamermans I love how that article throws a bunch of formulas and the source code is completely different and only seem to work for a circle of a radius of 1.Quartus
The circle radius is irrelevant, you just scale the value by however much your own circle's radius is. Got a circle with radius 100? Use 55.1785 instead of 0.551785.Philter
A
12

A nice explanation is provided in "Approximation of a cubic bezier curve by circular arcs and vice versa"1, by Aleksas Riškus.

Long story short: using Bezier curves you can achieve a minimum error of 1.96×10^-4, which is pretty ok for most applications.

For a positive quadrant arc, use the following points:

p0 = [0, radius]

p1 = [radius * K, radius]  
 
p2 = [radius, radius * K]

p3 = [radius, 0]

where K is a so-called "magic number", which is an non-rational number. It can be approximated as follows:

K = 0.5522847498
Aria answered 15/6, 2016 at 14:3 Comment(4)
K is a function of the arc. K = 0.5522847498 is only valid if the arc is of PI/2 radians.Pseudo
And if you're going to use it for quarter tau sections you're better off using Mortensen's value at 0.55191502449351057, to minimize the positive and negative error. And if you want to minimize the overall error oddly enough 0.552 is better than both of those.Federate
The total error for the Naive geometric value of ((4/3)*(sqrt(2) - 1) is 1401 over 10000000 samples. 1180 with Mortensen's and 1160 at .552.Federate
@Federate To minimize total squared error, the best value is 0.5519703814011128603134107. To minimize maximum error, the best value is Mortensen's, which to some more precision is 0.5519150244935105707435627.Whipstock
U
7

I'm answering to this old question (which should belong to Mathematics so writing the formulas is gonna be awful) with some demonstrations.

Suppose P0 and P3 are your initial and final point of your arc, P1 and P2 the control points of the Bézier curve, and x is the measure of the angle divided by two. Suppose x to be less that pi/2.

Let PM the midpoint of the segment P0P3 and PH the middle point of the arc. To approximate the arc, we want the Bézier curve to start in P0, pass through PH, end in P3, and be tangent to the arc in P0 and P3.

(Click on "Run code snippet" to show the figure. Curses to imgur still not supporting 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>

Let PE the intersection of the lines tangent to the arc in P0 and P3. In order for the curve to be tangent to the arc, P1 must lie on the segment P0PE, and P2 must lie on P3PE. Let k be the ratio P0P1/P0PE (also equal to P3P2/P3PE):

P1 = (1 - k)P0 + k PE

P2 = (1 - k)P3 + k PE

We also have the following (do some proportions):

PM = (P0 + P3) / 2

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

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

To simplify our computations, I've considered all vector points to be center-based, but in the end it won't matter.

The generic 4-points Bézier curve is given by the formula

C(t) = t^3 P3 + 3(1 - t)t^2 P2 + 3(1 - t)^2 t P1 + (1 - t)^3 P0

We must have C(1/2) = PH, so

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 sec(x)^2) / 8

So, this is our equation (multiplied by 8) to find k:

8 C(1/2) = 8 PH

=> (P0 + P3)(4 - 3 k + 3 k sec(x)^2) = 4(P0 + P3) sec(x)

Let's get rid of the vectors (P0 + P3), and we get:

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

=> 3 k (sec(x)^2 - 1) = 4(sec(x) - 1)

=> k = 4 / ( 3 * (sec(x) + 1) )

Now you know where to place the control points. Hooray!

If you have x = pi/4, you'll get k = 0.552... You might have seen this value around.

When dealing with elliptic arcs, all you have to do is to scale the points' coordinates accordingly.

If you have to deal with larger angles, I suggest to split them in more curves. That's actually what some softwares do when drawing arcs, since computing a Bézier curve is sometimes faster than using sines and cosines.

Unroot answered 30/10, 2016 at 10:11 Comment(2)
It's hard to practically grasp when the formulas don't explicitely state the x or y values for the control points. Are you able to provide practical formulas such as c1x = (...), c1y = (...), c2x = (...), c2y =(...)?Quartus
@Quartus You can derive them from the formula for C(t), e.g. Cx(t) = t^3 P3x + 3(1 - t)t^2 P2x + 3(1 - t)^2 t P1x + (1 - t)^3 P0xUnroot
P
5

There's Mathematica code at Wolfram MathWorld: Bézier Curve Approximation of an Arc, which should get you started.

See also:

Plangent answered 9/4, 2009 at 13:6 Comment(1)
A working link explaining the same thing would be pomax.github.io/bezierinfo/#circles_cubicPhilter
M
4

Raphael 2.1.0 has support for Arc->Cubic (path2curve-function), and after fixing a bug in S and T path normalization, it seems to work now. I updated *the Random Path Generator* so that it generates only arcs, so it's easy test all possible path combinations:

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

Test and if some path fails, I'd be happy to get report.

EDIT: Just realized that this is 3 years old thread...

Mulvaney answered 28/10, 2012 at 2:13 Comment(0)
C
2

I've had success with this general solution for any elliptical arc as a cubic Bezier curve. It even includes the start and end angles in the formulation, so there's no extra rotation needed (which would be a problem for a non-circular ellipse).

Crescint answered 8/4, 2011 at 0:4 Comment(0)
P
1

I stumbled upon this problem recently. I compiled a solution from the articles mentioned here in the form of a module.

It accepts start angle, end angle, center and radius as input.

It approximates small arcs (<= PI/2) pretty well. If you need to approximate something arcs from PI/2 to 2*PI you can always break them in parts < PI/2, calculate the according curves and join them afterward.

This solution is start and end angle order agnostic - it always picks the minor arc.

As a result you get all four points you need to define a cubic bezier curve in absolute coordinates.

I think this is best explained in code and comments:

'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);
}
Pseudo answered 11/4, 2017 at 15:52 Comment(0)
S
0

Swift solution based on @k88lawrence answer

Works for arcs <= 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)
}
Stickpin answered 1/1, 2020 at 15:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.