Let the user draw rotated polygons with right angles in C#

[rotated polygons]

This example lets the user draw rotated polygons where all edges are either parallel or perpendicular to a defined baseline direction. As a result, all of the polygon’s angles are right angles. (And yes, I know that the polygons aren’t actually defined and then rotated. It just seemed like the best way to describe them was to call them rotated polygons.)

The idea was to let you draw rotated polygons over a layout of a collection of buildings. To make that easier, the program lets you load an image of a map showing the buildings so you can draw over them.

The following section explains how you can use the program to draw rotated polygons. The rest of this post explains how the program works.

Using the Program

The program provides the following menu commands.

  • File
    • Open – This command lets you open an image file to use as a background. That lets you define the baseline so it lines up with a direction on the image.
    • New – This clears the picture’s background image and removes all currently defined rotated polygons.
    • Exit – This does exactly what you would expect.
  • Drawing
    • Set Baseline – This lets you click and drag to define a new baseline.
    • Draw Polygon – The command lets you start drawing a new polygon. I’ll explain how that works next.
    • Clear Polygons – This removes all currently defined rotated polygons.

To create a rotated polygon, press Ctrl+P or select the Drawing menu’s Draw Polygon command. Then left click to define the polygon’s vertices. The program automatically adjusts the location of the next point so the line between it and the previous point is either parallel or perpendicular to the baseline.

[rotated polygons]

For example, consider the picture on the right. Here I have defined two of the polygon’s vertices and the mouse is at the black cross. To find the vertex location for this mouse position, we consider the two dashed lines leading from the mouse position, one parallel to the baseline and the other perpendicular to it. The dashed lines extend until they are as close as possible to the polygon’s last vertex. The goal is to place the new vertex at the end of the shorter of the two dashed line. In this example, the dashed line that is parallel to the baseline is shorter. The program has drawn the polygon’s new tentative edge in green. (But the new tentative vertex isn’t drawn in red because I haven’t clicked the mouse yet to fix that point.)

After you have defined all of the polygon’s vertices, right-click to finish the polygon. At that point the program adjusts the final point so the line between it and the polygon’s first point is parallel or perpendicular to the baseline. For that to produce a correct result, the number of polygon points that you define before right-clicking should be even and at least four. If you define fewer than four points, the program discards the points.

If you define an odd number of points, then the program cannot easily adjust the final point so the lines it makes with the previous and first points are parallel and perpendicular to the baseline. Rather than trying to figure out what the program should do in this situation, I just decided that you should just select an even number of points. If you place the final point close to where it should go, then the program adjusts it properly and all is well.

Defining the Baseline

Instead of using a complicated set of MouseDown, MouseMove, and MouseUp event handlers to handle all mouse operations, this program installs and uninstalls those event handlers as they are needed to perform different tasks.

This section describes the mouse event handlers that the program uses to let you define the baseline. It’s pretty basic mouse event handling, so if you already know how to let the user select a line segment, you may want to skim or even skip the rest of this section.

The program stores the baseline’s start and end points in the following variables.

// The baseline start and end points.
private Point BaselineStart = new Point(0, 00);
private Point BaselineEnd = new Point(200, 100);

When you select the Drawing menu’s Set Baseline command, the program executes the following code.

// Let the user draw the baseline.
private void mnuDrawingSetBaseline_Click(object sender, EventArgs e)
{
    picCanvas.MouseDown += DrawBaseline_MouseDown;
    picCanvas.Cursor = Cursors.Cross;
}

This code registers the following DrawBaseline_MouseDown method to catch MouseDown events and changes the cursor to a crosshair.

private void DrawBaseline_MouseDown(object sender, MouseEventArgs e)
{
    picCanvas.MouseDown -= DrawBaseline_MouseDown;
    picCanvas.MouseMove += DrawBaseline_MouseMove;
    picCanvas.MouseUp += DrawBaseline_MouseUp;

    BaselineStart = e.Location;
    BaselineEnd = e.Location;
    picCanvas.Refresh();
}

When you press the mouse down, the event handler uninstalls itself so it no longer catches MouseDown events. It then installs the MouseMove and MouseUp event handlers that I’ll show you next.

It also saves the mouse’s current location in the BaselineStart and BaselineEnd variables and refreshes the program’s PictureBox. (I’ll show you its Paint event handler later.)

The following code shows the baseline’s MouseMove and MouseUp event handlers.

private void DrawBaseline_MouseMove(object sender, MouseEventArgs e)
{
    BaselineEnd = e.Location;
    picCanvas.Refresh();
}

private void DrawBaseline_MouseUp(object sender, MouseEventArgs e)
{
    picCanvas.MouseMove -= DrawBaseline_MouseMove;
    picCanvas.MouseUp -= DrawBaseline_MouseUp;
    picCanvas.Cursor = Cursors.Default;
}

The MouseMove event handler saves the mouse’s current position in variable BaselineEnd. It then refreshes the PictureBox to draw the current baseline selection.

The MouseUp event handler uninstalls the MouseMove and MouseUp event handlers and then resets the mouse cursor to the default.

Defining Rotated Polygons

The baseline is defined by two points, so you can define it by a single press/drag/release operation. The polygon could include any number of points, so the program needs to use a different selection mechanism. It lets you click multiple times to define the polygon’s vertices.

The program stores information about the new polygon while you are drawing it in the following two variables.

// The new polygon while under construction.
private List<PointF> NewPolygon = null;
private PointF LastPoint;

Variable NewPolygon is a list containing the vertices that are currently defined for the new rotated polygon. Variable LastPoint indicates the position that would be added to the polygon if you were to click the mouse now. (If this were a normal polygon and not one with edges parallel and perpendicular to the baseline, then LastPoint would simply be the mouse’s position.)

When you select the Drawing menu’s Draw Polygon command, the following code executes.

// Let the user draw a polygon.
private void mnuDrawingDrawPolygon_Click(object sender, EventArgs e)
{
    NewPolygon = new List<PointF>();

    picCanvas.MouseClick += DrawPolygon_MouseClick;
    picCanvas.MouseMove += DrawPolygon_MouseMove;
    picCanvas.Cursor = Cursors.Cross;
}

