macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

A Simple Drawing Sample in Quartz 2D
Pages: 1, 2, 3

Drawing the Star

To create the star, we'll follow the same basic steps we've been through before. The star is a more complex figure than the rectangles we've worked with so far. We're going to select an algorithm to construct a star-shaped path into the current path of our context. From there it's a simple matter of using CGContextFillPath to fill that path with white. We could easily construct the path so that it works just for our Texas flag. The path would be created in place over the blue field and at just the right size for this particular drawing.



In order to make our star-drawing routine a bit more generic, however, and to blatantly modify the sample so that it demonstrates more Quartz 2D functionality, we will eschew the obvious course and take a less direct approach. Our sample code will contain code to create a generic star path. With that done, it will show how changing the coordinate transformations before calling that routine will result in a star that is precisely positioned for our Texas flag. As an added bonus, the resulting routine will be general enough that some other application could use the path routine to draw other stars--say, the 50 stars of the U.S. flag. The code that creates the star path would be the same. All that would have to change is the coordinate manipulation code used to set up the coordinate system before it is called.

To draw our generic star path, we need five points that are evenly spaced around a circle. We can connect every other point in that set with lines, and the resulting figure is a five-pointed star. The sample code uses a little bit of trigonometry, in a loop, to simultaneously give us the points we need and generate the segments that connect them together. Don't let the math scare you; we could just as easily have taken our points from a geometry book and stored them in an array. The point is not to grasp the math behind the routine, but rather to understand the way the points are used to construct a path.

void DrawStar(CGContextRef inContext)
{
    CGPoint starPoints[5];
    const float starAngle = 2.0 * pi / 5.0;

    // Begin a new path (any previous path is 
    // discarded)
    CGContextBeginPath(inContext);
	
    // The point (1,0) is equivalent to 
    // (cos(0), sin(0))
    CGContextMoveToPoint(inContext, 1, 0);	

    // nextPointIndex is used to find every 
    // other point
    short nextPointIndex = 2;	
    for(short pointCounter = 1; 
              pointCounter < 5; 
              pointCounter++) {

        CGContextAddLineToPoint(inContext, 
		cos( nextPointIndex * starAngle ), 
		sin( nextPointIndex * starAngle ));
        nextPointIndex = (nextPointIndex + 2) % 5;

    }

    CGContextClosePath(inContext);

    CGContextFillPath(inContext);
}

Listing 3. The star's path-construction routine

A representation of the path this code creates is shown in Figure 3. The numbers in the figure indicate the order in which the points are added to the path. The call to CGContextBeginPath simply lets the context know that it can forget anything it has in its current path. It's not specifically needed in this case, but clearing the current path before you begin a new one is one of those habits that can save you headaches in more complicated code. The CGContextMoveToPoint routine begins constructing a new current path by setting the location of its first point. From here the loop adds a series of line segments to the path using CGContextAddLineToPoint. The code only draws four segments, and then closes the path with a call to CGContextClosePath.

When an application closes a path, it lets the computer know that the path should make a complete circuit. At the time Quartz 2D receives the close instruction, if the first and last points of the path do not match, the computer will add a final segment from the last point to the first point. This is exactly what we want, so we do that with our star code. A closed path is a bit different from just a path with the same staring and ending points. It is possible to create a path where the first and last points are coincident but the path is not closed. This has important implications when stroking the path, which we will have to leave until later.

Figure showing how the star path is constructed
Figure 3. Diagram of the star path

The path we have created is a lovely star shape, but it has a couple of issues in its current form that make it unsuitable for our flag. One of the most obvious problems, evident in Figure 3, is the direction that the star path points. On the Texas flag the apex of one arm of the star points straight up, while our star path doesn't have an arm pointing straight up. Less obvious from the diagram is the fact that the path we have created is centered around the origin. Our origin is the lower-left corner of the view, but we want the star to draw at the center of our blue field.

Finally, Figure 3 is a greatly enlarged representation of our path. The actual path we've created is tiny, with a radius of only one point. If we printed it on a high-resolution laser printer, it would be about 1/36 inch in diameter. That's on the order of the size of the period at the end of a sentence. Because we're drawing the screen's pixel map, that translates to few pixels at most. That's much too small for the flag of a state as big as Texas.

Earlier we looked at how coordinate transformations could simplify our drawing task. As it turns out, we can use coordinate transformations to make our path construction routine work in this case as well. To make the star larger, we can scale the coordinate system before creating its path. With the scaling factor in place, Quartz 2D will magnify every unit we cover in our star's path to several units (aka pixels) on the destination device. In a handy quirk of fate, the radius of our path just happens to be 1.0. That means all we need to do is decide what radius we would like our star to have, then scale the coordinate system by that amount to get a star path of the correct size.

Strange how that worked out so nicely, isn't it? The standards for the Texas flag tell us the star should be in a circle whose diameter is three-fourths of the width of the blue field. We're working with the radius of the star, which is half of a diameter, so we'll scale our star so that the radius is three-eighths of the width of the blue field:

// The star should be in a circle whose diameter 
// is 3/4 the width of the blue field.
float starRadius = 
		(blueRect.size.width * 3.0) / 8.0;	

...
CGContextScaleCTM(inContext, 
	starRadius, 
	starRadius);

...
DrawStar(inContext);

Listing 4. Scaling the coordinate system to make the star bigger

This works well and makes the star path as large as we need it to be. But scaling doesn't change the fact that the path is still centered on the origin, not the blue field. However, we've already seen that we can use CGContextTranslateCTM to relocate the origin to wherever we like. We can use that routine to move the origin to the center of the blue field before we draw our path. With that setup, the routine will still construct the star around the origin, but the origin will be at the center of the blue field. Consider, however, that we need to combine this translation with the scale transformation we've already decided to use. Some care is in order when combining transformations like this.

