Draw an improved heartagram in C#

[heartagram]

My previous post Draw a heartagram in C# uses a smooth curve to draw the curved part of a heartagram. The result is pretty good, but if you look closely you’ll see that its edges curve in to the shape’s points. This version of the program uses the techniques described int he post Connect two line segments with a circular arc in C# to replace the smooth curves with circular arcs.

The program follows the same basic approach as the previous version. It uses a method to create a GraphicsPath object that defines the heartagram and the draws that path. The following sections describe three key pieces of the program: the MakeHeartagram method that defines the GraphicsPath, and two helper methods GetControlPoints and PointAtFraction.

MakeHeartagram2

The following MakeHeartagram2 method returns a GraphicsPath that defines the heartagram.

// Return a GraphicsPath representing a heartagram.
private GraphicsPath MakeHeartagram2(PointF center,
    float radius, float pen_width)
{
    // Get the control points.
    PointF[] control_points =
        GetControlPoints(center, radius, pen_width);

    // Build the GraphicsPath.
    GraphicsPath path = new GraphicsPath();
    path.AddLine(control_points[0], control_points[4]);

    // Find the first arc.
    RectangleF arc1_rect;
    float arc1_start_angle, arc1_sweep_angle;
    PointF s1_far, s1_close, s2_far, s2_close;
    FindArcFromSegments(
        control_points[0], control_points[4],
        control_points[2], control_points[3],
        out arc1_rect, out arc1_start_angle, out arc1_sweep_angle,
        out s1_far, out s1_close, out s2_far, out s2_close);
    path.AddArc(arc1_rect, arc1_start_angle, arc1_sweep_angle);

    path.AddLine(control_points[2], control_points[3]);
    path.AddLine(control_points[3], control_points[1]);
    path.AddLine(control_points[1], control_points[2]);

    // Find the second arc.
    RectangleF arc2_rect;
    float arc2_start_angle, arc2_sweep_angle;
    FindArcFromSegments(
        control_points[1], control_points[2],
        control_points[5], control_points[0],
        out arc2_rect, out arc2_start_angle, out arc2_sweep_angle,
        out s1_far, out s1_close, out s2_far, out s2_close);
    path.AddArc(arc2_rect, arc2_start_angle, arc2_sweep_angle);

    path.AddLine(control_points[5], control_points[0]);

    // Close the figure so the last corner is mitered.
    path.CloseFigure();

    return path;
}

[heartagram]

This code first calls the GetControlPoints method (described shortly) to define some control points. The picture on the right shows the control points.

Next, the code builds a GraphicsPath object and adds a line segment from control point 0 to control point 4. It then uses the FindArcFromSegments described in the earlier post to create a circular arc connecting segment 0-4 to segment 2-3.

The method then adds straight segments 2-3, 3-1, and 1-2. It then adds another arc, this time connecting segment 1-2 with segment 5-0.

Finally, the method adds the straight segment 5-0, closes the figure, and returns the GraphicsPath.

GetControlPoints

The following GetControlPoints method returns the heartagram’s control points.

// Return a list of the heartagram's control points.
private PointF[] GetControlPoints(PointF center,
    float radius, float pen_width)
{
    // Define the control points.
    double pi_over_2 = Math.PI / 2;
    double dtheta = 2 * Math.PI / 5;
    double[] control_angles =
    {
        pi_over_2,
        pi_over_2 + dtheta,
        pi_over_2 + 2.5 * dtheta,
        pi_over_2 + 4 * dtheta,
    };
    double[] control_radii =
    {
        0.90 * radius - pen_width / 2f,
        0.90 * radius - pen_width / 2f,
        0.75 * radius - pen_width / 2f,
        0.90 * radius - pen_width / 2f,
    };
    PointF[] control_points =
        new PointF[control_angles.Length + 2];
    for (int i = 0; i < control_angles.Length; i++)
    {
        double x = center.X + control_radii[i] * Math.Cos(control_angles[i]);
        double y = center.Y + control_radii[i] * Math.Sin(control_angles[i]);
        control_points[i] = new PointF((float)x, (float)y);
    }

    // Find the end points of the arcs.
    control_points[4] = PointAtFraction(
        control_points[1], control_points[2], 0.4f);
    control_points[5] = PointAtFraction(
        control_points[3], control_points[2], 0.4f);

    return control_points;
}

[heartagram]

(I’ve repeated the earlier picture on the right so you can see where the control points lie.) This method first defines some angles and radii that it can use to generate control points 0 through 3. (I picked the angles and radii to produce a nice result. You can experiment with them if you like.) The code loops through those values and uses sines and cosines to define those control points. Notice that the code allocates two extra spaces for control points 4 and 5 in the control_points array.

After it has defines control points 0 through 3, the method calls the PointAtFraction method to find control point 4, which sits 40% (0.4 as a fraction) of the way from control point 1 to control point 2. It uses the same method to find control point 5 sitting 40% of the way from control point 3 to control point 2.

Note that I picked the 40% value because I thought it produced a nice result. You could tweak it if you like.

The method finishes by returning the control points.

PointAtFraction

The following PointAtFraction method finds a point a given fraction of the way from one point to another.

// Return a point that is the given fraction of
// the distance from start_point to to_point.
public static PointF PointAtFraction(PointF start_point,
    PointF to_point, float fraction)
{
    float dx = (to_point.X - start_point.X);
    float dy = (to_point.Y - start_point.Y);
    return new PointF(
        start_point.X + dx * fraction,
        start_point.Y + dy * fraction);
}

This method first sets dx and dy equal to the X and Y coordinate differences between the start and end points. In other words:

start_point +  = to_point

The code then multiples dx and dy by the desired fraction, adds the results to the start point’s coordinates, and converts that into a new PointF.

Summary

Those are the program’s main new pieces. See the previous post and download the example program to see additional details such as how the program actually draws the curve and how you can modify the program to label the control points. (Hint: search for the word “optionally.”)

You may never need to draw a heartagram, but you may find the techniques demonstrated by this example and the previous one useful some day for drawing line segments joined by smooth curves or circular arcs.


Download Example   Follow me on Twitter   RSS feed   Donate




About RodStephens

Rod Stephens is a software consultant and author who has written more than 30 books and 250 magazine articles covering C#, Visual Basic, Visual Basic for Applications, Delphi, and Java.
This entry was posted in drawing, graphics and tagged , , , , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.