This code sets variable NewPolygon to a new list of PointF objects. If the polygon’s vertices were where you clicked the mouse, then their and Y coordinates could be integers so you could store them in a list of Point instead of PointF. However, the vertices are adjusted to make the rotated polygon’s sides parallel or perpendicular to the baseline, so the vertex coordinates are not necessarily integers.

After initializing the NewPolygon list, the code installs event handlers to catch MouseClick and MouseMove events and sets the cursor to the crosshair.

When you click on a point, the following event handler executes.

private void DrawPolygon_MouseClick(object sender, MouseEventArgs e)
{
    // See if we are done with this polygon.
    if (e.Button == MouseButtons.Right)
    {
        // End this polygon.
        picCanvas.MouseClick -= DrawPolygon_MouseClick;
        picCanvas.MouseMove -= DrawPolygon_MouseMove;
        picCanvas.Cursor = Cursors.Default;

        // Is we have at least four points,
        // save the new polygon.
        if (NewPolygon.Count > 3)
        {
            SaveNewPolygon();
        }

        // Reset the new polygon.
        NewPolygon = null;
    }
    else
    {
        // Continue this polygon.
        PointF adjusted_point = AdjustPoint(e.Location);
        NewPolygon.Add(adjusted_point);
        LastPoint = adjusted_point;
    }

    picCanvas.Refresh();
}

If you clicked the right mouse button, then you are trying to end the new polygon. The code uninstalls the MouseClick and MouseMove event handlers and restores the cursor to the default. If you have defined at least four vertices, the code calls the SaveNewPolygon method (described shortly) to save the new polygon. The code finishes by setting the NewPolygon list to null.

If you did not click the right mouse button, then you are adding a new point to the polygon. In that case, the code calls the AdjustPoint method (described later) to move the point that you clicked so the line between it and the polygon’s previous point is either parallel or perpendicular to the baseline. The code adds the adjusted point to the new polygon and sets LastPoint equal to the adjusted point. (You’ll see how that is used later in the Paint event handler.)

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

private void DrawPolygon_MouseMove(object sender, MouseEventArgs e)
{
    if (NewPolygon.Count == 0) return;
    LastPoint = AdjustPoint(e.Location);
    picCanvas.Refresh();
}

If the new polygon has no points yet, this code simply returns. Otherwise the code adjusts the mouse’s current location and saves it in variable LastPoint. It then refreshes the PictureBox to show the partially completed rotated polygon.

Saving the Polygon

The program stores finished rotated polygons in the following Polygons list.

// The polygons.
private List<List<PointF>> Polygons = new List<List<PointF>>();

The following SaveNewPolygon method finishes the new polygon and adds it to that list.

// Fix the new polygon's last point so the final
// segment forms a right angle with the first segment.
private void SaveNewPolygon()
{
    NewPolygon[NewPolygon.Count - 1] =
        AdjustPoints(
            NewPolygon[NewPolygon.Count - 1],
            NewPolygon[0]);

    Polygons.Add(NewPolygon);
}

This code calls the AdjustPoints method described in the next section to adjust the new polygon’s last point so it lines up properly with the polygon’s first point. The method then adds the new polygon to the Polygons list.

Adjusting Points

By far the most interesting part of this program is the code that adjusts a point so it lines up properly with the new polygon’s existing vertices. The following AdjustPoint method adjusts a point so it lines up with the polygon’s last vertex.

// Adjust this point so it is perpendicular
// to the previous point in the new polygon.
private PointF AdjustPoint(PointF point)
{
    if (NewPolygon == null) return point;
    if (NewPolygon.Count == 0) return point;

    // Adjust the point to the last point
    // that is currently in the new polygon.
    return AdjustPoints(point, NewPolygon[NewPolygon.Count - 1]);
}

If the new polygon is null or has no vertices, then this method simply returns the original point unchanged. If neither of those conditions is true, then the code simply calls the AdjustPoints method described shortly to adjust the point and returns the result. That method decides which dashed line to follow in the earlier picture and where the adjusted point should lie.

One approach to adjusting the point would be to find the distance between the point to adjust and the reference point as multiples of vectors parallel and perpendicular to the baseline vector. That probably wouldn’t be quite as hard as it sounds, but there’s an easier way. (Or at least a way that’s easier to understand.)

The code first rotates the points so the baseline is parallel to the X axis. Then finding the correct adjusted point is simply a matter of determining whether the two points differ less in their X or Y coordinates.

For example, take a look at the picture below.


[rotated polygons]

The picture on the left shows the original rotated polygon in progress. The picture on the right has been rotated so the baseline is parallel to the X axis. In that picture it’s easy to see that the dashed line that we want to follow is the horizontal one because it is shorter than the vertical one.

The length of the vertical dashed segment is the difference between the two rotated points’ Y coordinates. Similarly the length of the horizontal dashed segment is the difference between the two rotated points’ X coordinates. To see which dashed segment is shorter, we simply calculate those lengths and compare them.

Finding the location of the adjusted point is also easy in the rotated picture. For example, to use the horizontal dashed segment (which we should in this example), the adjusted point has the X coordinate of the polygon’s current last vertex and the Y coordinate of the mouse’s position.

That gives us the following algorithm for adjusting a point.

  1. Find a transformation that rotates the baseline so it is perpendicular to the X axis.
  2. Rotate the point to adjust and the reference point.
  3. Subtract X and Y coordinates to see which dashed segment we should follow.
  4. Use the coordinates of the point to adjust and the reference point to find the adjusted point’s rotated location.
  5. Reverse the earlier transformation to move the adjusted point where it belongs in the original drawing.

The following AdjustPoints method follows those steps.

// Adjust a point so it is perpendicular
// to a reference point.
private PointF AdjustPoints(PointF point_to_adjust, PointF reference_point)
{
    if (NewPolygon == null) return point_to_adjust;
    if (NewPolygon.Count == 0) return point_to_adjust;

    // Transform the last point in the new polygon
    // and this point.
    Matrix matrix = GetTransform();
    PointF[] points =
    {
        reference_point,
        point_to_adjust,
    };
    matrix.TransformPoints(points);

    // Fix the transformed point.
    float dx = Math.Abs(points[1].X - points[0].X);
    float dy = Math.Abs(points[1].Y - points[0].Y);
    if (dx <= dy)
        points[1].X = points[0].X;
    else
        points[1].Y = points[0].Y;

    // Untransform the result.
    matrix.Invert();
    matrix.TransformPoints(points);

    return points[1];
}

