Drawing arc with bezier curves

2020-04-17 08:35发布

问题:

I am trying to draw an arc using bezier curves. I have learned that you can't draw a perfect circle using bezier curves but you can come close. Unfortunately the math is too complicated and I can't personally figure it out.

I can create the A1 slice below as a triangle, but I can't figure out how to determine the control points. Also if I try drawing a slice out of a circle in the opposite direction, notice how the control points seem to point in the negative direction.

So if I want a slice of a circle with a radius R and I have already computed the anchor points, how can I calculate the position of control point 1 and control point 2???

回答1:

Feng Yuan proposed simple method in his book Windows Graphics Programming: build an arc with radius 1, centered at OX axis, calculate Bezier approximation for it, and scale, translate and rotate control points for needed arc parameters. Here is my implementation of this method (in Delphi), modified for large arcs. C++ sources can be found somewhere in the Internet, but I hope that the logic is clear.

  GenerateBezierArc(200, 200, 150, Pi / 4, 3 * Pi / 2, Pts);
  Canvas.PolyBezier(Pts);

result:

type
  TPointArray = array of TPoint;

//calculates array of Bezier control points
//for circle arc with center CX, CY and radius R
procedure GenerateBezierArc(CX, CY, R: Integer;
                            StartAngle, SweepAngle: Double;
                            var Pts: TPointArray);
// C-Pascal translation from Feng Yuan book, with correction of source errors
var
  iCurve, NCurves: Integer;
  i: Integer;
  x0, y0, tx, ty, sn, cs, ASweep, AStart: Double;
  Px, Py: array [0 .. 3] of Double;
begin
  if SweepAngle = 0 then
    Exit;
  // if SweepAngle is too large, divide arc to smaller ones
  NCurves := Ceil(Abs(SweepAngle) / (Pi/2));
  SetLength(Pts, 3 * NCurves + 1);
  ASweep := SweepAngle / NCurves;

  // calculates control points for Bezier approx. of arc with radius=1,
  // circle center at (0,0), middle of arc at (1,0)
  y0 := Sin(ASweep / 2);
  x0 := Cos(ASweep / 2);
  tx := (1 - x0) * 4 / 3;
  ty := y0 - tx * x0 / (y0 + 0.0001);
  Px[0] := x0;
  Py[0] := -y0;
  Px[1] := x0 + tx;
  Py[1] := -ty;
  Px[2] := x0 + tx;
  Py[2] := ty;
  Px[3] := x0;
  Py[3] := y0;

  // rotation and translation of control points
  sn := Sin(StartAngle + ASweep / 2);
  cs := Cos(StartAngle + ASweep / 2);
  Pts[0].X := CX + Round(R * (Px[0] * cs - Py[0] * sn));
  Pts[0].Y := CY + Round(R * (Px[0] * sn + Py[0] * cs));

  for iCurve := 0 to NCurves - 1 do begin
    AStart := StartAngle + ASweep * iCurve;
    sn := Sin(AStart + ASweep / 2);
    cs := Cos(AStart + ASweep / 2);
    for i := 1 to 3 do begin
      Pts[i + iCurve * 3].X := CX + Round(R * (Px[i] * cs - Py[i] * sn));
      Pts[i + iCurve * 3].Y := CY + Round(R * (Px[i] * sn + Py[i] * cs));
    end;
  end;
end;


回答2:

The article referenced by Duncan's post is actually the result for 90 degree circular arc from a journal paper authored by Tor Dokken (the main author) and published in Computer Aided Geometric Design Vol 7 in 1990. It cited two approaches for approximating a 90 degree arc: a standard approach and a better approach. I will list the general formula for the "standard approach" below and leave out the general formula for the "better approach" as it requires a lot of typing:

For a circular arc with angular span A and unit radius, described as C(t) = (cos(t), sin(t)), where t=[0, A], a good cubic Bezier curve approximation can be obtained with the following control points:

P(0) = (1, 0),
P(1) = (1, 0) + L(0,1),
P(2) = (cosA, sinA) - L (-sinA, cosA),
P(3) = (cosA, sinA)

where L is a scalar constant depending on A as

L = (4/3)*tan(A/4)

Please note that the cubic Bezier curve approximation obtained this way always interpolates the two end points and the mid-point of the circular arc and the approximation error is always positive, which means the cubic Bezier curve is always "outside" the circular arc.

The maximum radial error (x(t)^2 + y(t)^2 - 1) from this simple formula is

Error_max = (4/27) * ( power(sin(A/4),6)/power(cos(A/4),2) )

When you want to approximate a general circular arc (any angle span and any radius radius) within a certain tolerance, you can use this formula to compute how many segments you need to break the circular arc into and approximate each arc segment with a cubic Bezier curve. Since this cubic Bezier curve will honor the end points and end slopes, all cubic Bezier curves obtained will join smoothly together.



回答3:

This article gives a set of 4 bezier curves that generates a very close approximation of a circle. It divides the circle into 4 quarters and each curve generates 1/4 of the circle.

I don't know how you'd come up with the control points for an arbitrary arc along a circle. You'd use trig to find the start and end points, but the middle points would be harder.

The conclusion of the article:

The maximum radial drift is 0.019608% with this approximation. This is 28% better than the standard approximation. Here is the final result:

Figure 4. The Bézier approximation is almost indistinguishable from a circle. Figure 4 was created using the Bézier curves: P_0 = (0,1), P_1 = (c,1), P_2 = (1,c), P_3 = (1,0) P_0 = (1,0), P_1 = (1,-c), P_2 = (c,-1), P_3 = (0,-1) P_0 = (0,-1), P_1 = (-c,-1), P_3 = (-1,-c), P_4 = (-1,0) P_0 = (-1,0), P_1 = (-1,c), P_2 = (-c,1), P_3 = (0,1) with c = 0.551915024494.

That's for a unit circle (a circle on the origin with a radius of 1) You'd need to scale it for other radius values.

EDIT:

If you assume that your arc will always be 1/4 of a circle or less, then you could use the Bezier curve for a 1/4 circle, and draw a portion of that arc by varying the range of the t parameter to a range less than t=0 -> t=1. You'd need to apply a rotation transform to your points to move them around the circle.