Wednesday, October 19, 2011

More About Approximating Circular Arcs With a Cubic Bezier Path


Back in April of this year I wrote an article called Approximating a Circular Arc With a Cubic Bezier Path which demonstrated how one could do the job with the latest version of Adobe Flex and Actionscript.  The article was based on a paper written by Aleksas Riskus called Approximation of a Cubic Bezier Curve by Circular Arcs and Vice Versa which explains how a single cubic Bezier curve can be used to approximate an arc of 90 degrees or less.  The paper actually presents two formulas for doing this, in sections (7) and (9), and I'd started with the latter because it seemed like the ActionScript version would be easier to write.  The fact that the formulas didn't work as written was probably for the best, because it forced me to read the paper carefully enough to roughly understand the derivations, at least for section (7).   Which is what I ended up implementing.  I also sent a note to Aleksas Riškus, who is a Mathematics professor in Lithuania, and not long after I'd published my article, he replied with corrections for the equations in section (9):

x2 = xc + ax – k2*ay,
y2 = yc + ay + k2*ax,
x3 = xc + bx + k2*by,                                  (9)
y3 = yc + by – k2*bx


Back then I was grateful for the reply and planned to write a follow-up blog that included a revised version of the ActionScript code.

Then I forgot about it.

Just recently Martin Fox sent me an email about the blog entry.  He pointed out that the circular arcs my demo application rendered were a bit flat when the swept angle was small, and that this was likely due to the constant tangent magnitude ( k*R in section (7) of the Riskus paper).  Simply recoding the method that computed the arcs in terms of the equations in section (9) corrected the problem.   And having reacquainted myself with this material I decided to factor the code for computing arcs into a utility class.   The utility class. ArcPathUtils.as, includes methods for creating spark.primitives.Path objects that render some useful variations on the basic arc:

createArcPathData(
    xc:Number, yc:Number, r:Number, a1:Number, a2:Number):String
createTriangularWedgePathData(
    xc:Number, yc:Number, r:Number, a1:Number, a2:Number):String

createRectangularSegmentPathData(
    xc:Number, yc:Number, r1:Number, r2:Number, a1:Number, a2:Number,
    caps:String="||"):String
createRectangularSegmentPathData(
    xc:Number, yc:Number, r1:Number, r2:Number, a1:Number, a2:Number,
    caps:String="()"):String

createRectangularSegmentPathData(
    xc:Number, yc:Number, r1:Number, r2:Number, a1:Number, a2:Number,
    caps:String="))"):String

createRectangularSegmentPathData(
    xc:Number, yc:Number, r1:Number, r2:Number, a1:Number, a2:Number,
    caps:String=")("):String



This set of static PathArcUtils methods compute the String value for a s:Path's data property.  They all create an arc centered at xc,yc that sweeps from angle a1, to a2.  The center of the arc is specified in pixel coordinates, and the sweep angles in degrees.  The r parameter for createArcPathData() and createTriangularWedgePathData() is the arc's radius, also in pixels.   The createRectangularSegmentPathData() method creates an rectangular arc "segment" whose ends are specified by the final caps parameter.    The arc segment has an inner and outer radius specified by the r1 and r2 parameters respectively.

The PathArcUtils class also includes a createArc() method, similar to createArcPathData(), which just returns the coordinates for the bezier curves as objects.

The application below demonstrates the s:Path arc variations.  The source code for the application can be found here:


You can try the different arc types, and "rectangular arc segment" end cap types, by clicking the buttons in the top row.   There are some limits to the relative values for the start and end angle, and for the inner and outer radius, to prevent the Path from self-intersecting enough to distort the expected shape (see the source code for the details).

11 comments:

  1. I was asked to formally grant permission to use the example code. I've added a Creative Commons license to the source code:


    This work is licensed under the Creative Commons Attribution 3.0
    Unported License. To view a copy of this license, visit
    http://creativecommons.org/licenses/by/3.0/ or send a letter to
    Creative Commons, 444 Castro Street, Suite 900, Mountain View,
    California, 94041, USA.

    ReplyDelete
  2. Very useful, thanks! URL of the second file is:

    https://sites.google.com/site/hansmuller/flex-blog/ArcButton.mxml

    @Hans
    maybe you can fix the URL

    ReplyDelete
    Replies
    1. Thanks for the feedback, it's (finally) fixed now.

      Delete
  3. You sir, made my day. I've been having some issues with SVG scaling using Inkscape to create pie charts, and was looking for a way to generate bezier curves with code.

    *tips hat*

    ReplyDelete
  4. [quote]The derivation of the control points for an arc of less than 90 degrees is a little more complicated. If the arc is centered around the X axis, then the length of the tangent line is r * tan(a/2), instead of just r. The magnitude of the vector from each arc endpoint to its control point is k * r * tan(a/2).[/quote]

    It`s wrong as if a = 90 degrees formula is not reduced to well known case. Actually magnitude is equal to k1 * r, where k1 = 4/3 * (1 - cos(a/2)) / sin(a/2), for a = 90 it reduced to 4/3 * (sqrt(2) - 1).

    ReplyDelete
  5. Thanks for posting this Hans.

    As a note for those following along, after converting PathArcUtils.createSmallArc() to 3D Studio Max "MaxScript", I had to add "* 4/3" to the end of the k2 assignment to generate correct control points (they were 33% too short before the change). Don't know if it's a bug in the posted code or some difference between 3DSM and Actionscript but if anyone else is porting the code and experiencing the same issue, there's the fix (as far as I can tell) ;)

    ReplyDelete
  6. Hans, thank you for this useful blog entry.

    I believe you can simplify these lines:
    const q1:Number = x1*x1 + y1*y1;
    const q2:Number = q1 + x1*x4 + y1*y4;
    const k2:Number = 4/3 * (Math.sqrt(2 * q1 * q2) - q2) / (x1 * y4 - y1 * x4);

    to just the line
    // Caution: I backported the line below from C, and have not tested or even compiled it:
    const k2:Number = 4/3 * (1 - Math.cos(a)) / (Math.sin(a));

    Incidentally, the only difficult I had in moving to C was that I was careless and left the “4/3” without decimal points, and C performed the integer division and dropped the fraction =:-|

    ReplyDelete
  7. Hello,
    Thank you for this post, it really saved my day (a few, even).
    However I still have a problem (which can't be reproduced using the embedeed *.as).
    However, when I'm trying to create a bezier circular segment approximation of PI/2 angle, the code (translated to c#) produces 3 segments, each rotated by 90% to previous one.
    Those 3 segments cover all quarters, except desired one.

    I did some thinking on the subject and haven't came to satisfying conclusion. Could you help with defining conditions for exactly 90* arcs?

    ReplyDelete
  8. this is super late and you might not ever see it but thank you SO MUCH for those corrections. I was tearing my hair out trying trying to implement (9) from Riškus's paper. this worked immediately!

    ReplyDelete
  9. Years after the fact, but for those porting this to another language and not getting the expected results: If you're using Java or similar, you need to change `3 / 4` to `3.0 / 4.0`, otherwise you are doing integer math and the result is always zero.

    ReplyDelete