Draw arc wedges in C#

[arc wedges]

This example defines several extension methods that draw various interesting items including the one that I originally set out to draw: arc wedges. The extension methods let you easily draw arc wedges, arcs with tic marks, line segments with tic marks and arrowheads, and arrowheads without line segments. Note that the wedges are isosceles trapezoids; their inner and outer edges are straight line segments and do not follow arcs of a circle.

I was inspired to draw arc wedges by a picture in an article that was trying to look like some sort of high-tech biometric identification system. Once I started on the final drawing, I also added arrowheads, arrows, and lines with tic marks.

When the program starts, it displays two forms. The picture here shows the main form. The second form shows samples of line segments with various arrowheads.

Before I show you the code to draw arc wedges, arrowheads, and lines with tic marks, let’s take a look at the main form’s code that uses those tools. When the form needs to draw, the following Paint event handler executes. The most interesting drawing code is highlighted in blue. I’ll describe those methods in later sections.

private void picCanvas_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

    // Draw inner arc tics.
    Point center = new Point(173, 89);
    float radius = 40;
    float start_angle = 40;
    float sweep_angle = 360 - 2 * start_angle;
    int num_tics = 24;
    float skip_angle = sweep_angle / num_tics;
    e.Graphics.DrawArcTics(
        Pens.Yellow, Pens.Yellow,
        center, radius, 10,
        start_angle, sweep_angle,
        skip_angle, 1, 1);

    // Draw outer wedges.
    float inner_radius2 = 60;
    float outer_radius2 = inner_radius2 + 40;
    int num_wedges = 12;
    float skip_degrees = sweep_angle /
        (2 * num_wedges + (num_wedges - 1));
    float draw_degrees = skip_degrees * 2;
    e.Graphics.DrawArcWedges(
        null, Pens.Black,
        center, inner_radius2, outer_radius2,
        start_angle, num_wedges,
        draw_degrees, skip_degrees);

    // Highlight one arc wedge.
    using (Pen pen = new Pen(Color.Purple, 2))
    {
        using (Brush brush = new SolidBrush(
            Color.FromArgb(80, pen.Color)))
        {
            start_angle += 3 * (draw_degrees + skip_degrees);
            e.Graphics.DrawArcWedges(
                brush, pen,
                center, inner_radius2, outer_radius2,
                start_angle, 1,
                draw_degrees, skip_degrees);
        }
    }

    // Draw the arrow pointing to the center.
    Brush arrow_brush = Brushes.Orange;
    PointF start_point = new PointF(center.X + 80, center.Y);
    e.Graphics.DrawSegment(
        start_point, center, Pens.Orange,
        Pens.Orange, 10, 10, 1, 2,
        Extensions.ArrowheadTypes.None,
        null, 0,
        Extensions.ArrowheadTypes.TriangleHead,
        Brushes.Orange, 8);

    // Draw the ticks above the pupil.
    PointF p1 = new PointF(center.X - 75, center.Y - 50);
    PointF p2 = new PointF(center.X + 75, center.Y - 50);
    e.Graphics.DrawSegment(
        p1, p2, Pens.White,
        Pens.White, 10, 15, 1, 1,
        Extensions.ArrowheadTypes.None, null, 0,
        Extensions.ArrowheadTypes.None, null, 0);
    PointF p3 = new PointF(center.X - 25, p1.Y - 20);
    PointF p4 = new PointF(center.X - 25, p1.Y - 10);
    e.Graphics.DrawArrowhead(Brushes.White, p3, p4, 10,
        Extensions.ArrowheadTypes.VHead);

    // Draw tic marks to the left.
    PointF p5 = new PointF(center.X - 75, center.Y - 50);
    PointF p6 = new PointF(center.X - 75, p5.Y + 15 * 7);
    e.Graphics.DrawSegment(
        p5, p6, Pens.White,
        Pens.White, 10, 15, 1, 1,
        Extensions.ArrowheadTypes.None, null, 0,
        Extensions.ArrowheadTypes.None, null, 0);
    PointF p7 = new PointF(p5.X - 20, center.Y + 20);
    PointF p8 = new PointF(p5.X - 10, center.Y + 20);
    e.Graphics.DrawArrowhead(Brushes.White, p7, p8, 10,
        Extensions.ArrowheadTypes.VHead);
}

The event handler sets the Graphics object’s SmoothingMode property, sets a few parameters, and then calls the DrawArcTics extension method. That method draws an arc with tic marks on it. In the picture at the top of the post, it draws the yellow arc surrounding the eye’s pupil.

[arc wedges]

Next, the code sets some more parameters and uses the DrawArcWedges extension method to draw the black arc wedges that list outside of the yellow arc. It then highlights one wedge in translucent purple. To place the wedge correctly, the code begins at the start angle used to draw all of the arc wedges and adds three times the number of degrees that the DrawArcWedges method skipped between wedges. The red arcs on the picture to the right show how updating start_angle moves the start angle to the beginning of the highlighted wedge. Most of the other parameters passed into the DrawArcWedges method are the same as those used before so the highlighted wedge falls on top of a previously drawn wedge. The only change is that the new call uses a purple pen and a translucent purple brush.

Now the program draws some arrows and arrowheads. It calls the DrawSegment extension method to draw the orange arrow that ends at the eye’s pupil. As you can see, the method draws the line segment, tic marks, and a triangular arrowhead.

The code then uses the same method to draw a white line segment with tic marks but no arrowheads above the pupil. It uses the DrawArrowhead extension method, which is also used by the DrawSegment method, to draw an arrowhead above the segment.

The event handler finishes repeating those steps to draw one more white line segment with tic marks and an arrowhead beside it to the left of the pupil.

That’s all there is to the Paint event handler. It’s relatively simple, although it contains a lot of statements that define parameters for the extension methods that it calls. The following sections describe the extension methods.

DrawArcTics

The following code shows the start of the DrawaArcTics extension method.

public static void DrawArcTics(this Graphics gr,
    Pen arc_pen, Pen tic_pen,
    Point center, float radius, float tic_length,
    float start_angle, float sweep_angle, float skip_degrees,
    int num_skip_start_tics, int num_skip_end_tics)

The following list describes the method’s parameters.

  • gr – This is the Graphics object for which the extension method is executing.
  • arc_pen – The method uses this pen to draw the arc. Set this to null not draw the arc.
  • tic_pen – The method uses this pen to draw the tic marks. Set this to null to not draw the tic marks.
  • center – This is the center of the arc’s circle.
  • radius – This is the radius of the arc’s circle.
  • tic_length – This is the length of the tic marks. The method draws them centered on the arc.
  • start_angle – This is the angle where the arc should start.
  • sweep_angle – This is the amount that the arc should sweep.
  • skip_degrees – This is the number of degrees that the method should skip between tic marks.
  • num_skip_start_tics – Set this to the number of tic marks that the method should not draw at the beginning of the arc. Use this to make the arc begin with a stretch that does not include tic marks.
  • num_skip_end_tics – Set this to the number of tic marks that the method should not draw at the end of the arc.

The following code shows the method.

// Draw tic marks around an arc.
public static void DrawArcTics(this Graphics gr,
    Pen arc_pen, Pen tic_pen,
    Point center, float radius, float tic_length,
    float start_angle, float sweep_angle, float skip_degrees,
    int num_skip_start_tics, int num_skip_end_tics)
{
    if (arc_pen != null)
    {
        RectangleF rect =new RectangleF(
            center.X - radius,
            center.Y - radius,
            2 * radius, 2 * radius);
        gr.DrawArc(arc_pen, rect, start_angle, sweep_angle);
    }
    if (tic_pen == null) return;

    int num_tics = (int)(sweep_angle / skip_degrees) + 1;
    double theta = DegToRad(start_angle);
    double skip_rad = DegToRad(skip_degrees);

    theta += skip_rad * num_skip_start_tics;
    num_tics -= num_skip_start_tics;

    num_tics -= num_skip_end_tics;

    float inner_radius = radius - tic_length / 2f;
    float outer_radius = radius + tic_length / 2f;

    for (int i = 0; i < num_tics; i++)
    {
        PointF p1 =
            new PointF(
                (float)(center.X + inner_radius * Math.Cos(theta)),
                (float)(center.Y + inner_radius * Math.Sin(theta)));
        PointF p2 =
            new PointF(
                (float)(center.X + outer_radius * Math.Cos(theta)),
                (float)(center.Y + outer_radius * Math.Sin(theta)));
        gr.DrawLine(tic_pen, p1, p2);

        theta += skip_rad;
    }
}

If the arc_pen parameter is not null, then the code uses the Graphics object’s DrawArc method to draw the arc with that pen.

If tic_pen is null, the method is done so it returns.

Otherwise if tic_pen is not null, the method calculates the number of tic marks that it must draw. It then uses the DegToRad helper method to convert the angle parameters from degrees to radians. That method is straightforward so I won’t show it here. It simply multiples the angle in degrees by π and divides the result by 180.

Next the code adds to the angle theta to skip the desired number of tic marks at the arc’s start. It also subtracts from num_tics the number of tic marks that it should skip at the arc’s beginning and end.

The code then calculates the inner and outer radii where the tic marks should begin and end. It then enters a loop to draw the tic marks.

Inside the loop, the code uses cosines and sines to calculate the X and Y coordinates of the tic marks’ end points and draws them. After each tic mark, it increases the angle theta by the skip angle. These calculations are done in radians because that’s what Math.Sin and Math.Cos use.

DrawArcWedges

The following code shows the start of the DrawArcWedges extension method.

public static void DrawArcWedges(this Graphics gr,
    Brush brush, Pen pen,
    Point center, float inner_radius, float outer_radius,
    float start_angle, int num_wedges,
    float draw_degrees, float skip_degrees)

The following list describes the method’s parameters.

  • gr – This is the Graphics object for which the extension method is executing.
  • brush – The method fills the arc wedges with this brush. Set this to null to make the method not fill the wedges.
  • pen – The method outlines the arc wedges with this pen. Set this to null to make the method not outline the wedges.
  • center – This is the center of the circle that defines the arc wedges.
  • inner_radius – This is the radius of the wedges’ inner corners.
  • outer_radius – This is the radius of the wedges’ outer corners.
  • start_angle – The method starts drawing at this angle.
  • num_wedges – This determines the number of arc wedges that the method draws.
  • draw_degrees – This gives the number of degrees that a wedge should span.
  • skip_degrees – This gives the number of degrees between wedges.

The following code shows the DrawArcWedges method.