If the new polygon is null or has no vertices, then this method simply returns the original point unchanged. If neither of those conditions is true, then the program calls the GetTransform method described shortly to get a transformation matrix that rotates the baseline so it is parallel to the X axis. The code makes an array holding the point to adjust and the reference point, and uses the matrix to rotate them.

Next the code determines whether the rotated points are closer in the X or Y directions and adjusts the point to adjust accordingly. The method then inverts the rotation matrix (so it reverses the rotation) and applies the inverted matrix to the points. The method finishes by returning the unrotated adjusted point.

The last piece of code that deals with adjusting points is the following GetTransform method.

// Return a transformation matrix that rotates
// the baseline so it is parallel to the X axis.
private Matrix GetTransform()
{
    float dx = BaselineStart.X - BaselineEnd.X;
    float dy = BaselineStart.Y - BaselineEnd.Y;
    double angle = -Math.Atan2(dy, dx) * 180 / Math.PI;
    Matrix matrix = new Matrix();
    matrix.Rotate((float)angle);
    return matrix;
}

This method is actually fairly straightforward. It calculates the difference in X and Y coordinates between the baseline’s two end points. It then uses the Math.Atan2 method to calculate the angle that the baseline makes with respect to the X axis.

Next the code creates a new Matrix object and calls its Rotate method, passing that method the negative of the baseline angle. The method then returns the resulting matrix.

Drawing

The program’s Paint event handler, which is shown in the following code, is somewhat involved but not very complicated.

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

    using (Pen pen = new Pen(Color.Red, 2))
    {
        // Draw the baseline.
        e.Graphics.DrawLine(pen, BaselineStart, BaselineEnd);

        pen.Color = Color.Yellow;
        pen.DashPattern = new float[] { 3, 3 };
        e.Graphics.DrawLine(pen, BaselineStart, BaselineEnd);

        // Draw the defined polygons.
        pen.Color = Color.Blue;
        pen.DashStyle = DashStyle.Solid;
        using (Brush brush = new SolidBrush(Color.FromArgb(128, Color.LightBlue)))
        {
            foreach (List<PointF> points in Polygons)
            {
                e.Graphics.FillPolygon(brush, points.ToArray());
                e.Graphics.DrawPolygon(pen, points.ToArray());
            }
        }

        // Draw the new polygon if there is one.
        if (NewPolygon != null)
        {
            pen.Color = Color.Green;
            pen.DashStyle = DashStyle.Solid;
            if (NewPolygon.Count > 1)
                e.Graphics.DrawLines(pen, NewPolygon.ToArray());
            e.Graphics.DrawLine(pen,
                NewPolygon[NewPolygon.Count - 1],
                LastPoint);
            foreach (PointF point in NewPolygon)
                e.Graphics.FillEllipse(Brushes.Red,
                    point.X - 3, point.Y - 3, 6, 6);
        }
    }
}

This code sets the e.Graphics object to draw anti-aliased shapes and then creates a thick, red pen. It uses the pen to draw the baseline, changes the pen so it is a dashed yellow pen, and draws the baseline again. The result is a thick baseline that alternates dashes of red and yellow.

Next the code makes the pen solid blue. It then loops through any polygons that are stored in the Polygons list. Each of those entries is itself a list of points. The code calls the points list’s ToArray method to convert the points into an array and uses the result to fill and outline the polygon.

After it has drawn any existing polygons, the code draws the new polygon if one is under construction. To do that, the code makes the pen green. If the new polygon contains more than one vertex, the code uses the Graphics object’s DrawLines method to draw the edges that connect those vertices. It then draws a line from the last vertex to the point stored in LastPoint. (Recall that LastPoint holds the adjusted mouse position. That is where the polygon’s next vertex will go if you click the mouse now.) The method finishes by looping through the vertices again, this time drawing a red circle at each.

Conclusion

This is a fairly specialized application, but it does demonstrate a few reusable techniques. It shows how you can install and uninstall mouse event handlers to perform different operations.

The example also shows how you can let the user draw rotated polygons. You may never need to do that, but it might be useful to let the user draw polygons that have edges parallel to the X and Y axes. You can do that by using a horizontal baseline. Or you can simplify the program by removing the rotation and unrotation transformations.

Download the example to see additional details including the code that can optionally draw the dashed lines shown in some of the pictures above.


Download Example   Follow me on Twitter   RSS feed   Donate




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

Let the user save an image of a smooth curve in C#

[smooth curve]

The post Let the user draw a smooth curve in C# shows how you can let the user draw a smooth curve. This example extends that one to let you save the curve in an image file.

There are a few approaches that you might take. The following sections describe two approaches.

Saving the PictureBox Image

The post Get the image of a control or form, or a form’s client area in C# shows how you can capture a control’s image in a bitmap. The following code shows the GetControlImage method.

private Bitmap GetControlImage(Control ctl)
{
    Bitmap bm = new Bitmap(ctl.Width, ctl.Height);
    ctl.DrawToBitmap(bm,
        new Rectangle(0, 0, ctl.Width, ctl.Height));
    return bm;
}

This method creates a Bitmap with the same size as the control. It then calls the control’s DrawToBitmap method to draw the control’s image onto the Bitmap. It finishes by returning the Bitmap.

When you select the File menu’s Save Image command, the following code uses the GetControlImage method to save an image of the example’s PictureBox.

// Save an image of the PictureBox.
private void mnuFileSaveImage_Click(object sender, EventArgs e)
{
    if (sfdPicture.ShowDialog() == DialogResult.OK)
    {
        // Get an image of the PictureBox.
        using (Bitmap bm = GetControlImage(picCanvas))
        {
            // Save the image.
            SaveImage(bm, sfdPicture.FileName);
        }
    }
}

