Title: Make an Apollonian gasket filled with images in C#
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.
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 experiment with it and to see additional details.
|