// Draw arc wedges.
public static void DrawArcWedges(this Graphics gr,
    Brush brush, Pen pen,
    Point center, float inner_radius, float outer_radius,
    float start_angle, int num_wedges,
    float draw_degrees, float skip_degrees)
{
    double theta = DegToRad(start_angle);
    double draw_rad = DegToRad(draw_degrees);
    double skip_rad = DegToRad(skip_degrees);
    for (int i = 0; i < num_wedges; i++)
    {
        PointF[] points =
        {
            new PointF(
                (float)(center.X + inner_radius * Math.Cos(theta)),
                (float)(center.Y + inner_radius * Math.Sin(theta))),
            new PointF(
                (float)(center.X + outer_radius * Math.Cos(theta)),
                (float)(center.Y + outer_radius * Math.Sin(theta))),
            new PointF(
                (float)(center.X + outer_radius * Math.Cos(theta + draw_rad)),
                (float)(center.Y + outer_radius * Math.Sin(theta + draw_rad))),
            new PointF(
                (float)(center.X + inner_radius * Math.Cos(theta + draw_rad)),
                (float)(center.Y + inner_radius * Math.Sin(theta + draw_rad))),
        };
        if (brush != null) gr.FillPolygon(brush, points);
        if (pen != null) gr.DrawPolygon(pen, points);

        theta += draw_rad + skip_rad;
    }
}

The method first converts the angle parameters from degrees to radians and then loops through the desired number of arc wedges. For each wedge, the code uses Math.Cos and Math.Cos to calculate the positions of the wedge’s corners. It fills and outlines the wedge appropriately and then increases the angle theta by the angular size of the wedge and the gap between wedges.

DrawSegment

The DrawSegment draws a line segment with optional tic marks and arrowheads at both ends. The following list describes the method’s parameters.

  • gr – This is the Graphics object for which the extension method is executing.
  • start_point – This is the point where the segment starts.
  • end_point – This is the point where the segment ends.
  • line_pen – The method draws the segment with this pen. Set this to null to not draw the segment.
  • tic_pen – The method draws the tic marks with this pen. Set this to null to not draw tic marks.
  • tic_length – This is the length of the tic marks. The method centers the tic marks across the segment and makes them perpendicular to it.
  • tic_spacing – This is the distance along the segment between tic marks.
  • num_skip_start_tics – This indicates the number of tic marks that the method should skip at the beginning of the segment.
  • num_skip_end_tics – This indicates the number of tic marks that the method should skip at the end of the segment.
  • start_arrowhead_type – This determines the type of arrowhead that should be drawn at the beginning of the segment. Set this to Extensions.ArrowheadTypes.None to not draw a starting arrowhead.
  • start_arrowhead_brush – This is the brush used to draw the starting arrowhead.
  • start_arrowhead_radius – This determines the size of the starting arrowhead. The exact effect depends on the arrowhead type, but this is basically half of the arrowhead’s width. Note that this is not scaled for different segment thicknesses. (The Graphics class’s DrawLine method scales end caps for segments of different thicknesses.)
  • end_arrowhead_type – This determines the type of arrowhead that should be drawn at the end of the segment. Set this to Extensions.ArrowheadTypes.None to not draw an ending arrowhead.
  • end_arrowhead_brush – This is the brush used to draw the ending arrowhead.
  • end_arrowhead_radius – This determines the size of the ending arrowhead.

The following code shows the DrawSegment method.

// Draw a lines segment with optional tic marks and arrowheads.
public static void DrawSegment(this Graphics gr,
    PointF start_point, PointF end_point,
    Pen line_pen,
    Pen tic_pen, float tic_length, float tic_spacing,
    int num_skip_start_tics, int num_skip_end_tics,
    ArrowheadTypes start_arrowhead_type,
    Brush start_arrowhead_brush, float start_arrowhead_radius,
    ArrowheadTypes end_arrowhead_type,
    Brush end_arrowhead_brush, float end_arrowhead_radius)
{
    // Draw the line.
    if (line_pen != null)
        gr.DrawLine(line_pen, start_point, end_point);

    // Draw the tic marks.
    if (tic_pen != null)
    {
        // Get unit vectors in the directions
        // of the segment and perpendicular.
        float dx = end_point.X - start_point.X;
        float dy = end_point.Y - start_point.Y;
        float length = (float)Math.Sqrt(dx * dx + dy * dy);
        dx /= length;
        dy /= length;
        float nx = dy;
        float ny = -dx;

        // Convert to the desired lengths.
        dx *= tic_spacing;
        dy *= tic_spacing;
        nx *= tic_length / 2f;
        ny *= tic_length / 2f;

        // Prepare if we should skip the first tic mark.
        PointF point = start_point;
        int num_tics = (int)(length / tic_spacing) + 1;

        // Skip starting tics if desired.
        num_tics -= num_skip_start_tics;
        point.X += num_skip_start_tics * dx;
        point.Y += num_skip_start_tics * dy;

        // Skip ending tics if desired.
        num_tics -= num_skip_end_tics;

        // Draw the tic marks.
        for (int i = 0; i < num_tics; i++)
        {
            PointF p1 = new PointF(
                point.X + nx,
                point.Y + ny);
            PointF p2 = new PointF(
                point.X - nx,
                point.Y - ny);
            gr.DrawLine(tic_pen, p1, p2);

            point.X += dx;
            point.Y += dy;
        }
    }

    // Draw the arowheads.
    DrawArrowhead(gr, start_arrowhead_brush,
        end_point, start_point, start_arrowhead_radius,
        start_arrowhead_type);
    DrawArrowhead(gr, end_arrowhead_brush,
        start_point, end_point, end_arrowhead_radius,
        end_arrowhead_type);
}

The method first checks the line_pen parameter and draws the line segment if that pen is not null.

Next, if tic_pen is not null, the method draws the tic marks. To do that it gets the difference in X and Y coordinates between the segment’s end and start points. It divides those differences by the segment’s length so the vector <dx, dy> has length one.

The code then switches those values and negates one to get a vector <nx, ny> = <dy, -dx> that has length one and that is perpendicular to the line segment.

The code will use the vector <dx, dy> to move along the line segment, so it multiplies dx and dy by tic_spacing so it will move the correct amount between tic marks.

Similarly the method will use the vector <nx, ny> to move from the segment to the tic marks’ end points. It will move half of the tic mark length to each side of the segment, so it multiplies nx and ny by half of tic_length.

The code then calculates the number of tic marks it would need to fill the entire segment. The code then subtracts the number of starting tic marks that it should skip and updates the start point accordingly. It also subtracts the number of ending tic marks that it should skip.

Finally, the code enters a loop to draw the tic marks. For each tic mark, it adds vector <nx, ny> to the current position on the segment to find the tic mark’s end points. It connects those points and then uses dx and dy to update the current position on the segment so it is ready to draw the next tic mark.

The method finishes by calling the DrawArrowhead method twice to draw the segment’s starting and ending arrowheads.

DrawArrowhead

The DrawArrowhead method draws an arrowhead pointing in a certain direction. The following enumeration defines the types of arrowheads that the method can draw.

// Arrowhead types.
public enum ArrowheadTypes
{
    None,
    TriangleHead,
    TriangleTail,
    VHead,
    VTail,
    Broadhead,
    Broadtail,
};

[arc wedges]

The picture on the right shows samples of each of the arrowhead and tail types. From top-to-bottom they demonstrate the None, Triangle, V, and Broad styles.

The following list describes the DrawArrowhead method’s parameters.

  • gr – This is the Graphics object for which the extension method is executing.
  • brush – The method uses this brush to fill the arrowhead.
  • from_point – The arrowhead is drawn at the end of a line segment that starts at this point.
  • to_point – The arrowhead is drawn at the end of a line segment that ends at this point.
  • radius – This defines the size of the arrowhead. The exact effect depends on the arrowhead type, but this is basically half of the arrowhead’s width.
  • arrowhead_type – This specifies the type of arrowhead or tail to draw. Set this to ArrowheadTypes.None to draw nothing.

Note that this method only uses the segment defined by start_point and end_point to position and orient the arrowhead or tail. It does not actually draw the segment.

The following code shows the shell of the DrawArrowhead method.

// Draw an arrowhead at the indicated point.
public static void DrawArrowhead(this Graphics gr, Brush brush,
    PointF from_point, PointF to_point, float radius,
    ArrowheadTypes arrowhead_type)
{
    if (arrowhead_type == ArrowheadTypes.None) return;

    // Get the vectors that we need.
    float dx = to_point.X - from_point.X;
    float dy = to_point.Y - from_point.Y;
    float length = (float)Math.Sqrt(dx * dx + dy * dy);
    float ux = dx / length; // Unit distance along the arrow.
    float uy = dy / length;
    float rx = uy;    // Unit distance perpendicular to the arrow.
    float ry = -ux;

    // Generate the arrowhead's points.
    PointF[] points = null;
    switch (arrowhead_type)
    {
        case ArrowheadTypes.Broadhead:
            ...
            break;
        case ArrowheadTypes.Broadtail:
            ...
            break;
        case ArrowheadTypes.TriangleHead:
            ...
            break;
        case ArrowheadTypes.TriangleTail:
            ...
            break;
        case ArrowheadTypes.VHead:
            ...
            break;

        case ArrowheadTypes.VTail:
            ...
            break;
    }

    gr.FillPolygon(brush, points);
}

The method first checks arrowhead_type and returns if it is None.

The method then gets two the unit (length one) vectors much as the DrawSegment method did. The vector <ux, uy> points in the same direction as the line segment. The vector <rx, ry> points perpendicularly to the right of the segment as you look along the segment.

The code then declares an array of PointF objects and enters the switch statement that makes up the bulk of the method. The different switch cases use the <dx, dy> and <nx, ny> vectors to fill in the points array to define the different kinds of arrowheads and tails.

After the switch blocks end, the program simply uses the points in the points array to fill the polygon that defines the arrowhead shape.

The following code shows how the method defines the TriangleHead shape.

case ArrowheadTypes.TriangleHead:
    points = new PointF[]
    {
        new PointF(to_point.X, to_point.Y),
        new PointF(
            to_point.X - radius * ux + radius * rx,
            to_point.Y - radius * uy + radius * ry),
        new PointF(
            to_point.X - radius * ux - radius * rx,
            to_point.Y - radius * uy - radius * ry),
    };
    break;

[arc wedges]

The picture on the right shows how the code generates the points that it saves in the points array. Each of the vectors is multiplied by the radius, although I have omitted that from the picture to save space.

The code saves the line segment’s end point in the first array entry.

For the second point, it starts at the segment’s end point, subtracts the vector <ux, uy> (times the radius), and then adds the vector <rx, ry> (again, times the radius).

For the third point, the code starts at the segment’s end point, subtracts the vector <ux, uy> (times the radius), and then subtracts the vector <rx, ry> (all times the radius).

Together the three points define the arrowhead’s triangle.

The other switch cases define their arrow pieces similarly, although some of them are a bit more complicated. Download the example to see how they work.

Conclusion

This example defines some useful drawing extension methods. Even if you never need to draw arc wedges, line segments with arrowheads, or tic marks on an arc, the basic technique of adding extension methods to the Graphics class is extremely useful.

Download the example to experiment with the methods, to see additional details, or to try adding your own extension methods.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics, mathematics | Tagged , , , , , , , , , , , , | Leave a comment

