How to best approximate a geometrical arc with a B

2019-01-13 16:11发布

问题:

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?

回答1:

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.



回答2:

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 text 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 bezier 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 * (√(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!



回答3:

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

See also:

  • Drawing a circle with Bézier Curves
  • Approximation of Circle Using Cubic Bezier Curve


回答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...



回答5:

A nice explanation is provided in "Approximation of a "Cubic Bezier Curve by Circular Arcs"

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


回答6:

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.



回答7:

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).



回答8:

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);
}