This code displays a SaveFileDialog. If you select a file and click Save, the program calls the GetControlImage method to get an image of the program’s PictureBox. It then uses the SaveImage method described in the post Save images with an appropriate format depending on the file name’s extension in C# to save result into an image file.

[smooth curve]

The picture on the right shows the result. Notice that the picture includes the PictureBox control’s borders. The easiest way to fix that is to make the control not display a border. Another approach is to find the client area inside the control’s image and copy it out of the bitmap. To use that approach, look at the way the earlier post gets an image of the form’s client area.

Saving a Drawn Image

A better method than the previous one is to draw the curve on a new bitmap. This is a bit more work than the previous method, but the result is easier to control.

To use this technique, first move all of the drawing code into a separate method that takes a Graphics object as a parameter. Then make the Paint event handler call that method.

The following code shows how this example does this.

// Draw the curve and its points.
private void picCanvas_Paint(object sender, PaintEventArgs e)
{
    DrawTheCurve(e.Graphics, true);
}

// Draw the curve.
private void DrawTheCurve(Graphics gr, bool draw_points)
{
    gr.SmoothingMode = SmoothingMode.AntiAlias;

    // Draw the curve.
    if (Points.Count > 1)
    {
        // Make a pen to use.
        using (Pen pen = new Pen(Color.Blue))
        {
            // See if we're currently drawing.
            if (Drawing)
            {
                // Use a dashed pen.
                pen.DashPattern = new float[] { 5, 5 };
            }

            // Draw the curve.
            gr.DrawCurve(pen, Points.ToArray(), Tension);
        }
    }

    // Draw the points.
    if (draw_points && Drawing && (Points.Count > 0))
    {
        const int r = 4;
        foreach (Point point in Points)
        {
            Rectangle rect = new Rectangle(
                point.X - r, point.Y - r, 2 * r, 2 * r);
            gr.FillRectangle(Brushes.White, rect);
            gr.DrawRectangle(Pens.Black, rect);
        }
    }
}

The Paint event handler simply calls the new DrawTheCurve method passing it the e.Graphics parameter and the value true to indicate that DrawTheCurve should draw boxes at the curve’s control points.

The DrawTheCurve method draws the curve more or less as the earlier example did. The only differences are that it takes its Graphics object as a parameter and that it only draws the control points if its draw_points parameter is true.

Now that you have the code that draws the curve in a separate method, you can use that method for other things. For example you could use it to draw the curve on the printer. I’ll leave that for you to do if you like. You may want to modify the drawing code to use different colors, thicker lines, of a different scale for the printer.

When you select the File menu’s Save Drawing command, the following code uses the DrawTheCurve method to save a drawing of the curve in an image file.

// Draw the curve onto a bitmap and save it.
private void mnuFileSaveDrawing_Click(object sender, EventArgs e)
{
    if (sfdPicture.ShowDialog() == DialogResult.OK)
    {
        // Make a Bitmap.
        int wid = picCanvas.ClientSize.Width;
        int hgt = picCanvas.ClientSize.Height;
        using (Bitmap bm = new Bitmap(wid, hgt))
        {
            // Draw the curve on the bitmap.
            using (Graphics gr = Graphics.FromImage(bm))
            {
                DrawTheCurve(gr, false);
            }

            // Save the image.
            SaveImage(bm, sfdPicture.FileName);
        }
    }
}

This code displays a SaveFileDialog. If you select a file and click Save, the code then creates a Bitmap with the same dimensions as the PictueBox control’s client area. It makes an associated Graphics object and passes it into the DrawTheCurve method. The code finishes by calling the SaveImage method to save the result.

Conclusion

This example shows how to let the user save a smooth curve into a file. More importantly it shows how you can place drawing code in a separate method so you can reuse it in different places. That lets you easily display your smooth curve on the screen, save it into an image file, or send the drawing to the printer.


Download Example   Follow me on Twitter   RSS feed   Donate




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

Use a sprite class to animate bouncing cats in C#

[sprites]

A sprite is an object that you use to control a single object in an animation or game. Normally a sprite class must do two things: draw its object and move its object.

This example defines an ImageSprite class that draws spinning, bouncing cat heads. The following sections describe the ImageSprite class and the main program.

ImageSprite

The following code shows the class’s beginning.

class ImageSprite
{
    public float Angle, DAngle;
    public PointF Center, Velocity;
    public Bitmap Picture;
    private float Radius;