Make an Apollonian gasket filled with images in C#


[Apollonian gasket]

This example shows how to draw an Apollonian gasket filled with images of a cat. (I promise that this will be my last cat fractal for a while, although I may have one other cat image post coming up. ;-))

To build an Apollonian gasket, start with a large circle that contains three other circles that are tangent to each other and to the outer circle. Next, find smaller circles that are tangent to all of the possible combinations of three adjacent circles. For example, for each pair of inner circles, find a circle tangent to that pair and the outer circle. Also find the circle that is tangent to all three of the inner circles.

Every time you create a new circle, you create new areas adjacent to that circle where you can find other smaller tangent circles. You recursively find those circles, creating new areas, and finding circles inside them until you reach some desired depth of recursion.

[Apollonian gasket]

The picture on the right shows an Apollonian gasket.

The example Draw an Apollonian gasket in C# explains how you can find an Apollonian gasket. This example modifies that one by placing an image inside each of the gasket’s circles. Not that for the picture at the top of the post I made the cat image a bit too large inside its bitmap so the cat’s ears extend outside of the circles. I thought that looked better. If you make the cat smaller, there’s more space between the cats.

To make drawing the images easier, I created a Circle class. The following section describes that class. The rest of this post explains the other new parts of the program.

The Circle Class

The following code shows the beginning of the Circle class.

// Represents a circle.
class Circle
{
    private static Bitmap _GasketImage = null;
    private static Rectangle SourceRect;
    public static Bitmap GasketImage
    {
        set
        {
            _GasketImage = value;
            SourceRect = new Rectangle(0, 0,
                _GasketImage.Width,
                _GasketImage.Height);
        }
    }
    public static PointF GasketCenter;
    ...

The class first defines some fields that it will use to draw its image. The private _GasketImage variable holds the image that should be drawn inside the circle. You can set this equal to null if you don’t want to draw an image.

The SourceRect field will hold a rectangle that indicates the image’s bounds. The class uses that value later to make drawing the image easier.

Notice that _GasketImage is static so it is shared by all instances of the class. That value only needs to be set once to the image that the gasket will draw and then all of the Circle objects can use the value.

Next the class defines a static GasketImage property. The property includes a set procedure but not a get procedure so the property is write-only. The set procedure saves its value in the _GasketImage value and initializes SourceRect to indicate the area that contains the image.

The class then defines the GasketCenter field. This value is a point indicating the center of the gasket. The program will use that to determine how to rotate each of the circle images.

The next part of the class deals with the circle’s geometry.

public PointF Center;
public float Radius;
public Circle(float new_x, float new_y, float new_radius)
{
    Center = new PointF(new_x, new_y);
    Radius = Math.Abs(new_radius);
}
...

As you would expect, the Center and Radius values give the circle’s center point and radius.

The class’s constructor simply saves the circle’s center point and radius.

Next, the class defines the following method.

// Return the circle's bounds.
public RectangleF GetBounds()
{
    return new RectangleF(
        Center.X - Radius, Center.Y - Radius,
        2 * Radius, 2 * Radius);
}

The GetBounds method returns the circle’s bounding rectangle.

The last and most interesting piece of the Circle class is the following Draw method.

// Draw the circle.
public void Draw(Graphics gr, Pen pen)
{
    if (Radius < 1) return;
    if (_GasketImage != null)
    {
        GraphicsState state = gr.Save();
        gr.ResetTransform();
        float dx = Center.X - GasketCenter.X;
        float dy = Center.Y - GasketCenter.Y;
        float angle = 0;
        if ((dx != 0) || (dy != 0))
        {
            angle = (float)(Math.Atan2(dy, dx) * 180 / Math.PI);
            angle += 90;
        }
        gr.RotateTransform(angle);
        gr.TranslateTransform(Center.X, Center.Y, MatrixOrder.Append);

        PointF[] dest_points =
            {
                new PointF(-Radius, -Radius),
                new PointF(+Radius, -Radius),
                new PointF(-Radius, +Radius),
            };
        gr.DrawImage(_GasketImage,
            dest_points, SourceRect, GraphicsUnit.Pixel);
        gr.Restore(state);
    }
    gr.DrawEllipse(pen, GetBounds());
}

If the circle has a radius less than one, then the method simply returns because. There’s little point in drawing an image that has a radius of less than one pixel.

Next, if the circle’s GasketImage value is defined, the method draws the image. Before it does that, it uses the Graphics object’s Save method to save that object’s state so it can restore it later. It then resets the graphics transformation to remove any rotations or translations that were added earlier by other Circle objects.

Next the code calculates the angle from the gasket’s center to the circle’s center. To do that it gets the X and Y offset between those two points and then uses Math.ATan2 to calculate the angle. It converts the angle from radians into degrees and adds 90 degrees to make the bottom of the image point toward the center of the gasket.

The rest is relatively easy. The code applies a rotation transformation to orient the image. It then applies a translation to move the origin to the circle’s center. Next the method creates an array of points that defines the image’s upper left, upper right, and lower left corners when it is centered at the origin. The code then draws the image centered at the origin. The Graphics object’s transformations automatically scale the image so it fits within the points defined by dest_points, rotates the image to the correct angle, and then translates the image to the circle’s position.

After it finishes drawing the image, the code resets the Graphics object’s state. (That isn’t really necessary in this example because every Circle object clears the Graphics object’s transformations, but it’s a good practice.)

The last thing the Draw method does is outline the circle with the pen passed into the method.

Colors

The program’s File menu lets you select the image to draw. It also lets you save the resulting gasket. Those menu items are straightforward so I won’t cover them in detail. The way the program handles colors is slightly more interesting.

The program’s Colors menu lets you pick three colors: a background color, a circle background color, and a circle outline color.

The program fills the gasket’s bitmap with the background color. It then fills the large outer circle with the circle background color. Finally, the Circle class’s Draw method outlines each circle with the circle outline pen.

The main program uses the following code to initially define the colors.

// Various colors.
private Color BitmapBackgroundColor = Color.Transparent;
private Color CircleBackgroundColor = Color.Transparent;
private Color CircleOutlineColor = Color.Transparent;

If you do not change the colors, they are transparent so you won’t see them in the final result.

The Colors menu’s commands display a ColorDialog to let you pick new colors. Unfortunately that dialog does not let you set the color’s alpha (opacity) component, so there’s no way to pick transparent or semi-transparent colors. You can create or download your own color dialog that lets you specify transparency if you like. Maybe I’ll do that some day, but for now I decided to not worry about it. If you change a color and then decide that you want to make it transparent again, just restart the program and begin again.

The rest of the program follows the previous example closely. It uses that post’s code to recursively find the circles that make up the gasket. This example has just a few main differences.

The first set of differences occurs before the program starts finding circles. The FindApollonianPacking method that starts the process creates the result bitmap and sets the Circle class’s static GasketCenter value to mark the center of the gasket. That method also clears the bitmap with the background color and fills the large outer circle with its color.

The last main difference occurs in the code that draw the circles. That code calls the FindApollonianCircle method defined by the earlier post. The new version of that method returns a Circle object representing a circle. The drawing code then calls the Circle object’s Draw method to draw the image.

Conclusion

This example is similar to the previous one that draws Apollonian gaskets. The difference is that this version draws an image inside each of the gasket’s circle. The Circle class encapsulates the code that draws the images so it makes the main program’s code a bit simpler. That would also allow you to easily change the way the circles are drawn or filled if you want to do that. Simply change the way the Circle class draws its circles.

Download the example to see additional details and to experiment with the program.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in algorithms, drawing, graphics, mathematics | Tagged , , , , , , , , , , , | 2 Comments

Draw an image spiral in C#


[image spiral]

This example lets you draw image spirals similar to the one shown above. This example lets you draw image spirals similar to the one shown above. It’s mostly just for fun, although it does include a useful exercise in graphics transformations.

The program has quite a few controls that you can use to change the final image’s appearance. The following section explains how to use those controls to draw image spirals. The rest of the post explains how the program works.

Using the Program

The following lists summarize the program’s controls.

Inside the File menu:

  • Open – This lets you select the spiral image. (The cat in the previous picture.) This image should have a transparent background.
  • Background Color – This lets you set the spiral’s background image.
  • Background Image – This lets you set the spiral’s background picture. (The star field in the previous image.)
  • Save As – This saves the current spiral image in a file.
  • Exit – I’m sure you can guess what this does.

On the program’s form:

