Browse DevX
Sign up for e-mail newsletters from DevX


"Fat" Marching Ants: An Algorithmic Experimentation Using GDI+ : Page 3

"Marching ants" are a common UI feature in image editing programs but giving the ants a little more visual texture is a harder problem than you'd ever dream. This article discusses four different algorithms for making elegant, 3D ants with varying levels of performance, accuracy, and control.




Building the Right Environment to Support AI, Machine Learning and Deep Learning

Algorithm No. 3: Whole Ant FMA
And now for something completely different. The arc-based and line-based FMA algorithms sliced and diced the poor ant and rendered it piecemeal. This algorithm takes a different approach. It renders a full ant in one swoop. This is impossible of course. At the graphics hardware level everything is rendered pixel-by-pixel by a fancy "Laser" (my homage to Dr. Evil) cannon or some other gadget. What I mean is that at the graphics API level, which is what the code interfaces with, I prepare a whole ant shape and fill it in one call. Admittedly, I use GDI+'s facilities and I don't write everything from scratch. If you want to rough it you can calculate the ant contour (two parallel lines and two "parallel" arcs) and fill the enclosed area (flood fill or multiple Bresenham lines between points with the same Y on the contour).

The whole ant FMA needs to address two issues: how to draw an ant in various orientations and how to handle ants that are split between two line segments. To solve these problems, I prepared an ant shape using GDI+ GraphicsPath facility and for each line segment in a specific orientation I rotate the ant using a transformation matrix. In order to draw seemingly partial ants I use clipping. This means that if an ant is split between two consecutive line segments I actually draw two ants. One ant at the end of the first line segment and another ant at the beginning of the second line segment (with appropriate offsets).

I clip the rendering area so the parts of the ants that should not be rendered will not stick out. Figure 9 shows the whole ant FMA without clipping. Figure 10 shows what it looks like with clipping. In both figures I emphasize in green the clipping region. The effect (with clipping of course) is quite pleasant and there is no need to draw the "knee."

Figure 9. The whole ant FMA without clipping is visually untidy.
Figure 10. The whole ant FMA with clipping is tidy and eliminates the need to draw the "knee."

Algorithm No. 4: Dash FMA
Figure 11. The Dash FMA looks pretty good but you can forget about making an ant split between two line segments effectively.
Suppose the graphics API you are using inherently supports rendering of "fat" dashed lines in a given offset. Well, this is the case with GDI+. The algorithm in this case is very simple for each line segment: just draw a line with the proper dash attributes (see Figure 11). There are two issues with this method: It's difficult to control the ant shape and there is no concept of splitting an ant properly between consecutive line segments.

GDI+ Intro
GDI+ is an object-oriented API for 2D graphics programming. It supersedes the old Win32 GDI (Graphics Device Interface). It takes a lot of the drudgery out of doing graphics on Windows due to its object-oriented programming model, which differs from the handle-based programming model of GDI. GDI+ is not a one-to-one mapping of GDI. There is much more functionality. The organization of classes in name spaces and the APIs are superb. Inefficient constructs and functions such as SetPixel and LineDDA were dropped.

GDI+ is implemented as a plain DLL (gdiplus.dll) with a flat C API (some 600 functions), which is bundled with a C++ API (wrapper classes). Microsoft recommends and supports only the C++ API. In addition the .NET framework provides a managed wrapper for GDI+ in the System.Drawing.* namespaces. The most significant drawback of GDI+ is its relatively slow performance. People in the know claim that it is due to a lack of hardware acceleration for GDI+. It should improve in future versions of the .NET framework. Anyway, it should be adequate for most business and UI purposes. Just don't write Doom 4 using GDI+.

I'll quickly introduce the main objects I use in Squigler (the name I gave to my fat marching ant program) and in the GDI+ demo program. There is much more to GDI+ and I encourage you to take the plunge.

—The Graphics Class
The Graphics class is the heart and soul of GDI+; it encapsulates the device context and offers a plethora of properties and methods to control every aspect of rendering such as rendering quality, clipping, and transformations. It also offers a rich variety of DrawXXX methods to draw anything from lines and arcs to text and polygons. Painting shapes is equally easy with the FillXXX methods.