    public ImageSprite(float angle, float dangle,
        PointF center, PointF velocity, Bitmap picture)
    {
        Angle = angle;
        DAngle = dangle;
        Center = center;
        Velocity = velocity;
        Picture = picture;

        Radius = Math.Min(picture.Width, picture.Height) / 2f;
    }

The class begins with fields that define the position and orientation of the sprite’s image. The Angle value holds the angle at which the image is currently rotated. DAngle indicates the amount by which the Angle is modified whenever the sprite moves its image.

The Center value stores the image’s location. The Velocity value’s X and Y properties indicate how much the Center is updated in the X and Y directions.

Note that the DAngle and Velocity values are measured in change per second.

The Picture field holds the picture that the sprite should draw. All of this example’s sprites hold the same cat image, but you could give different sprites different images if you like. Finally, the Radius field indicates the radius that the image should have.

The class’s constructor simply initializes the sprite’s fields.

The following code shows how the class updates the sprite’s position and orientation.

public void Move(Rectangle bounds, float elapsed)
{
    Center.X += Velocity.X * elapsed;
    float right = Center.X + Radius;
    if (right > bounds.Right)
    {
        right = bounds.Right - (right - bounds.Right);
        Center.X = right - Radius;
        Velocity.X = -Velocity.X;
    }
    float left = Center.X - Radius;
    if (left < 0)
    {
        left = -left;
        Center.X = left + Radius;
        Velocity.X = -Velocity.X;
    }

    Center.Y += Velocity.Y * elapsed;
    float bottom = Center.Y + Radius;
    if (bottom > bounds.Bottom)
    {
        bottom = bounds.Bottom - (bottom - bounds.Bottom);
        Center.Y = bottom - Radius;
        Velocity.Y = -Velocity.Y;
    }
    float top = Center.Y - Radius;
    if (top < 0)
    {
        top = -top;
        Center.Y = top + Radius;
        Velocity.Y = -Velocity.Y;
    }

    Angle += DAngle * elapsed;
}

To update the Center field’s X value, the method adds Velocity.X times the amount of time that has elapsed since the last time the Move method was called. Multiplying the change per second by elapsed seconds allows the program to keep roughly the same amount of movement no matter how quickly or slowly the program calls the Move method. That’s helpful because the program’s timer doesn’t necessarily always fire with the same frequency. It may fire more or less quickly depending on the other programs running on the system and the number of sprites running in this program.

Next, the method checks the sprite’s X coordinate to see if it is moving off of the left or right edges of the bounds passed into the method (which represent the edges of the program’s form). If the X coordinate is moving out of bounds, the code updates the X coordinate and reverses the Velocity.X value so the sprite starts moving in the other direction.

The method then performs similar steps to update the sprite’s Y position.

The method finishes by adding DAngle times the elapsed time to the Angle value to rotate the sprite.

The final part of the sprite class is the following Draw method.

public void Draw(Graphics gr)
{
    GraphicsState state = gr.Save();
    gr.ResetTransform();
    gr.RotateTransform(Angle);
    gr.TranslateTransform(Center.X, Center.Y, MatrixOrder.Append);
    gr.DrawImage(Picture, new PointF(-Radius, -Radius));
    gr.Restore(state);
}

This method draws the sprite. It first saves the Graphics object’s state so it won’t interfere with any other graphics method that may run later. In this case, saving and restoring the state prevents the sprite from interfering with the other sprites.

The method then calls the Graphics object’s ResetTransform method to remove any current transformations. It adds a rotation by the sprite’s Angle and translates the sprite to place it at its Center. The method draws the sprite’s image and the restores the Graphics object’s state.

Main Program

The following code shows how the program initializes its sprites.

private List<ImageSprite> Sprites =
    new List<ImageSprite>();

private void Form1_Load(object sender, EventArgs e)
{
    Random rand = new Random();

    for (int i = 0; i < 10; i++)
    {
        float angle = rand.Next(-360, 360);
        float dangle = rand.Next(-180, 180);
        float scale = rand.Next(2, 5) / 20f;
        Bitmap bm = ResizeImage(Properties.Resources.cat, scale);
        float rx = bm.Width / 2f;
        float ry = bm.Height / 2f;
        PointF center = new PointF(
            rand.Next((int)rx, picCanvas.ClientSize.Width - (int)rx),
            rand.Next((int)ry, picCanvas.ClientSize.Height - (int)ry));
        float vx = rand.Next(10, 50);
        if (rand.Next(0, 2) == 1) vx = -vx;
        float vy = rand.Next(10, 50);
        if (rand.Next(0, 2) == 1) vy = -vy;
        PointF velocity = new PointF(vx, vy);

        Sprites.Add(new ImageSprite(angle, dangle, center, velocity, bm));
    }

    LastTime = DateTime.Now;
    tmrFrame.Enabled = true;
}

This code starts by defining a list of ImageSprite objects. The form’s Load event handler uses a loop to create 10 sprites. Most of the loop uses random numbers to initialize various sprite properties.

Possibly the most interesting piece of code here is the call to ReizeImage. That method simply takes an image as a parameter, resizes it, and returns the resized result. I’ll describe that method shortly.

After it generates the sprite’s parameters, the Load event handler uses them to create the sprite and adds the new sprite to the Sprites list.

The following code shows the ResizeImage method.

private Bitmap ResizeImage(Bitmap bm, float scale)
{
    int width = (int)(bm.Width * scale);
    int height = (int)(bm.Height * scale);
    Bitmap result_bm = new Bitmap(width, height);
    using (Graphics gr = Graphics.FromImage(result_bm))
    {
        PointF[] dest_points =
        {
            new PointF(0, 0),
            new PointF(width, 0),
            new PointF(0, height),
        };
        RectangleF src_rect = new RectangleF(
            0, 0,
            Properties.Resources.cat.Width,
            Properties.Resources.cat.Height);
        gr.DrawImage(Properties.Resources.cat,
            dest_points, src_rect, GraphicsUnit.Pixel);
    }
    return result_bm;
}

This method calculates the scaled image’s width and height, and makes a bitmap of that size. It creates an associated Graphics object, makes a dest_points array indicating where the image should be drawn on the new bitmap, and makes a src_rect indicating which part of the image to draw. The method calls the Graphics object’s DrawImage method to draw the original method onto the new bitmap and returns the result.

When the main form’s timer fires, the following code executes.

private DateTime LastTime;

private void tmrFrame_Tick(object sender, EventArgs e)
{
    DateTime now = DateTime.Now;
    float elapsed = (float)(now - LastTime).TotalSeconds;

    foreach (ImageSprite sprite in Sprites)
    {
        sprite.Move(picCanvas.Bounds, elapsed);
    }
    LastTime = now;
    picCanvas.Refresh();
}

The LastTime value records the last time that the timer fired. This form-level variable is initialized to the time when the program starts.

The timer’s event handler gets the current time and subtracts LastTime from it to see how much time has passed since the last Tick event. The code then loops through the sprite’s calling their Move methods and passing the method the number of seconds that have elapsed.

After it finishes moving the sprites, the code refreshes the picCanvas control to make the following Paint event handler execute.

private void picCanvas_Paint(object sender, PaintEventArgs e)
{
    e.Graphics.InterpolationMode = InterpolationMode.High;
    foreach (ImageSprite sprite in Sprites)
    {
        sprite.Draw(e.Graphics);
    }
}

This event handler simply loops through the sprites calling their Draw methods.

Conclusion

Yes, I know you’re unlikely to need to draw a bunch of bouncing, spinning cats. However, you can use the same techniques to draw all sorts of animated objects. You can also use different sprite classes to manage different kinds of objects. For example, you could build an asteroids game by using separate classes to manage the ship, bullets, and asteroids. (Although note that the asteroids in that game wrap around the screen rather than bouncing off of its edges.)


Download Example   Follow me on Twitter   RSS feed   Donate




Posted in animation, classes, graphics | Tagged , , , , , , , | Leave a comment

Draw interlocked circles in C#

[interlocked circles]

This example draws a set of interlocked circles so they alternate between above and below each other. As you can see from the picture, the circles are made up of colored lines with black outlines so it’s easy to see when one passes over another. The problem is that there’s no order in which you can draw the circles so they appear properly woven together.

The program works in two stages. First it finds the points where interlocked circles intersect. It then draws the circles and their intersections. Much of the code that performs those operations is in the Circle class, which I’ll describe shortly. First I’ll describe the Poi class that the program uses to store information about points of intersection.

Points of Intersection

The following Poi class stores information about the point of intersection (POI) between two interlocked circles. If two circles intersect, then they share a common Poi object that represents that point of intersection.

class Poi
{
    public PointF Location;
    public Circle[] Circles = null;
    public Circle CircleOnTop = null;