  • Width, Height – These set the size of the spiral image.
  • A, B – These are parameters that determine the shape of the spiral’s points. In general, larger values of B expand more quickly.
  • # Spirals – The program draws this number of spirals. The picture above draws two spirals. If you follow one of them, you’ll see that there is another spiral between its laps around the center.
  • Scale – This is a scale factor that is applied to the main image (the cat). If you make this larger, the space between images will shrink or the images may overlap. Make this smaller to reduce the space between images. (Experiment with it.)
  • DTheta – This gives the number of degrees that the program should move around the spiral between images.
  • Darken – If this box is checked, then images closer to the center of the spiral are darker, giving the appearance that they are farther away. The following picture shows an example.


[image spiral]

General Approach

One way you could draw the image spiral would be to find a point on the spiral, determine how far away the next loop on the next spiral is, and make the image tall enough to fill the area between the two spirals. You could then increment the angle theta that you use to generate points on the spiral by an amount large enough to move past the current image.

That method would work and in some sense would the the “right” way to do it. Unfortunately it would also be pretty hard. I worked on a version that takes that approach, and it was much more work than it was worth and this version is complicated enough as it is.

The approach I took instead is to let the user control the image’s size and the amount by which the program moves around the spiral between images. The Scale text box lets you adjust the size of the images to get a nice result. The DTheta text box lets you determine how far around the spiral to move between images.

MakeSpiral

When you click Draw, the program calls the following MakeSpiral method.

// If we have a background and base image, make the spiral.
private void MakeSpiral()
{
    // Get the parameters.
    int width, height, num_spirals;
    float scale, A, B, dtheta;
    if (ParametersInvalid(out width, out height,
        out scale, out A, out B, out dtheta, out num_spirals)) return;

    // Size the result PictureBox and the form.
    picResult.ClientSize = new Size(width, height);
    int wid = Math.Min(panScroll.Left + picResult.Width + panScroll.Left, 800);
    int hgt = Math.Min(panScroll.Top + picResult.Width + panScroll.Left, 800);
    if (wid < ClientSize.Width) wid = ClientSize.Width;
    if (hgt < ClientSize.Height) hgt = ClientSize.Height;
    ClientSize = new Size(wid, hgt);

    // Make the result bitmap.
    Bitmap bm = new Bitmap(width, height);
    using (Graphics gr = Graphics.FromImage(bm))
    {
        gr.SmoothingMode = SmoothingMode.AntiAlias;
        if (BgImage != null)
        {
            // Draw the background image.
            using (Brush brush = new TextureBrush(BgImage))
            {
                gr.FillRectangle(brush, 0, 0, width, height);
            }
        }
        else
        {
            // Fill the background with the background color.
            gr.Clear(BgColor);
        }

        // Draw the spiral.
        DrawImageSpiral(gr, BaseImage, width, height,
            scale, A, B, dtheta, num_spirals);
    }

    // Display the result.
    picResult.Image = bm;
}

This method calls the ParametersInvalid method to get the spiral’s parameters. That method simply parses the various values that you entered on the form. If any of the values is missing or does not parse correctly, the method displays an error message and returns false so the MakeSpiral method exits without doing anything. The ParametersInvalid method is straightforward so it isn’t shown here. Download the example to see the details.

Next the MakeSpiral method sizes the picResult PictureBox to hold the image spiral. It then sets the form’s client size so it is large enough to display the result.

The method then creates a bitmap to hold the image spiral and makes a Graphics object to go with it. If you used the File menu’s Background Image command to set a background image, the code draws that image on the bitmap. Note that the code does not preserve the background image’s aspect ratio. If the image does not have the same width-to-height ratio as the image spiral bitmap, the image will be distorted to fit.

If you did not pick a background image, then the method fills the bitmap with the currently selected background color.

Next the method calls the DrawImageSpiral method described in the following section to draw the spiral on top of the background. It finishes by displaying the bitmap on the picResult PictureBox.

DrawImageSpiral

The following DrawImageSpiral method draws the spiral’s images on the bitmap.

// Draw the spiral of images.
private void DrawImageSpiral(Graphics gr, Bitmap image,
    int width, int height, float scale, float A, float B,
    float dtheta, int num_spirals)
{
    // Find the maximum distance to the rectangle's corners.
    PointF center = new PointF(width / 2, height / 2);
    float max_r = (float)Math.Sqrt(
        center.X * center.X + center.Y * center.Y);

    // Draw the spirals.
    float start_angle = 0;
    float d_start = (float)(2 * Math.PI / num_spirals);
    for (int i = 0; i < num_spirals; i++)
    {
        List<PointF> points =
            GetSpiralPoints(center, A, B, start_angle, dtheta, max_r);

        foreach (PointF point in points)
        {
            float dist = Distance(center, point);
            float r = dist / 2;
            DrawImageAt(gr, center, point, r, scale);
        }

        start_angle += d_start;
    }
}

This method finds the coordinates of the center of the image spiral. It then calculates the distance from the center of the image to its corners. Later the program will generate points on the spiral until they are this distance away from the spiral’s center. The code adds the height of the base image (the cat) to the distance so it is sure that it generates points far enough away from the center to make any images that overlap the bitmap visible.

The method then loops through the desired number of spirals. For each spiral, the code calls the GetSpiralPoints method to get points on the spiral where images should be drawn. It loops through those points and calls DrawImageAt to draw the image at each point.

The following section describes the GetSpiralPoints method. The section after that describes the DrawImageAt method.

GetSpiralPoints

The following GetSpiralPoints method generates the points on the image spiral where images should be drawn.

// Return points that define a spiral.
private List<PointF> GetSpiralPoints(
    PointF center, float A, float B,
    float angle_offset, float dtheta, float max_r)
{
    // Get the points.
    List<PointF> points = new List<PointF>();
    float min_theta = (float)(Math.Log(0.1 / A) / B);
    for (float theta = min_theta; ; theta += dtheta)
    {
        // Calculate r.
        float r = (float)(A * Math.Exp(B * theta));

        // Convert to Cartesian coordinates.
        float x, y;
        PolarToCartesian(r, theta + angle_offset, out x, out y);

        // Center.
        x += center.X;
        y += center.Y;

        // Create the point.
        points.Add(new PointF((float)x, (float)y));

        // If we have gone far enough, stop.
        if (r > max_r) break;
    }
    return points;
}

This method calculates a minimum value for theta where the points on the spiral are so close together that they occupy the same pixel. It then enters a loop where the looping variable theta starts at that minimum value. Each trip through the loop, the methods adds the value dtheta to theta. The value dtheta is the value that you entered on the form that was parsed and converted into radians by the ParametersInvalid method.

Inside the loop, the code uses the value theta to calculate the polar coordinate radius r. The code uses the equation r = Ae to draw a logarithmic spiral. For more information on that type of spiral, see my post Draw a logarithmic spiral in C#.

After the has the point’s r and theta values, the code converts the points into Cartesian coordinates. It offsets the spiral by the position of the spiral’s center so the spiral is centered on the bitmap and adds the new point to the points list.

When the r value is greater than the maximum necessary distance from the center of the image, the code breaks out of the loop and returns the points.

DrawImageAt

The following code shows one of the example’s more interesting methods, the DrawImageAt method that draws the base image (the cat) appropriately rotated and scaled at a point on the spiral.

// Draw the base image at the indicated point
// appropriately scaled and rotated.
private void DrawImageAt(Graphics gr, PointF center,
    PointF point, float r, float image_scale)
{
    float dx = point.X - center.X;
    float dy = point.Y - center.Y;
    float angle = (float)(Math.Atan2(dy, dx) * 180 / Math.PI) + 90;
    float sx = r / BaseImage.Width * image_scale;
    float sy = r / BaseImage.Height * image_scale;
    float scale = Math.Min(sx, sy);
    GraphicsState state = gr.Save();
    gr.ResetTransform();
    gr.ScaleTransform(scale, scale);
    gr.RotateTransform(angle, MatrixOrder.Append);
    gr.TranslateTransform(point.X, point.Y, MatrixOrder.Append);

    PointF[] dest_points =
    {
        new PointF(-BaseImage.Width / 2, -BaseImage.Height / 2),
        new PointF(BaseImage.Width / 2, -BaseImage.Height / 2),
        new PointF(-BaseImage.Width / 2, BaseImage.Height / 2),
    };
    RectangleF src_rect = new RectangleF(0, 0, BaseImage.Width, BaseImage.Height);

    if (chkDarken.Checked)
    {
        float radius = Math.Min(center.X, center.Y) / 3f;
        float dark_scale = r / radius + 0.2f;
        if (dark_scale > 1) dark_scale = 1;
        Bitmap bm = AdjustBrightness(BaseImage, dark_scale);
        gr.DrawImage(bm, dest_points, src_rect, GraphicsUnit.Pixel);
    }
    else
    {
        gr.DrawImage(BaseImage, dest_points, src_rect, GraphicsUnit.Pixel);
    }

    gr.Restore(state);
}

The method first gets the X and Y distances dx and dy from the spiral’s center to the point. It uses those values to calculate the point’s angle around the spiral. (The GetSpiralPoints method could save each point’s theta value so we wouldn’t need to calculate it here.) The code adds 90 degrees to the angle so the image will have its base pointing toward the center of the image spiral.

The code then calculates the image’s scale factor. To find horizontal and vertical scale factors, the code divides the point’s distance from the spiral’s center by the image’s width/height and multiples the result by the scale factor that you entered in the Scale text box. The code then uses the smaller of the two scale values.

Next the program saves the Graphics object’s state so it can restore it later. (That isn’t strictly necessary in this example but it’s a good practice. The code then resets the Graphics object’s transform to remove any previous transformations and installs new transformations. First it scales the drawing by the scale factor that it calculated. It then rotates the result by the angle calculated earlier and finishes by translating the image to its desired location.

Now if the method draws the image at the origin, it will be scales, rotated, and translated into position. To draw the image at the origin, the code defines three destination points that indicate where the upper left, upper right, and lower left corners of the image should be placed. It also defines a source rectangle indicating the part of the image that should be drawn.

At this point, the program is finally ready to draw … almost. If you checked the Darken box, the code calculates a scaled radius value and uses it to find a scale darkening factor. I just picked this value through trial and error to make the middle third of the images increasingly dark but not too dark. You can fiddle with this formula if you like.

After calculating the darkening scale factor, the code calls the AdjustBrightness method to make a copy of the base image that has been darkened.

The AdjustBrightness method uses an ImageAttributes object to quickly adjust an image’s brightness. To see how that works, look at my post Use an ImageAttributes object to adjust an image’s brightness in C#

Finally, after it has a darkened copy of the base image, the code simply draws it at the origin and the Graphics object’s transformations do the rest.

If you did not check the Darken box, the program simply draws the image at the origin and the Graphics object’s transformations do the rest.

Conclusion

I think the basic idea behind the program is understandable enough. You generate points on a spiral spaced some angle apart. For each point, you draw an image that is scaled and rotated so its bottom is toward the spiral’s center. Unfortunately the details are pretty complicated. That’s why I decided to let you control the angle between images and to give you some control over the image scaling. Allowing you to change the scale factor also lets you experiment with the program to find more interesting results. For example, you can increase the scale factor to make the images touch or even overlap.

Download the example and give it a try. If you produce any particularly interesting results, post links to them in the comments below.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics, image processing, mathematics | Tagged , , , , , , , , , , | Leave a comment

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




Posted in drawing, graphics | Tagged , , , , , , , , , | Leave a comment

Draw a heartagram in C#

[example]

The heartagram is a symbol created by vocalist Ville Valo and used by the band HIM. I’m not particularly a fan, but I think the heartagram shape is interesting so I wanted to make one with a C# program.

The most interesting part of the program is the following MakeHeartagram method, which creates a GraphicsPath that defines the heartagram.

// Return a GraphicsPath representing a heartagram.
private GraphicsPath MakeHeartagram(PointF center,
    float radius, float pen_width)
{
    // Define scales to place the points the right
    // distances from the edges of the circle.
    float[] scales =
    {
        1 - 1.75f * pen_width / radius,
        1 - 1.75f * pen_width / radius,
        0.9f,
        0.9f,
        1 - 1.75f * pen_width / radius,
    };

    // Find the points of a pentagon within the area.
    double angle = Math.PI / 2.0;
    double dtheta = Math.PI * 2.0 / 5.0;

    PointF[] points = new PointF[5];
    for (int i = 0; i < points.Length; i++)
    {
        points[i] = new PointF(
            (float)(center.X + radius * Math.Cos(angle) * scales[i]),
            (float)(center.Y + radius * Math.Sin(angle) * scales[i]));
        angle += dtheta;
    }

    // Find the top intersection point.
    PointF top = new PointF(
        center.X,
        center.Y - radius * 0.8f);

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

    float tension = 1f;
    PointF[] curve1_points =
    {
        points[1],
        points[3],
        points[0],
    };
    path.AddCurve(curve1_points, tension);

    PointF[] curve2_points =
    {
        points[0],
        points[2],
        points[4],
    };
    path.AddCurve(curve2_points, tension);

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

    return path;
}

The method first creates a scales array. The method uses the values in that array to define points arranged in a pentagon. The scale values move the pentagon’s points closer or farther from the heartagram’s circumference to give a good result.

[example]

The program uses the variable angle to loop over points that define the pentagon. Each angle is 2 π / 2 radians after the angle before it. As the program loops over the points’ angles, it multiplies the heartagram’s radius by the angle’s scale factor and the sine of cosine of the angle to get the point’s location. When the loop is finished, the points array holds the pentagon’s points. The picture on the right shows the pentagon with its corners labeled. Notice that the scale values result in a non-centered pentagon. The points must be moved away from the heartagram’s circumference to make the final shape’s corners just touch the circumference.

The program then creates a GraphicsPath object to hold the heartagram and adds the pieces of the shape to it. It first adds a line from point 4 to point 1.

Next, the code creates a smooth curve starting at point 1, moving to point 3, and ending at point 0. That creates the smooth lobe on the heartagram’s upper right.

The code then creates a second smooth curve starting at point 0, moving to point 2, and ending at point 4. That creates the left lobe.

[example]

The method calls the GraphicsPath object’s CloseFigure method to make the figure a closed curve. If you skip that step, the GraphicsPath treats this as an open curve so its other corners are mitered but the final corner is not as shown in the picture on the right.

Finally, the method returns the GraphicsPath object.

The program uses the following method to draw the heartagram’s background, enclosing ellipse, and the heartagram itself.

private void DrawHeartagram(Graphics gr, PointF center,
    float radius, Brush brush, Pen pen)
{
    gr.FillEllipse(brush,
        center.X - radius,
        center.Y - radius,
        2 * radius,
        2 * radius);
    gr.DrawEllipse(pen,
        center.X - radius,
        center.Y - radius,
        2 * radius,
        2 * radius);

    // Make a GraphicsPath to represent the heartagram.
    GraphicsPath path = MakeHeartagram(center, radius, pen.Width);

    // Draw the GraphicsPath.
    gr.DrawPath(pen, path);
}

This code fills and outlines an ellipse with the brush and pen that are passed into the method. In this example, the brush is a linear gradient brush and the pen is a thick, black pen.

After it draws the ellipse, the program calls the MakeHeartagram method described earlier to get a GraphicsPath that defines the heartagram. The code then draws that path with the pen.

Download the example to experiment with it and to see additional details.

The result is pretty good, but it’s not exactly the same as the shape drawn by Ville Valo. If you look closely at the heartagram drawn by this program, you’ll see that its edges curve in to the shape’s points. In other words, they are not straight. You can also see that the rounded lobes in this program’s heartagram are narrower than they are in heartagrams that you can find online. Both of those issues arise because this program uses a smooth curve to define the lobes. The heartagrams online seem to use circular arcs for the lobes.

In my next post, I’ll describe an improved version of the program that uses circular arcs for the heartagram’s lobes.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics | Tagged , , , , , , , , , | 1 Comment

Connect two line segments with a circular arc in C#


[circular arc]

Recently I wanted to make a circular arc that connected two line segments. The .NET DrawCurve method lets you connect points with a smooth curve relatively easily, but the curve is a spline and not a circular arc. (I’ll post examples showing both versions of the program I was making a bit later.) This example explains how you can find a circular arc that connects two line segments.

First suppose the line segments are tangent to some circle as shown in the following picture.


[circular arc]

In this case, you can use some geometry (which I’ll described later) to find the circular arc. Unfortunately if you let the user define the line segments by clicking with the mouse, it’s very likely that the line segments won’t be tangents of any circle. In that case you will need to extend one of the segments as shown in the following picture.


[circular arc]

Shortly I’ll show how you can extend one of the segments to make it tangent to a circle. However, first notice that it is not always possible to extend a segment to a tangent, at least in the way that we want. For example, the following picture shows two line segments that cannot be extended in a simple way so they become tangents of a circle. You can make these segments tangents, but not in the simple way that I want.


[circular arc]

I won’t talk further about these stranger arcs, although you could find them if you want to connect segments with that kind of circular arc.

The following three sections explain:

  • How to extend one of the segments so both segments are tangent to a circle
  • How to find the circle
  • How to find the circular arc
  • The program’s C# code

Extending a Segment

Extend the two line segments until they intersect as shown in the following picture.


[circular arc]

There are infinitely many circles tangent to the two extended line segments. The circle that we want is the one that is tangent at one of the original segments’ end points. That end point will be the one that is closer to the lines’ point of intersection (POI). The other tangent point is the same distance away from the POI, and its line segment must be extended to that point.

Here’s the algorithm for extending one of the line segments until they are both tangent to a circle.

  1. Find the POI between the two extended line segments.
  2. On each segment, find the end point that is closer to the POI.
  3. Of the two closer points, find the one that is closer to the POI. Call that point P1. Let D be the distance from point P1 to the POI.
  4. Find the point on the other extended line segment that is distance D from the POI. Call that point P2.

If the two line segments are parallel, then they do not intersect. You can still find a circular arc to connect the segments, but you’ll need to use a slightly different method, which I won’t cover here. Again, this isn’t the kind of circular arc I need.

Finding the Circle

Now that you have the points P1 and P2, the following picture shows how you can find the circle.


[circular arc]

The two segments are tangent to the circle so lines that are pependicular to those segments at the tangent points will pass through the center of the circle. Make two lines that are perpendicular to points P1 and P2, and find the intersection of those two lines. That is the circle’s center.

Now calculate the distance between the center and either point P1 or P2 to get the circle’s radius.

Together the circle’s center and radius define the circle.

Finding the Circlular Arc

After you know the circle’s center, you can calculate the start angle and sweep angle for the circular arc. (The DrawArc method uses a start angle and sweep angle so that’s what we need to find.)

First, subtract P1s X and Y coordinates from the coordinates of the circle’s center to get the distances dx and dy from the circle’s center to point P1. Now use Math.ATan2(dy, dx) to find the angle from the circle’s center to point P1.

Use similar steps to find the angle to point P2. Finally, subtract the two angles to get the sweep angle for the circular arc.

The circle’s center and radius gives us the bounding rectangle that should contain the circular arc. The start and sweep angles tell where the circular arc should start and how far it should extend. Those are all of the values that we need to draw the circular arc.

C# Code

STOP: Be sure you understand the previous sections before you look at the code. The code just implements the techniques described in the previous discussion, so it will be a lot easier to follow if you understand that discussion.

Saving Points

When you click on the program’s form, the following code springs into action.

private Listlt;PointF> Seg1Points = new List();
private Listlt;PointF> Seg2Points = new List();

// Save a new point.
private void Form1_MouseClick(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        if (Seg1Points.Count == 2) Seg1Points = new List();
        Seg1Points.Add(e.Location);
        Refresh();
    }
    else
    {
        if (Seg2Points.Count == 2) Seg2Points = new List();
        Seg2Points.Add(e.Location);
        Refresh();
    }
}

This code adds the clicked point to either the Seg1Points or Seg2Points list, depending on whether you left- or right-click. If the correct list already contains two points, the code replaces it with a new list. It then adds the new point and refreshes to redraw the picture.

Drawing the Picture

The following Paint event handler draws the picture.

private void Form1_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