—Coordinate System and Basic Structures
GDI+ uses a coordinate system based on floating point rather than integer units. This may seem strange since eventually you must draw your lines, curves, and shapes on integer pixels (raster graphics, remember?). The reason is that the graphic objects may undergo various transformations and the rounding errors that might be introduced could accumulate and distort the end result. The type of each coordinate is the System.Single ('float' in c#). It is not a System.Double ('double' in C#) because floats have 7 digits of precision (which is more than enough for graphics transformations) and take 32-bit each. Each double has 15 digits of precision but takes a whopping 64-bit. So, it is a very reasonable tradeoff of accuracy vs. space. It is a bit uncomfortable when you need the trigonometric functions (sin, cos, atan, etc.), because they all use 'double' arguments and return values. In these cases, you have to cast between float and double—back and forth.

Most of the methods that require locations or sizes have multiple overloads and accept either integer values or floating point values. The integer values are for convenience and can be used when no transformation is performed on the relevant object. The classes that represent locations, sizes, and areas are Point, PointF, Size, SizeF, Rectangle, and RectangleF. The classes whose names end with 'F' have float fields, while the others have integer fields. The Region class can be used to describe arbitrary complex areas.

—Pens and Brushes
When using the DrawXXX and FillXXX functions, pass as first argument either a pen or a brush. A pen controls every aspect of the drawn line such as color, width, dash style, end points style, and line joins.

pen = new Pen(Color.Crimson, 5); pen.StartCap = LineCap.DiamondAnchor; pen.EndCap = LineCap.ArrowAnchor; g.DrawLine(pen, new Point(5, 10), new Point(20, 30);

The code above demonstrates how to create a pen with a crimson color and a line width of 5 pixels, assign it certain cap styles, and finally draw a line between two points using this pen.

A brush controls how the interior of closed graphic shapes (polygons or graphic paths) will look. There are multiple brush types: solid, hatch, texture, and gradient. Solid brushes are the simplest and just paint an area with a uniform color:

Brush brush = new SolidBrush(Color.OliveDrab); Rectangle rect = new Rectangle(200, 200, 150, 150); g.FillRectangle(brush, rect);

There are multiple constructors for pens and brushes so be sure to choose the one best suited to your needs and enjoy the various defaults without specifying excruciatingly every property.

—Lines and Arcs
Lines and arcs are the Swiss army knife of 2D graphics. They are both very easy to use and allow creating the contours of any shape. You can draw lines and arcs using the overloaded DrawLine and DrawArc methods of the Graphics class passing in a pen:

pen = new Pen(Color.Crimson, 5); pen.StartCap = LineCap.DiamondAnchor; pen.EndCap = LineCap.ArrowAnchor; g.DrawLine(pen, new Point(5, 10), new Point(20, 30);

When drawing an arc the arguments are a rectangle (defined by four integers/floats or two points) an ellipse, a start angle, and a sweep angle. The arc will start at the start angle and continue to "sweep" as far as the sweep angle defines. For a full circle pass 360 as the sweep angle. Note the angles are measured in degrees and not radians.

A graphics path is a rather powerful beast. It allows aggregating multiple figures, lines, arcs, and curves. It even allows a single path to contain multiple disconnected figures. It can easily perform various transformations (see "Transforms," below) on a graphics path by manipulating its Transform attribute. You can render a graphics path by using the DrawPath or FillPath methods of the Graphics class. The code below demonstrate how to create a graphics path, move it a certain distance (using a translation transform), and render it.

Figure 12. GDI+ transformations translate, rotate, and scale turned the version on the left into the version on the right.

// Create a transformation matrix Matrix m = new Matrix(); // move 100 pixels to the right and 50 pixels down m.Translate(100, 50); // Create an ant-like graphics path GraphicsPath p = new GraphicsPath(); p.AddLine(0, 0, 50, 0); p.AddArc(50, 0, 20, 20, -90, 180); p.AddLine(50, 20, 0, 20); p.AddArc(-10, 0, 20, 20, 90, -180); // move the graphics path by applying the translation matrix m_path.Transform(m); // Draw the path and fill it too g.FillPath(new SolidBrush(Color.Orange), p); g.DrawPath(new SolidBrush(Color.Black), p);

Transforms are the way to move, rotate, and scale things in GDI+. They can also be used to do interesting things to colors. The basic idea is all these transformations can be represented by matrices. Matrices can be combined by multiplying so multiple effects can be achieved by applying the product of multiple matrix multiplications (the result is a single matrix). Figure 12 presents a bunch of lines, arcs, and a graphics path. The whole enchilada has been rendered twice: once as is and then again after "suffering" three transformations (translate, rotate, and scale).

The code for the entire program is in Listing 1. The Paint event handler of the form calls to methods Draw() and DrawSkewed(). Draw() simply draws all the objects on the screen as seen on the left side of Figure 7. DrawSkewed() creates a combined transformation matrix, applies it to the entire Graphics object and then calls Draw() again. The results can be seen on the right side of Figure 12.

Thanks for your registration, follow us on our social networks to keep up-to-date