If you scale the coordinate system first and follow it with a translation, the computer will magnify your translation distances by the scaling factor. In the simple case, therefore, if we want to perform our translation with unscaled axes, we need to apply the translation before scaling the axes. In general, reversing the order of any two transformations has the potential to lead to vastly different results. This phenomenon leads to a popular malady in 3-D graphics programming called the dreaded black screen. The term comes from the black screen that is shown when 3-D graphics transformations are applied incorrectly, moving the objects in the scene outside the visible area. With Quartz 2D you are more likely to see a white screen than a black one, but the underlying cause is the same. If you can't see your drawing, double-check to ensure that your transformations are applied in the proper order.

With the translation in place, code looks like this:

// The star should be in a circle whose diameter 
// is 3/4 the width of the blue field.
float starRadius = 
        (blueRect.size.width * 3.0) / 8.0;	

...
CGContextTranslateCTM(inContext, 
	CGRectGetMidX(blueRect), 
	CGRectGetMidY(blueRect));

CGContextScaleCTM(inContext, 
	starRadius, 
	starRadius);

...
DrawStar(inContext);

Listing 5. Adding a translation to move the star into position

At this point, our star is drawing in the correct place and is of the correct size, but it's still not pointing the right way. By now you can probably guess that what we'll do is rotate the coordinate system. You can ask Quartz 2D to rotate the coordinate system about the origin by any angle. When the axes are set up in the default orientation, positive angles turn the axes counterclockwise and negative angles turn the axes clockwise (if the coordinate axes are in QuickDraw's orientation, the directions are reversed). Just as translations depend on the scale of the coordinate system, rotations depend on the position of the origin. In this case we want to get the origin into place (by performing our translation) before we turn the axes. Rotations are not really affected by scale factors, so it doesn't matter in which order we have the scale and rotate transformations.

The sample code adds the rotation after the scale. We need to rotate so that any of our star's points are turned upward. A convenient one to use is the point that is bisected by the x-axis. To make that point turn upward, we can rotate the coordinate system 90 degrees counterclockwise so that the x-axis points upward. We need to rotate the coordinate system by 90 degrees counterclockwise. Rotation angles in Quartz 2D, however, are specified in radians, not degrees. To convert degrees to radians we simply multiply the number of degrees by (pi / 180) radians per degree. That means our rotation will be 90 * (pi / 180) radians per degree or simply (pi / 2) radians. The final code looks like this:

// The star should be in a circle whose diameter 
// is 3/4 the width of the blue field.
float starRadius = 
        (blueRect.size.width * 3.0) / 8.0;	

// Rearrange the coordinate system for the 
// DrawStar routine then call it to draw the star.
CGContextSaveGState(inContext);
CGContextTranslateCTM(inContext, 
	CGRectGetMidX(blueRect),
	CGRectGetMidY(blueRect));
	
CGContextScaleCTM(inContext, 
	starRadius, 
	starRadius);
	
CGContextRotateCTM(inContext, pi / 2);

CGContextSetRGBFillColor(inContext, 
	1.0, 1.0, 1.0, 1.0);

DrawStar(inContext);
	
CGContextRestoreGState(inContext);

Listing 6. Adding a rotation

Once again, we wrap our transformation code in a CGContextSaveGState and CGContextRestoreGState pair. Our changes are encapsulated and less likely to affect surrounding code, should we ever add any. We've also added the code that sets the current fill color to white. Our DrawStar routine uses CGContextFillPath, which means that the system will use a technique called the nonzero winding rule to determine which pixels are inside the star.

A winding rule in this case is simply the technique the computer uses to decide which pixels are inside the path and therefore need to be painted with the current fill color. The path we've created for the star is self-intersecting and can be used to demonstrate other options. If you want to see one, change the CGContextFillPath call to CGContextEOFillPath. That tells Quartz 2D to fill the path using the even/odd winding rule. Apple provides at least two explanations of these winding rules at its web site. One can be found under "Filling a Path" in Chapter 4 of Drawing With Quartz 2D. Another is part of the discussion of the NSBezierPath class from Cocoa. You can find this at a page titled "Bezier Paths." In addition, most modern graphics textbooks have descriptions of these winding rules in the sections on filling polygons or paths.

A final note, as mentioned briefly before: when you issue a drawing command to paint the current path (usually either CGContextFillPath or CGContextStrokePath), the drawing operation will consume the current path and leave you with a clean slate. Our star drawing code re-creates the path each time, but if we were going to draw a lot of stars (say, the 50 stars on the U.S. flag), it might be more efficient to compute the path once and reuse it repeatedly.

To give you a glimpse at how we might accomplish this, our star code could create its path in a CGPath object and reuse the resulting CGPathRef to draw as many stars as necessary. Using CGPath to draw multiple stars is left as an exercise for you.

Final Thoughts

This concludes a simple sample of the way an application builds Quartz 2D graphics. We learned how Quartz 2D images are drawn from the background to the foreground by layering painted shapes. We saw how an application creates a shape by constructing a path in the drawing context. Once it has created a path, the code asks Quartz 2D to either fill the interior of the shape or outline the path with a stroke.

We looked at some convenience routines for working with simple shapes like rectangles, and created a more complex shape from line segments. We explored some of the basic tools that Quartz 2D gives you for selecting colors. We also looked at ways to use coordinate transforms to simplify these drawing tasks. In spite of how far we've come, however, we've examined only basic shapes, straight lines, and fills. We haven't created a stroke or even played with Bézier curves yet. There's still much to learn, but the little information we've covered so far provides fertile ground for further exploration.

Scott Thompson is a professional, contract software engineer with a long-standing interest in both computer graphics and their mathematics.


Return to MacDevCenter.com.