    // See if both segments are defined.
    if ((Seg1Points.Count == 2) &&
        (Seg2Points.Count == 2))
    {
        // Both segments are defined.
        // Find the arc.
        RectangleF rect;
        float start_angle, sweep_angle;
        PointF s1_far, s1_close, s2_far, s2_close;
        FindArcFromSegments(
            Seg1Points[0], Seg1Points[1],
            Seg2Points[0], Seg2Points[1],
            out rect, out start_angle, out sweep_angle,
            out s1_far, out s1_close, out s2_far, out s2_close);

        using (Pen thick_pen = new Pen(Color.Green, 2))
        {
            // Draw the revised segments.
            e.Graphics.DrawLine(thick_pen, s1_far, s1_close);
            e.Graphics.DrawLine(thick_pen, s2_far, s2_close);

            // Draw the arc.
            thick_pen.Color = Color.Red;
            e.Graphics.DrawArc(thick_pen, rect, start_angle, sweep_angle);

            // Draw the returned points that connect to the arc.
            e.Graphics.FillPoint(Brushes.Red, s1_far, 5);
            e.Graphics.FillPoint(Brushes.Red, s1_close, 5);
            e.Graphics.FillPoint(Brushes.Red, s2_far, 5);
            e.Graphics.FillPoint(Brushes.Red, s2_close, 5);
        }
    }
    else
    {
        // Both segments are not defined.
        using (Pen thick_pen = new Pen(Color.Green, 2))
        {
            // Draw the segments.
            if (Seg1Points.Count == 2)
                e.Graphics.DrawLine(thick_pen,
                    Seg1Points[0], Seg1Points[1]);
            if (Seg2Points.Count == 2)
                e.Graphics.DrawLine(thick_pen,
                    Seg2Points[0], Seg2Points[1]);
        }
    }

    // Draw the user-selected points. This will
    // overwrite all but one of the returned points.
    foreach (PointF point in Seg1Points)
        e.Graphics.FillPoint(Brushes.Green, point, 5);
    foreach (PointF point in Seg2Points)
        e.Graphics.FillPoint(Brushes.Green, point, 5);
}

The code first determines whether both lists contain two points. If they do, then you have fully specified both line segments so the code can connect them with a circular arc.

If both lists contain two points, then the code calls the FindArcFromSegments method to get the circular arc parameters. The code then draws the picture. First it draws the line segments returned by the FindArcFromSegments method. That method sets its s1_far, s1_close, s2_far, and s2_closeparameters to the updated end points of the two segments. Three of those points are points that the user clicked. The fourth is the one labeled P2 in the earlier picture that was moved so its segment was tangent to the circle.

Next the code draws the arc. This piece of code finishes by drawing dots at the revised segments’ end points. Note that the code uses the FillPoint extension method to draw the points. That extension method and a few others are useful but not very complicated or interesting, so I won’t describe them here. Download the example to see how they work.

If the two point lists are not both full, the program draws either of the two line segments that are defined.

The code finishes by drawing any points that were selected by the user. If all four points are defined, then that will draw over three of the four points drawn earlier; all except the one labeled P2 earlier.

FindArcFromSegments

The following FindArcFromSegments method performs the calculations described earlier.