    public Poi(PointF location, Circle circle1, Circle circle2)
    {
        Location = location;
        Circles = new Circle[] { circle1, circle2 };
    }

    public override string ToString()
    {
        return string.Format("({0}/{1}", Circles[0], Circles[1]);
    }

    // Return the other circle.
    public Circle OtherCircle(Circle this_circle)
    {
        if (Circles[0] != this_circle) return Circles[0];
        return Circles[1];
    }

    // Return the circle on the bottom, if the top circle is assigned.
    public Circle CircleOnBottom()
    {
        if (CircleOnTop == null) return null;
        return OtherCircle(CircleOnTop);
    }
}

The Location field indicates where the intersection occurs, the Circles array stores references to the two circles, and CircleOnTop indicates the circle that is on top at the point of intersection.

The class’s constructor simply saves the two circles. The ToString method just returns the names of the circles.

The OtherCircle method simply returns the Poi object’s circle that is not passed in as a parameter. The CircleOnBottom method uses that method to return the circle that is on the bottom. It first checks whether CircleOnTop is defined and, if it is, it returns the other circle.

The Poi class holds information about an intersection, but it doesn’t do much. The Circle class described in the following sections performs the most interesting work.

Finding Points of Intersection

The following code snippet shows how the beginning of the Circle class.

class Circle
{
    public PointF Center;
    public float Radius, CircleThickness, OutlineThickness;
    public Color FillColor, OutlineColor;
    public List<Poi> Pois = new List<Poi>();
    public List<float> Angles = new List<float>();
    public int NumAssigned = 0;
    ...
}

The Center and Radius values define the circle’s position and size. The CircleThickness and OutlineThickness values indicate how thick the circle’s edge and outlines should be. The FillColor and OutlineColor values hold the circle’s colors.

The other values are a bit more interesting. The Pois list holds information about places where other circles intersect this one. The Angles list holds the angles from the center of the circle to the corresponding points of intersection. The last value, NumAssigned, indicates the number of Poi objects that this circle has that have not yet had their CircleOnTop values assigned.

The class’s AllPoisAreAssigned method shown in the following code uses the NumAssigned value.

// Return true if all of the POIs have been assigned.
public bool AllPoisAreAssigned()
{
    return NumAssigned == Pois.Count;
}

This method simply returns true if NumAssigned equals the number of Poi objects meaning all of those objects have been assigned.

Finding POIs

After the program defines its interlocked circles, it calls the Circle class’s static FindPois method shown in the following code.

// Find the circles' POIs in sorted order.
public static void FindPois(Circle[] circles)
{
    // Find the POIs.
    for (int i = 0; i < circles.Length; i++)
    {
        for (int j = i + 1; j < circles.Length; j++)
        {
            PointF p1, p2;
            FindCircleCircleIntersections(
                circles[i].Center.X, circles[i].Center.Y, circles[i].Radius,
                circles[j].Center.X, circles[j].Center.Y, circles[j].Radius,
                out p1, out p2);
            if (!float.IsNaN(p1.X))
            {
                Poi poi = new Poi(p1, circles[i], circles[j]);
                circles[i].Pois.Add(poi);
                circles[j].Pois.Add(poi);
            }
            if (!float.IsNaN(p2.X))
            {
                Poi poi = new Poi(p2, circles[i], circles[j]);
                circles[i].Pois.Add(poi);
                circles[j].Pois.Add(poi);
            }
        }
    }

    // Sort the POIs.
    foreach (Circle circle in circles)
    {
        circle.SortPois();
    }

    // Initially none of the circles has its POIs assigned.
    List<Circle> unfinished = new List<Circle>(circles);

    // Repeat until all circles are completely assigned.
    while (unfinished.Count > 0)
    {
        // At this point, all unfinished circles have no assignments.

        // Make a list to hold circles that are partially assigned.
        List<Circle> partially_assigned = new List<Circle>();

        // Add the first unfinished circle to the
        // partially_assigned list.
        // Arbitrarily make it on top in its first POI.
        Circle circle = unfinished[0];
        unfinished.RemoveAt(0);
        partially_assigned.Add(circle);
        if (circle.Pois.Count > 0)
            circle.Pois[0].CircleOnTop = circle;

        // Process circles in the partially_assigned
        // list until it is empty.
        while (partially_assigned.Count > 0)
        {
            // Remove the first circle from the list.
            circle = partially_assigned[0];
            partially_assigned.RemoveAt(0);

            // Assign the remaining entries for this circle.
            circle.Assign(unfinished, partially_assigned);
        }
        // When we reach this point, partially_assigned
        // is empty and the most recent connected
        // component has been assigned.
    }
    // When we reach this point, unfinished is
    // empty and all Circles have been assigned.
}

The method first uses two nested loops to loop over every pair of interlocked circles. The outer loop makes i loop from 0 to the index of the last circle. The inner loop makes j loop from i + 1 to the index of the last circle. That makes the loops consider each pair of circles only once. For example, the program first compares circle 0 to circle 1. Because the inner loop makes j start at i + 1, the code does not later compare circle 1 to circle 0.

For each unique pair of circles, the program calls the FindCircleCircleIntersections method to see where the two circles intersect. For information on that method, see my post Determine where two circles intersect in C#.

The program creates Poi objects to store information about any intersections between the two circles.

Next, the code loops through the circles and calls their SortPois methods to sort their POIs by their angles with respect to the circles’ centers. I’ll show that method shortly.

The method then creates a list of Circle objects named unfinished to hold Circle objects that have not yet been processed. None of the Poi objects of these circles have their CircleOnTop values assigned. The code passes circles array into the list’s constructor so the list initially holds all of the circles.

The program then enters a loop that lasts as long as the unfinished list is not empty. Within the loop some Circle objects may may have both assigned and unassigned Poi objects. The code creates a list named partially_assigned to hold those Circle objects that are partially assigned. Initially that list is empty.

The code removes the first Circle from the unfinished list and adds it to the partially_assigned list. If the Circle has any Poi objects, the code also arbitrarily sets that Circle to be on top in its first Poi. Note that this determines whether the Circle is on the top or bottom for all of its other points of intersection because they alternate: on-top/on-bottom.

Now the program enters another loop that executes as long as the partially_assigned list is not empty. Within the loop, the code removes the first Circle from the partially_assigned list and calls that object’s Assign method. I’ll describe that method shortly, but for now I’ll just tell you what it does. That method assigns the CircleOnTop value for all of the Circle‘s Pois. When it makes an assignment, the method also adds the other circle that shares the Poi to the partially_assigned list and removes it from the unfinished list.

After the FindPois method finishes processing an entry in the partially_assigned list, it repeats the process for the next item in the list.

Once the partially_assigned list is empty, the method processes the next entry in the unfinished list. When the unfinished list is empty, all of the interlocked circles have been completely assigned so the method is done.

SortPois

The Circle class’s SortPois method shown in the following code sorts a Circle object’s Pois list.

// Sort this circle's POIs.
private void SortPois()
{
    // Calculate the POIs' angles.
    Angles = new List<float>();
    foreach (Poi poi in Pois)
    {
        float dx = poi.Location.X - Center.X;
        float dy = poi.Location.Y - Center.Y;
        double radians = Math.Atan2(dy, dx);
        Angles.Add((float)(radians / Math.PI * 180));
    }

    // Sort the POIs by angle.
    Poi[] poi_array = Pois.ToArray();
    float[] angle_array = Angles.ToArray();
    Array.Sort(angle_array, poi_array);

    // Save the POIs and angles.
    Pois = new List<Poi>(poi_array);
    Angles = new List<float>(angle_array);
}

This method first sets the Circle object’s Angles value to a new list of float. It then loops through the Pois list and sets the corresponding Angles value for each Poi.

Next the code converts the Pois and Angles lists into arrays and calls the Array class’s Sort method. It passes that method the two arrays so it sorts poi_array while using angle_array as the sort keys. The method finishes by using the sorted arrays to reinitialize the Circles and Angles lists so they are in sorted order.

Assign

The Circle class’s Assign method assigns the CircleOnTop value for a Circle object’s Poi objects. This method should only be called if at least one Poi has already been assigned. The following code shows the method.

// Assign POIs for this circle alternating
// top/bottom with index first_on_top on top.
// Add other circles that share this one's POIs
// to the partially_assigned list and remove
// them from the unfinished list.
private void Assign(List<Circle> unfinished,
    List<Circle> partially_assigned)
{
    // If this circle has no Pois or if all of its Pois
    // are assigned, remove it from the list and return.
    if ((Pois.Count == 0) || AllPoisAreAssigned())
    {
        partially_assigned.Remove(this);
        return;
    }

    // Find the first POI that is assigned.
    int first_assigned = -1;
    for (int i = 0; i < Pois.Count; i++)
    {
        if (Pois[i].CircleOnTop != null)
        {
            first_assigned = i;
            break;
        }
    }

    // See whether the first assigned Poi is this Circle.
    bool last_was_this = (Pois[first_assigned].CircleOnTop == this);

    // Assign the other Pois.
    for (int i = 1; i < Pois.Count; i++)
    {
        int index = (first_assigned + i) % Pois.Count;
        if (Pois[index].CircleOnTop == null)
        {
            // Find the other Circle at this Poi.
            Circle other = Pois[index].OtherCircle(this);

            // Place the correct Circle on top.
            if (last_was_this)
                Pois[index].CircleOnTop = other;
            else
                Pois[index].CircleOnTop = this;

            // If the other circle is not completely assigned and
            // is not aleady on the partially_assigned list, add it.
            if (!other.AllPoisAreAssigned() &&
                !partially_assigned.Contains(other))
                partially_assigned.Add(other);

            // Remove the other Circle from the unfinished list.
            if (unfinished.Contains(other))
                unfinished.Remove(other);

            NumAssigned++;
        }

        // Remember whether this Poi has this Circle on top.
        last_was_this = !last_was_this;
    }
}

If the Circle object has no Poi objects or if all of the Poi objects have been assigned, then the method simply removes the Circle from the partially_­assigned list and returns.

If the method does not immediately return, it loops through the Pois list and finds the first object that has CircleOnTop assigned. It sets variable first_assigned equal to the index of that object.

The method then sets variable last_was_this to true if the first assignment had the current Circle on top. The code enters a loop where value i runs from 1 to the number of Poi objects in the Pois list. It sets index = (first_assigned + i) % Pois.Count. As a result, the value index loops through the list’s indices starting at first_assigned + 1 and ending at first_assigned - 1.

For the new index value, the code checks to see if the corresponding Poi object has a non-null CircleOnTop value. If CircleOnTop is null, the code gets the other Circle at this Poi. (I’ll show the OtherCircle method shortly.)

If the last Poi has the current Circle on top, then the code makes the current Poi place the other Circle on top. If the last Poi has its other Circle on top, then the code places the current Circle on top of the current Poi.

This step assigns the CircleOnTop value for the Poi, which is shared by the other Circle. That means the other Circle has now been partially assigned. If that other Circle has not yet been completely assigned and if it is not already in the partially_assigned list, the code adds it there. If the other Circle is in the unfinished list, the code also removes it from that list.

After it has finished assigning this Poi, the method increments the Circle object’s NumAssigned value. It then toggles the last_was_this value so alternate Poi objects have the current Circle on top.

OtherCircle

The Poi class’s OtherCircle method takes as an input parameter a Circle and then returns the Poi object’s other circle. The following code shows how the method works.

// Return the other circle.
public Circle OtherCircle(Circle this_circle)
{
    if (Circles[0] != this_circle) return Circles[0];
    return Circles[1];
}

This method simply compares the input Circle to the Poi object’s first Circle. If the two do not match, the method returns the first Circle object. If the two do match, the method returns the second Circle object.

Drawing Circles

The following code shows the main form’s Paint event handler.

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

