Title: Draw an image spiral in C#
This example lets you draw image spirals similar to the one shown on the right. 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 picture on the right shows an example.
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 = AeBθ 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 to experiment with it and to see additional details.
|