// Find a circular arc connecting the segments.
// Return the arc's parameters. Also return new points
// to define the segments so you can draw
// s1_far -> s1_close -> arc -> s2_close -> s2_far.
// Three os those points will be original segments points.
private void FindArcFromSegments(
    PointF s1p1, PointF s1p2,
    PointF s2p1, PointF s2p2,
    out RectangleF rect,
    out float start_angle, out float sweep_angle,
    out PointF s1_far, out PointF s1_close,
    out PointF s2_far, out PointF s2_close)
{
    // See where the segments intersect.
    PointF poi;
    bool lines_intersect, segments_intersect;
    PointF close1, close2;
    FindIntersection(s1p1, s1p2, s2p1, s2p2,
        out lines_intersect, out segments_intersect,
        out poi, out close1, out close2);

    // See if the lines intersect.
    if (!lines_intersect)
    {
        // The lines are parallel. Find the 180 degree arc.
        throw new NotImplementedException("The segments are parallel.");
    }

    // Find the point on each segment that is closest to the poi.
    float close_dist1, close_dist2, far_dist1, far_dist2;

    // Make s1_close be the closer of the points.
    if (s1p1.Distance(poi) < s1p2.Distance(poi))
    {
        s1_close = s1p1;
        s1_far = s1p2;
        close_dist1 = s1p1.Distance(poi);
        far_dist1 = s1p2.Distance(poi);
    }
    else
    {
        s1_close = s1p2;
        s1_far = s1p1;
        close_dist1 = s1p2.Distance(poi);
        far_dist1 = s1p1.Distance(poi);
    }

    // Make s2_close be the closer of the points.
    if (s2p1.Distance(poi) < s2p2.Distance(poi))
    {
        s2_close = s2p1;
        s2_far = s2p2;
        close_dist2 = s2p1.Distance(poi);
        far_dist2 = s1p2.Distance(poi);
    }
    else
    {
        s2_close = s2p2;
        s2_far = s2p1;
        close_dist2 = s2p2.Distance(poi);
        far_dist2 = s1p1.Distance(poi);
    }

    // See which of the close points is closer to the poi.
    if (close_dist1 < close_dist2)
    {
        // s1_close is closer to the poi than s2_close.
        // Find the point on seg2 that is distance
        // close_dist1 from the poi.
        s2_close = PointAtDistance(poi, s2_far, close_dist1);
        close_dist2 = close_dist1;
    }
    else
    {
        // s2_close is closer to the poi than s1_close.
        // Find the point on seg1 that is distance
        // close_dist2 from the poi.
        s1_close = PointAtDistance(poi, s1_far, close_dist2);
        close_dist1 = close_dist2;
    }

    // Find the arc.
    FindArcFromTangents(
        s1_close, s1_far,
        s2_close, s2_far,
        out rect, out start_angle, out sweep_angle);
}

The code first calls the FindIntersection method to find the point where the lines containing the line segments intersect. You can learn about that method in my post Determine where two lines intersect in C#.

If the lines do not intersect, then the segments are parallel. You can write code to handle that situation if you like. This example just throws an exception.

Next, the method determines for each segment which end point is closer and which is farther from the POI. To do that, the code uses the Distance extension method. (Download the example to see how it works.)

The code then determines which of the segments’ close end points is closest to the POI. The distance from that point to the POI is the value D shown on the earlier pictures. The code moves the other segment’s close point so it is that same distance D from the POI. The code uses the PointAtDistance helper method described shortly to move the point.

Now the points s1_close, s1_far, s2_close, and s2_far define two segments that are tangent to the circle so we can find the arc. The code does that by calling the FindArcFromTangents method that is described in the following section. that method returns the values that this method (FindArcFromSegments) needs to return, so this method is finished.

The following PointAtDistancehelper method returns a point along a segment that is a specified distance away from the segment’s first end point. (The program uses this method to find a point distance D away from the segments’ POI.)

// Find a point on the line p1 --> p2 that
// is distance dist from point p1.
private PointF PointAtDistance(PointF p1, PointF p2, float dist)
{
    float dx = p2.X - p1.X;
    float dy = p2.Y - p1.Y;
    float p1p2_dist = (float)Math.Sqrt(dx * dx + dy * dy);
    return new PointF(
        p1.X + dx / p1p2_dist * dist,
        p1.Y + dy / p1p2_dist * dist);
}

This method calculates the horizontal and vertical distances dx and dy between the segment’s two end points. It divides those distances by the segment’s length and multiples by the desired distance from the first end point. The method adds the result to the first end point’s coordinates and that gives the desired point.

FindArcFromTangents

The following code shows the FindArcFromTangents method.

// Find the arc that connects points s1p2 and s2p2.
private void FindArcFromTangents(
    PointF s1_close, PointF s1_far,
    PointF s2_close, PointF s2_far,
    out RectangleF rect,
    out float start_angle, out float sweep_angle)
{
    // Find the perpendicular lines.
    PointF perp_point1, perp_point2;

    float dx1 = s1_close.X - s1_far.X;
    float dy1 = s1_close.Y - s1_far.Y;
    perp_point1 = new PointF(
        s1_close.X - dy1,
        s1_close.Y + dx1);

    float dx2 = s2_close.X - s2_far.X;
    float dy2 = s2_close.Y - s2_far.Y;
    perp_point2 = new PointF(
        s2_close.X + dy2,
        s2_close.Y - dx2);

    // Find the point of intersection between segments
    // s1_close --> perp_point1 and
    // s2_close --> perp_point2.
    bool lines_intersect, segments_intersect;
    PointF poi, close_p1, close_p2;
    FindIntersection(
        s1_close, perp_point1,
        s2_close, perp_point2,
        out lines_intersect, out segments_intersect,
        out poi, out close_p1, out close_p2);

    // Find the radius.
    float dx = s1_close.X - poi.X;
    float dy = s1_close.Y - poi.Y;
    float radius = (float)Math.Sqrt(dx * dx + dy * dy);

    // Create the rectangle.
    rect = new RectangleF(
        poi.X - radius,
        poi.Y - radius,
        2 * radius, 2 * radius);

    // Find the start, end, and sweep angles.
    start_angle = (float)(Math.Atan2(dy, dx) * 180 / Math.PI);
    dx = s2_close.X - poi.X;
    dy = s2_close.Y - poi.Y;
    float end_angle = (float)(Math.Atan2(dy, dx) * 180 / Math.PI);

    // Make the angle less than 180 degrees.
    sweep_angle = end_angle - start_angle;
    if (sweep_angle > 180)
        sweep_angle = sweep_angle - 360;
    if (sweep_angle < -180)
        sweep_angle = 360 + sweep_angle;
}

The method first finds two line segments that are perpendicular to the original segments at the points P1 and P2 as shown in the earlier pictures. If a segment has X and Y components <dx, dy>, then the two sets of components <dy, -dx> and <-dy, dx> define two perpendicular segments. The code finds the original vectors’ components and switches them to get perpendicular components. It adds those components to the two points P1 and P2 to get the ends of the desired perpendicular segments. For example, the first perpendicular segment starts at point s1_close and ends at point perp_point1.

Next, the method uses the FindIntersection method to see where the two perpendicular lines intersect. That point is the center of the circle.

Now the code finds the distance from the center of the circle to the point P1. that gives the circle’s radius. Now that we know the circle’s center and radius, we can define the bounding rectangle for the circular arc.

The last thing the method needs to do is to calculate the start and sweep angles. It uses Meth.Atan2 to calculate the start angle (for point P1) and the end angle (for point P2). It then sets the sweep angle equal to the difference.

Because we want to use the smaller arc around the circle, the code then checks that the sweep angle is less than 180 degrees. If the angle is greater than 180 degrees, the code resets it to that angle minus 360 to make the arc go the right direction around the circle.

Similarly if the sweep angle is less than -180 degrees, the code resets it to 360 plus the angle, again to make the arc go the right direction around the circle. (try commenting out those if statements and experimenting to see what they do.)

Summary

This program still only draws one kind of circular arc. For example, it doesn’t draw any of the kinds of circular arcs shown in the following picture.


[circular arc]

With some work, you may be able to use the techniques described in this post to find those other kinds of circular arc if you need them some day. Meanwhile, download the example program and give it a try.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics | Tagged , , , , , , , , | Leave a comment

Crop images to specific sizes in C#

[example]

Sometimes I need to crop images to specific sizes or aspect ratios. For example, the following list shows some recommended image sizes for Google Business.

  • Profile image – 250×250 pixels
  • Cover photo – 1080×608 pixels
  • Shared images – 497×373 pixels

Unfortunately if you use a program like MS Paint to crop the image, it’s hard to get a desired size that is nicely centered on an area of interest. This example shows how you can crop images by positioning a selection rectangle over a desired part of the image.

Use the File menu’s Open command to open an image file. Next, enter the width and height of the area that you want to select. Then click and drag the selection rectangle to select the part of the image that you want to keep. Finally, use the File menu’s Save As command to save the selected part of the image into a new file.

The following sections describe the main pieces of the program’s code.

Dragging the Selection Rectangle

The program uses the following variables to keep track of the selection rectangle.

private Rectangle SelectedRect = new Rectangle();
private bool Dragging = false;
private Point LastPoint;

The variable SelectedRect gtracks the currently selected rectangle. The value Dragging is true while a drag is in progress. The point LastPoint stores the last mouse position while a drag is in progress.

When you press the mouse button down on the picImage PictureBox, the following event handler executes.

// Start dragging.
private void picImage_MouseDown(object sender, MouseEventArgs e)
{
    // If the mouse is not inside the
    // selection rectangle, do nothing.
    if (!SelectedRect.Contains(e.Location)) return;

    // Start the drag.
    LastPoint = e.Location;
    Dragging = true;
}

This code calls the SelectedRect rectangle’s Contains method to determine whether the mouse’s location is inside the rectangle. If the mouse is not inside the rectangle, the method returns without doing anything.

If the mouse is inside the rectangle, the code saves the mouse location in variable LastPoint and sets Dragging to true.

If you move the mouse over the PictureBox, the following event handler executes.

// Continue dragging.
private void picImage_MouseMove(object sender, MouseEventArgs e)
{
    // See if we are dragging.
    if (Dragging)
    {
        // We are dragging. Move the selected rectangle.
        SelectedRect.X += e.Location.X - LastPoint.X;
        SelectedRect.Y += e.Location.Y - LastPoint.Y;
        LastPoint = e.Location;
        picImage.Refresh();
    }
    else
    {
        // We are not dragging. Update the cursor.
        if (SelectedRect.Contains(e.Location))
            Cursor = Cursors.SizeAll;
        else
            Cursor = Cursors.Default;
    }
}

This code checks the Dragging variable to see if a drag is in progress.

If a drag is in progress, the code subtracts the coordinates of the previous mouse location (stored in the variable LastPoint) from the mouse’s current location. It uses the difference in coordinates to update the upper left corner of the selection rectangle. It then saves the mouse’s current location in variable LastPoint and refreshes the PictureBox to display new selection rectangle.

If no drag is in progress, the code calls the selection rectangle’s Contains method to see if the mouse if over the currently selected rectangle. If the mouse is over the rectangle, the program displays the SizeAll cursor. If the mouse is not over the rectangle, the program displays the default cursor.

When you release the mouse button, the following event handler executes.

// Stop dragging.
private void picImage_MouseUp(object sender, MouseEventArgs e)
{
    Dragging = false;
}

This code simply sets Dragging to false to end the current drag.

Drawing the Selection Rectangle

The following code shows how the program’s PictureBox draws the selection rectangle.