    // Draw the circles.
    foreach (Circle circle in Circles) circle.Draw(e.Graphics);

    // Draw the on top POIs.
    foreach (Circle circle in Circles) circle.DrawPois(e.Graphics);
}

[interlocked circles]

This method sets the Graphics object’s SmoothingMode property. It then loops through the Circle objects and calls their Draw methods (shown shortly) to draw the circles. At this point, the circles drawn last are drawn on top so the circles are not interlocked. The picture on the right shows what they might look like at this point.

Next the code loops through the Circle objects again, this time calling their DrawPois methods (also shown shortly) to draw the POIs so those on top look like they are on top.

The following sections show the Circle class’s Draw and DrawPois methods.

Draw

The following code shows the Circle class’s Draw method.

// Draw the circle.
public void Draw(Graphics gr)
{
    using (Pen fill_pen = new Pen(FillColor, CircleThickness))
    {
        using (Pen outline_pen = new Pen(OutlineColor, OutlineThickness))
        {
            gr.DrawThickArc(Center, Radius, 0, 360, fill_pen, outline_pen);
        }
    }
}

This method creates two pens. The fill_pen is used to fill the interior of the circle’s edge. The outline_pen is used to draw the outline along the sides of the circle’s edges.

After creating the pens, the method calls the DrawThickArc method to draw the circle.