// Draw the selection rectangle.
private void picImage_Paint(object sender, PaintEventArgs e)
{
    try
    {
        // Draw the selection rectangle.
        using (Pen pen = new Pen(Color.Red, 2))
        {
            e.Graphics.DrawRectangle(pen, SelectedRect);

            pen.Color = Color.Yellow;
            pen.DashPattern = new float[] { 5, 5 };
            e.Graphics.DrawRectangle(pen, SelectedRect);
        }
    }
    catch
    {
    }
}

This code creates a two-pixel-wide red Pen and uses it to draw the selection rectangle.

The code then changes the pen’s color to yellow, gives it a dash pattern, and redraws the rectangle with the modified pen. The result is the thick, dashed, red and yellow rectangle shown in the picture at the top of the post.

Saving the Selected Area

When you invoke the File menu’s Save As command, the following code executes.

// Save the selected area.
private void mnuFileSaveAs_Click(object sender, EventArgs e)
{
    if (sfdImage.ShowDialog() == DialogResult.OK)
    {
        try
        {
            // Copy the selected area into a new Bitmap.
            Bitmap bm = new Bitmap(
                SelectedRect.Width,
                SelectedRect.Height);
            using (Graphics gr = Graphics.FromImage(bm))
            {
                gr.DrawImage(picImage.Image, 0, 0,
                    SelectedRect, GraphicsUnit.Pixel);
            }

            // Save the new Bitmap.
            SaveImage(bm, sfdImage.FileName);
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
    }
}

This code displays a SaveFileDialog to let you pick the file that should contain the selected part of the image. If you pick a file and click Save, the code creates a new Bitmap that is the same size as the rectangle and makes a Graphics object associated with the Bitmap. The code then uses the Graphics object’s DrawImage method to draw the selected part of the image on the Bitmap.

The code finishes by using the SaveImage method to save the bitmap into the file with the appropriate file format (JPG, PNG, BMP, etc.). For information on the SaveImage method, see my post Save images with an appropriate format depending on the file name’s extension in C#.

Resizing the Rectangle

If you change the value in the width or height text box, one of the following event handlers executes.

private void txtWidth_TextChanged(object sender, EventArgs e)
{
    // Get the new value.
    int value;
    if (!int.TryParse(txtWidth.Text, out value)) value = 0;
    SelectedRect.Width = value;
    picImage.Refresh();
}

private void txtHeight_TextChanged(object sender, EventArgs e)
{
    int value;
    if (!int.TryParse(txtHeight.Text, out value)) value = 0;
    SelectedRect.Height = value;
    picImage.Refresh();
}

These methods try to parse the width and height values. If a value is not a valid integer, the program sets that value to zero. It then sets the selection rectangle’s corresponding size accordingly and refreshes the PictureBox to show the resized rectangle.

Conclusion

This example lets you crop images to a specific size relatively easily. A nice enhancement would be to let you change the size of the selection rectangle while preserving its aspect ratio. Maybe I’ll add that feature some day if I’m really bored.

Download the example to experiment with it and to see additional details.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics, image processing, tools | Tagged , , , , , , , , , , | Leave a comment

Draw on top of a background image in C#


[background image]

Drawing on a background image is actually very easy. It seems to be a common question on the internet, however, so I decided to make this example.

This example draws on a background image in two ways. If you just move the mouse around, it draws the smiley face shown above. If you click and drag the mouse, the program draws an area selection rectangle as shown in the following picture.


[background image]

All you need to do to draw on a background image is to set a PictureBox control’s Image property and then draw in its Paint event handler without calling the Graphics object’s Clear method. If that’s all you wanted to know, you’re done with this post. If you want to learn a bit more about what the example program does, read on.

The program does all of its drawing in a PictureBox control’s Paint event handler. Before I explain how that works, I’ll explain the mouse events that control the area selection rectangle. (If you only want to know how to draw on the background image, skip to the section “Drawing on a Background Image.”)

Mouse Events

The program uses the following variables to track the selection rectangle.

private bool SelectingArea = false;
private Point StartPoint, EndPoint;

The program sets the SelectingArea to true while you are selecting an area. The StartPoint and EndPoint values keep track of where you press the mouse button and the mouse’s current location.

When you press the mouse button over the PictureBox, the following event handler executes.

// Start drawing.
private void picCanvas_MouseDown(object sender, MouseEventArgs e)
{
    StartPoint = e.Location;
    EndPoint = e.Location;
    SelectingArea = true;
    this.Cursor = Cursors.Cross;

    // Refresh.
    picCanvas.Refresh();
}

This code saves the mouse’s location in the StartPoint and EndPoint variables. It then sets SelectingArea to true to indicate that an area selection is in progress. It sets the cursor to a crosshair and finishes by refreshing the PictureBox to make it redraw itself.

When you move the mouse, the following event handler executes.

// Continue drawing.
private void picCanvas_MouseMove(object sender, MouseEventArgs e)
{
    // Update the end point.
    EndPoint = e.Location;

    // Refresh.
    this.Refresh();
}

This code simply saves the mouse’s new position and refreshes the PictureBox to make it redraw itself. Note that this code does not check the SelectingArea variable; it executes whether of not the mouse is pressed. That allows the Paint event handler to draw either the smiley face or the selection rectangle.

When you release the mouse button, the following event handler executes.

// Continue drawing.
private void picCanvas_MouseUp(object sender, MouseEventArgs e)
{
    SelectingArea = false;
    this.Cursor = Cursors.Default;

    // Do something with the selection rectangle.
    Rectangle rect = MakeRectangle(StartPoint, EndPoint);
    Console.WriteLine(rect.ToString());
}

This code first sets SelectingArea to false to indicate that the area selection is over. It then calls the MakeRectangle helper method described shortly to create a rectangle determined by the start and end points. It then displays the rectangle in the Output window. If you really wanted to allow the user to select an area, you would make the program do something with the rectangle here.

The following code shows the MakeRectangle helper method.

// Make a rectangle from two points.
private Rectangle MakeRectangle(Point p1, Point p2)
{
    int x = Math.Min(p1.X, p2.X);
    int y = Math.Min(p1.Y, p2.Y);
    int width = Math.Abs(p1.X - p2.X);
    int height = Math.Abs(p1.Y - p2.Y);
    return new Rectangle(x, y, width, height);
}

This method finds the two points’ minimum X and Y coordinates and the area’s width and height. It then uses those values to create and return a new Rectangle.

Drawing on a Background Image

To draw on the background image, first set the PictureBox control’s Image property to the background image. You may also want to set its SizeMode property to AutoSize to make the control fit the image.

Now simply draw on the control in the control’s Paint event handler. The Paint event automatically resets the control’s image whenever it needs to redraw so you don’t need to clear the event handler’s Graphics object. In fact, if you call the e.Graphics.Clear method, you will erase the background image.

The following code shows the picCanvas control’s Paint event handler.

// Draw the selection rectangle.
private void picCanvas_Paint(object sender, PaintEventArgs e)
{
    if (SelectingArea)
    {
        using (Pen pen = new Pen(Color.Yellow, 2))
        {
            e.Graphics.DrawRectangle(pen,
                MakeRectangle(StartPoint, EndPoint));

            pen.Color = Color.Red;
            pen.DashPattern = new float[] { 5, 5 };
            e.Graphics.DrawRectangle(pen,
                MakeRectangle(StartPoint, EndPoint));
        }
    }
    else
    {
        DrawSmiley(e.Graphics, EndPoint, 50);
    }
}

If you are currently selecting an area, then the code creates a thick yellow pen. It uses the MakeRectangle helper method to define the currently selected rectangle and draws it.

Next, the code changes the pen’s color to red and gives it a {5, 5} dash pattern so it draws 5 units and then skips 5 units. (Here a unit is a multiple of the pen’s width, so in this example the pen draws 10 pixels and then skips 10 pixels.) The code redraws the rectangle with the new pen parameters to give it a yellow and red dashed appearance.

(Multi-colored dashed lines show up relatively well no matter what colors the background image has. The dash pattern’s origin also changes as the mouse moves, giving the rectangle making the “marching ants” effect and making it even easier to see.)

If you are not currently selecting an area, the event handler simply calls the DrawSmiley method to draw a smiley face at the mouse’s current location, which is stored in variable EndPoint. That method is straightforward so I won’t describe it here. Download the example program to see how it works.

That’s all there is to drawing over the background. Just draw without erasing anything.

Drawing on Forms

You can draw on a form’s background image much as you can draw on a PictureBox, but you need to deal with two potential problems: tiling and tearing.

Tiling

A form does not have an Image property so you can’t place the background image there. You can set the form’s BackgroundImage property to the image, but then by default the form tiles the image to fill itself with copies of the image. There are a few ways that you can handle that.

The easiest approach is to just let the form tile its background image and not worry about it.

A second approach is to size the form to fit its image and then set its FromBorderStyle property to prevent the user from resizing the form and displaying the tiled copies. If you like, you can use the following Load event handler to fit the form to its background image and make it non-resizable.

// Size the form to fit its bacground image.
private void Form1_Load(object sender, EventArgs e)
{
    ClientSize = BackgroundImage.Size;
    FormBorderStyle = FormBorderStyle.FixedDialog;
}

A third approach is to change the form’s BackgroundImageLayout property to prevent the form from tiling its background image. If you set this property to None, Center, Stretch, or Zoom, then the form won’t tile the image.

The fourth and final approach that I’ll mention is to place the image in a PictureBox that is on the form.

Tearing

If you modify the example program slightly so it draws on the form’s background, the result suffers from some extremely annoying image tearing. As you move the mouse, the image flickers madly, often displaying only part of the background image and pieces of the drawing. For example, the image might sometimes look like the following.


[background image]

Fortunately you can make this problem disappear if you simply set the form’s DoubleBuffered property to true. That makes the form buffer its image until the image is complete before it displays the result.

Summary

All you really need to do to draw on a background image is to set a PictureBox control’s Image property and then draw in its Paint event handler without calling the Graphics object’s Clear method. Download the example and give it a try. For some extra practice, remove the PictureBox, display the image on the form, and attach the form’s events to the provided event handlers to see what happens.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in drawing, graphics, image processing | Tagged , , , , , , , , , , | Leave a comment

Make a Word document with one picture on each page in C#


[Word document]

This example shows how you can make a program create a Word document that contains a picture on each page.

Before you start writing code, open the Add References dialog, click on the COM tab, and add a reference to “Microsoft Word 12.0 Object Library” (or whatever version you have installed on your system).

When you click the Select Pictures button, the program executes the following code.

// Let the user select the pictures.
private void btnSelectPictures_Click(object sender, EventArgs e)
{
    if (ofdPictures.ShowDialog() == DialogResult.OK)
        lstFiles.DataSource = ofdPictures.FileNames;
}

This code displays the OpenFileDialog named ofdPictures. If you select one or more files and click Open, the program sets the DataSource property for the lstFiles ListBox equal to the dialog’s FileNames array. That makes the ListBox display the names of the selected files.

You can then review the list of files and click the Create Document button to execute the following code.

// Create the Word document.
private void btnCreateDocument_Click(object sender, EventArgs e)
{
    // Get the Word application object.
    Word._Application word_app = new Word.ApplicationClass();

    // Make Word visible (optional).
    word_app.Visible = true;

    // Create the Word document.
    object missing = Type.Missing;
    Word._Document word_doc = word_app.Documents.Add(
        ref missing, ref missing,
        ref missing, ref missing);

    // Make one page per picture.
    object collapse_end = Word.WdCollapseDirection.wdCollapseEnd;
    object page_break = Word.WdBreakType.wdPageBreak;
    for (int i = 0; i < lstFiles.Items.Count; i++)
    {
        // Get the file's name.
        string filename = (string)lstFiles.Items[i];

        // Go to the end of the document.
        range = word_doc.Paragraphs.Last.Range;

        // Add the picture to the range.
        Word.InlineShape inline_shape =
            range.InlineShapes.AddPicture(
                filename, ref missing, ref missing,
                ref missing);

        // Add a paragraph.
        Word.Range range.InsertParagraphAfter();

        // Add a caption.
        FileInfo file_info = new FileInfo(filename);
        range.InsertAfter("Picture " +
            i.ToString() + ": " + file_info.Name);

        // If this isn't the last page, insert a page break.
        if (i < lstFiles.Items.Count - 1)
        {
            range.Collapse(ref collapse_end);
            range.InsertBreak(ref page_break);
        }
    }

    // Save the document.
    object doc_filename = Path.GetFullPath(
        Path.Combine(Application.StartupPath, "..\\..")) +
        "\\pictures.docx";
    word_doc.SaveAs(ref doc_filename, ref missing, ref missing,
        ref missing, ref missing, ref missing, ref missing,
        ref missing, ref missing, ref missing, ref missing,
        ref missing, ref missing, ref missing, ref missing,
        ref missing);

    // Close.
    object save_changes = false;
    word_doc.Close(ref save_changes, ref missing, ref missing);
    word_app.Quit(ref save_changes, ref missing, ref missing);

    MessageBox.Show("Done");
}

This code creates a Word application object (the Word server) and makes it visible. It then uses the server to create a new Word document.

Next the program loops through the file names in the program’s ListBox. Inside the loop, the program uses the document’s Paragraphs.Last.Range value to get a Range object that represents the end of the document.

The code then creates a new InlineShapes object holding the picture in the range. It adds a paragraph after the picture and then adds a text caption after that.

Next, if this is not the last picture, the program collapses the range so it represents the spot after the range and adds a page break.

After it has finished creating all of the pages, the program saves the Word document.

As is usually the case with Word automation, the hardest part is figuring out what Word objects and methods to use. Download the example to experiment with the program and to see additional details.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in Office, Word | Tagged , , , , , , , , | Leave a comment

See which section is under the mouse in a sunburst chart in C#


[sunburst chart]

My earlier post Make a sunburst chart in C#, Part 4 shows how you can draw a sunburst chart. This example shows how you can tell which section the mouse is over when you move the mouse over the chart. It also shows how you can draw a wedge for a “missing” element.

Finding the Wedge Under the Mouse

To keep track of the wedges, the program uses the following Wedge class.

public class Wedge
{
    public GraphicsPath Path;
    public Color FgColor, BgColor;
    public string Text;
    public bool IsHidden;

    public Wedge(GraphicsPath path, Color fg_color,
        Color bg_color, string text, bool is_hidden)
    {
        Path = path;
        FgColor = fg_color;
        BgColor = bg_color;
        Text = text;
        IsHidden = is_hidden;
    }

    // Return true if the Wedge contains this point.
    public bool ContainsPoint(Point point)
    {
        return Path.IsVisible(point);
    }

    // Return the Wedge's text.
    public override string ToString()
    {
        return Text;
    }
}

This class stores the GraphicsPath object used to draw the wedge. It also stores the wedge’s foreground and background colors, text, and an IsHidden value that indicates whether the wedge should be drawn. I’ll say more about that in the following section, which deals with hidden edges.

The wedge-finding code really only needs the Wedge class’s GraphicsPath object. The other values are just there so the program can display something. You could add other pieces of information to identify the wedge if you like. For example, you could add a Name property to the wedges in the XML data and then make the program save that data in the Wedge objects.

The Wedge class’s ContainsPoint method returns true if a given point lies within the wedge. To do that, it simply calls the GraphicsPath object’s IsVisible method and returns the result.

The last piece of the class overrides its ToString method to return the wedge’s text. Overriding this method is useful because it allows the debugger to display a Wedge object by using its text instead of the default, which is the class name.

The program uses the following code to define two objects used to keep track of the wedges.

// The items' wedges.
private List Wedges = null;

// The Wedge that is currently under the mouse.
private Wedge WedgeUnderMouse = null;

The Wedges list holds a Wedge object for each of the wedges. As you can probably guess, the WedgeUnderMouse field holds a reference to the wedge that is currently under the mouse.

When the program starts or is resized, it draws the wedges needed to make the sunburst chart. The previous version of the program just drew the chart’s wedges on a bitmap. The new version also creates a new Wedge object to represent each wedge. For example, it uses the following statement to create a Wedge for the chart’s root.

wedges.Add(new Wedge(path, fg_color, bg_color,
    XmlDoc.DocumentElement.Name,
    IsHidden(XmlDoc.DocumentElement)));

The program uses the following code to create a Wedge object for a child wedge.

// Make the item's wedge.
wedges.Add(new Wedge(path, fg_color, bg_color, text, is_hidden));

Most of the parameters to the Wedge class’s constructor were already found by the program’s previous version. The final parameter indicates whether the wedge should be drawn. I’ll say more about how that is used and the IsHidden method in the following section

When the mouse moves over the sunburst chart’s PictureBox, the following event handler executes.

// Display information about the wedge under the mouse.
private void picSunburst_MouseMove(object sender, MouseEventArgs e)
{
    // Find the wedge under the mouse.
    foreach (Wedge wedge in Wedges)
    {
        if (wedge.ContainsPoint(e.Location))
        {
            DisplayWedgeInfo(wedge);
            return;
        }
    }

    // We didn't find a wedge containing
    // the mouse. Clear the info.
    DisplayWedgeInfo(null);
}

This method simply loops through the objects in the Wedges list calling their ContainsPoint methods. If it finds an object that contains the mouse’s location, the method calls the DisplayWedgeInfo method to display that wedge’s information and then returns. If none of the Wedge objects contains the mouse’s location, the method calls the DisplayWedgeInfo method passing it null to clear any previously displayed information.

The following code shows the DisplayWedgeInfo method.

// If this is a new Wedge under the mouse,
// display its information.
private void DisplayWedgeInfo(Wedge wedge)
{
    // If the Wedge under the mouse has
    // not changed, do nothing.
    if (wedge == WedgeUnderMouse) return;
    WedgeUnderMouse = wedge;

    // See if the FgColor is Transparent.
    if ((wedge == null) || (wedge.IsHidden))
    {
        // It's null or Transparent. Clear the label.
        lblWedgeUnderMouse.Text = "";
    }
    else
    {
        // It's not Transparent.
        // Display the Wedge's information.
        lblWedgeUnderMouse.Text = wedge.Text.Replace("\\n", " ");
    }
}

This method compares its parameter to the previously displayed Wedge stored in the WedgeUnderMouse variable. If the new object is the same as the old one, then that object’s information is already shown so the method returns.

If the new Wedge object is different from the previously displayed one, the code saves the new object in the variable WedgeUnderMouse. Next, if the wedge is null or hidden, the method clears the text in the lblWedgeUnderMouse label.

If the wedge is not null and not hidden, the code displays its text in the label. The code also replaces the \n escape sequence with a space so it can display multi-line text.

Drawing Empty Wedges

One approach you can use to draw an empty wedge is to make an entry that has the same FgColor and BgColor values. Then the entry’s text and background have the same color, so you can’t see the text.

Unfortunately the program makes descendant elements inherit the foreground and background colors of their parents. If you give an element matching foreground and background colors, then its descendants inherit those colors. To make them visible, you would need to explicitly assign them new colors. That’s not the end of the world, but it removes the advantage of inherited colors and clutters the XML file.

To avoid those problems, I modified the program to recognize a new IsHidden attribute. For example, the following code shows the part of the example program’s XML data that defines the fruit section of the sunburst chart.

<Fruit BgColor="Orange" IsHidden="true">
  <Banana />
  <Peach />
  <Frog />
</Fruit>

The first line defines the Fruit element. It gives that element an orange background color so its children inherit that color. The Fruit element has IsHidden="true", so that element is marked as hidden.

When the program is loading an element, it uses the following IsHidden method to determine whether the wedge should be hidden.

// Return true if the wedge should be hidden.
private bool IsHidden(XmlNode node)
{
    if (node.Attributes["IsHidden"] == null) return false;
    return (bool.Parse(node.Attributes["IsHidden"].Value));
}

This method gets an XML node’s IsHidden attribute. If the attribute is not present, then the method returns false to indicate that the node should not be hidden. If the attribute is present, then the method parses it ass a Boolean value and returns the result.

The last interesting new piece in the example is where it decides whether it should draw a wedge. The DrawSunburstChild method fills the wedge’s GraphicsPath and draws its text. Before it does so, it checks whether the wedge should be hidden. For example, the following code snippet fills and outlines the wedge’s GraphicsPath.

// See if this wedge should be hidden.
bool is_hidden = IsHidden(node);

bg_color = GetNodeColor(node, "BgColor", default_bg_color);
if (!is_hidden)
{
    using (Brush brush = new SolidBrush(bg_color))
    {
        gr.FillPath(brush, path);
    }
    gr.DrawPath(Pens.Black, path);
}

This code calls the IsHidden method to see if the wedge should be hidden. It then gets the wedge’s background color. If the wedge should not be hidden, the code fills the wedge’s GraphicsPath and outlines it in black.

Summary

To determine the wedge under the mouse, the program uses a list of Wedge objects. When the mouse moves, it simply loops through the objects to see if any contain the mouse’s location. This is a simple and very versatile strategy that you can use to determine what the mouse is above in many programs.

To avoid drawing a wedge, the program allows its XML elements to have a new IsHidden attribute. When it processes an element, the code now saves that value in the Wedge class’s IsHidden field. It also uses the value to decide whether it should draw the wedge. You can use a similar technique to add other attributes to the XML elements so they can hold other information.

Because the program now uses Wedge objects to store information about the wedges, you could modify it to do other things to the wedges. For example, you could redraw any individual wedge to give it new colors, perhaps when the mouse moves over it, when the user clicks on it, or when the user selects the corresponding item in the TreeView control on the program’s left side.

This example only includes a few snippets of code because most of the code is the same as in the earlier post. See that post and download this example to see all of the code, to experiment with the program, and to learn about other details.


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in algorithms, drawing, graphics | Tagged , , , , , , , , , , , | 3 Comments