The DrawThickArc method is an extension method added to the Graphics class that draws an arc of a circle. The following code shows that method.

// Draw a thick arc with different inside and outside pens.
public static void DrawThickArc(this Graphics gr, PointF center, float radius,
    float start_angle, float sweep_angle,
    Pen fill_pen, Pen outline_pen)
{
    // Draw the main arc.
    gr.DrawArc(fill_pen,
        center.X - radius,
        center.Y - radius,
        2 * radius, 2 * radius,
        start_angle, sweep_angle);

    // Draw the outer outline.
    float r1 = radius + fill_pen.Width / 2f + outline_pen.Width / 2f;
    gr.DrawArc(outline_pen,
        center.X - r1,
        center.Y - r1,
        2 * r1, 2 * r1,
        start_angle, sweep_angle);

    // Draw the inner outline.
    float r2 = radius - fill_pen.Width / 2f - outline_pen.Width / 2f;
    gr.DrawArc(outline_pen,
        center.X - r2,
        center.Y - r2,
        2 * r2, 2 * r2,
        start_angle, sweep_angle);
}

This method first draws the body of the arc with the fill_pen.

The code then calculates a new radius for the arc’s outer edge. The new radius is the original arc radius plus half of the fill_pen thickness plus half of the outline_pen thickness. The method then uses the new radius and the outline_pen to draw the outer edge.

The code draws the inner edge similarly. It calculates a new radius equal to the original radius minus half of the fill and outline pen thicknesses and then draws the inner edge.

DrawPois

The DrawPois method shown in the following code draws a Circle object’s POIs where the Circle should be in top.

// Draw the POIs where this circle is on top.
public void DrawPois(Graphics gr)
{
    using (Pen fill_pen = new Pen(FillColor, CircleThickness))
    {
        using (Pen outline_pen = new Pen(OutlineColor, OutlineThickness))
        {
            for (int i = 0; i < Pois.Count; i++)
            {
                if (Pois[i].CircleOnTop == this)
                {
                    const float sweep_angle = 30;
                    float start_angle = Angles[i] - sweep_angle / 2f;
                    gr.DrawThickArc(Center, Radius,
                        start_angle, sweep_angle, fill_pen, outline_pen);
                }
            }
        }
    }
}

This method first creates the Circle object’s fill and outline pens. It then loops through the object’s Pois list. If a Poi has this circle in its CircleOnTop value, then the method calls the DrawThickArc method to draw the piece of the circle where the POI lies.

Note that this won’t work if the two interlocked circles at this POI meet at a very shallow angle. In that case, the 30 degree sweep angle used by the code will not be large enough to cover the entire area where the two circles overlap. You can make the sweep angle larger, but then the method may draw over part of a different POI if this Circle has multiple POIs that are close together. The value 30 degrees works well for the examples that I tested.

The Main Program

When the program starts, the form’s Load event handler uses the following code to define the circles shown in the picture at the top of this post.

// See where the circles intersect.
private void Form1_Load(object sender, EventArgs e)
{
    // Make some circles.
    // Four all interlocked.
    Circles = new Circle[]
    {
        new Circle(new PointF(100, 100), 50, 10, 2, Color.Orange, Color.Black),
        new Circle(new PointF(150, 100), 50, 10, 2, Color.Green, Color.Black),
        new Circle(new PointF(100, 150), 50, 10, 2, Color.Red, Color.Black),
        new Circle(new PointF(150, 150), 50, 10, 2, Color.Blue, Color.Black),
    };

    // Find the circles' POIs.
    Circle.FindPois(Circles);
}

This code simply creates an array of Circle objects and then calls the Circle class’s FindPois method to find their POIs. The Paint event handler does the rest.

Conclusion

[interlocked circles]

As I mentioned earlier, the 30 degree sweep angle may not work if the interlocked circles have POIs too close together or if two circles intersect at too small an angle. The method of alternating which circle belongs on top also only works if circles that intersect always intersect twice. For example, in the picture on the right, the red and green circles intersect only once. As a result, the red circle has three intersections so there is no way that it can be on top half of the time and on the bottom half of the time.

Download the example program to experiment with it. The code contains a some compiler directives that let you easily try out a few test patterns. If you build particularly interesting pictures and post them somewhere, feel free to mention them in the comments below.

[interlocked circles] [interlocked circles] [interlocked circles] [interlocked circles] [interlocked circles] [interlocked circles]


Download Example   Follow me on Twitter   RSS feed   Donate




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

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 , , , , , , , , , , , , | 2 Comments

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