MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Programming With Cocoa

Introduction to Cocoa Graphics, Part 2

11/06/2001

Today's column is part two of our push to learn 2D graphics in Cocoa. In the last column, I covered how NSView and NSBezierPath work together to let us draw simple objects, and I discussed how to use some of the Foundation Framework’s fundamental data types in drawing. This allowed us to make ellipses and rectangles using NSBezierPath.

Now I'll build upon these concepts to draw arbitrary paths that are constructed from straight lines and curves, which allow us to make complex shapes.

Today's initial setup will be identical to that of the previous column (except for different filenames, of course). Start by creating a new project called Curves (or whatever you like), and add to it the files for a subclass of NSView called CurvesView (or, again, whatever suits your fancy). After that, go through the steps that we went through in the last column to connect CurvesView to an NSView container in Interface Builder. When you have completed these essential first steps, you’ll be ready to continue with this project.

Before I delve into the mechanics of NSBezierPath, allow me to indulge in a tiny bit of history about Bezier paths. Bezier paths (or curves) were developed by the French mathematician and automobile engineer Pierre Bezier (surprise!) in the 1970s. They were originally developed for CAD/CAM systems to help design and prototype Renault car bodies, and thus they made their way into computer graphics.

We’ve all had some contact with Bezier curves, as they are the underpinnings of Adobe’s Postscript drawing system. One of the reasons that Bezier curves are used extensively in computer graphics is because of their mathematical nature; Bezier curves are described by equations. Shapes drawn using Bezier curves can be magnified without any loss of sharpness or increase in granularity, as is common with bitmap images.

That's enough history for now. Let's get back to work.

Constructing paths

Bezier paths are built using a small set of construction methods such as –moveToPoint:, -lineToPoint:, and –curveToPoint:controlPoint1:controlPoint2: (more on this method to come).

When you construct paths, keep in mind the idea of the current point. The current point is the ending point of the last element you added, and the starting point of your next path segment. Essentially, the current point is the most recent point you used in constructing your path. This explains why –lineToPoint: only takes one NSPoint argument, when we know full well that two points define a line. For example, if your current point was (100, 100), then the code would create a vertical line:


[aPath lineToPoint:NSMakePoint(100, 300)];
 

It is understood that path segments will begin at the current point, and extend to the point indicated in the argument. Using the –moveToPoint: method, we can change the location of the current point without adding to the path. This is useful for defining the starting point of a freshly instantiated path, or for simply "lifting your pencil" in the middle of constructing a path.

Let’s consider the example of drawing a simple triangle to illustrate these ideas. The way we’ll do this is to first define three points that will be the vertices of the triangle; then create a new, empty instance of NSBezierPath; construct the path of our triangle using the three points we've defined; and finally, draw it on screen. All of this will be written in the –drawRect method of CurvesView:


- (void)drawRect:(NSRect)rect
{
    // The three vertices of the triangle
    NSPoint p1 = NSMakePoint(100, 100);
    NSPoint p2 = NSMakePoint(200, 300);
    NSPoint p3 = NSMakePoint(300, 100);

    // Constructing the path
    NSBezierPath *triangle = [NSBezierPath bezierPath];
    [triangle moveToPoint:p1];
    [triangle lineToPoint:p2];
    [triangle lineToPoint:p3];
    [triangle lineToPoint:p1];

    // Draw the path
    [[NSColor blueColor] set];
    [triangle stroke];	// or [triangle fill];
}
 

Comment on this articleNow that we know you can create graphics in Cocoa, let's discuss how we can put these tools to use.
Post your comments

As you can see, we first defined the three vertices of the triangle, and then we started our path construction by instantiating NSBezierPath, assigning the variable triangle to this object, and then “moving” to the first point—putting our pencil on the paper. From there we added the three lines connecting the vertices, set the color, and finally told the triangle to draw itself onto the screen.

NSBezierPath has a method called –closePath that appends a path element to your path that is a straight line from the current point to the starting point of your path — it connects the beginning and end of your path with a line, thereby closing it. As we’ll see in a moment, it’s a good idea to close your path (if your path is indeed supposed to be a closed path) when you’ve finished. We could have used –closePath above to finish our triangle by replacing the last line's construction code


[triangle lineToPoint:p1];
 

with a –closePath message to triangle:


[triangle closePath];
 

Both of these messages to triangle have the effect of adding a straight line from p3 to p1. The second message, using –closePath, has the effect of actually joining the endpoint to the starting point of the path rather than creating the appearance of a closed path, as is done in the first method. In a moment, we’ll see a more annoying consequence of not using –closePath.

In addition to the methods that take absolute points as their arguments, NSBezierPath declares a set of methods that allow us to indicate relative positions of points. Thus, instead of saying p2 should be at (200, 300), we can say that p2 is (100, 200) relative to p1. So now p2 is more of a direction vector, as opposed to an absolute location in the plane. Using the method relativeLineToPoint:, we can rewrite our method as:


- (void)drawRect:(NSRect)rect
{
    // The three vertices of the triangle
    NSPoint p1 = NSMakePoint(100, 100);
    NSPoint rp2 = NSMakePoint(100, 200);
    NSPoint rp3 = NSMakePoint(100, -200);

    // Constructing the path
    NSBezierPath *triangle = [NSBezierPath bezierPath];
    [triangle moveToPoint:p1];
    [triangle relativeLineToPoint:rp2];
    [triangle relativeLineToPoint:rp3];
    [triangle closePath];

    // Draw the path
    [[NSColor blueColor] set];
    [triangle stroke];	// or [triangle fill];
}
 

Learning CocoaLearning Cocoa
By Apple Computer, Inc.
Table of Contents
Index
Sample Chapter
Full Description
Read Online -- Safari

Lines aren’t the only things we use to build shapes; we can do curves as well, using the method –curveToPoint:controlPoint1:controlPoint2:. As you can see, this method is a bit more complicated than making a line, so let me say a thing or two about these "control points" we see in the method name.

Each endpoint of the curve has a control point associated with it. The starting point of the curve (the current point) and controlPoint1 define a vector that is tangent to the curve at the current point.

Similarly, the endpoint of the curve (from the argument list) and controlPoint2 define another vector that is tangent to the curve at the endpoint. Together these two vectors provide enough information for the algorithms behind NSBezierPath to fit a cubic polynomial equation to the provided constraints. This polynomial, in turn, defines a very natural-looking curve. You can see the different parts of a curve in the figure below.

Screen shot.
Figure 1.

We can incorporate a curve into our triangle to give it a wavy bottom in the following way:


- (void)drawRect:(NSRect)rect
{
    // The three vertices of the triangle
    NSPoint p1 = NSMakePoint(100, 100);
    NSPoint p2 = NSMakePoint(200, 300);
    NSPoint p3 = NSMakePoint(300, 100);

    // The control points of the curve
    NSPoint c1 = NSMakePoint(200, 200);
    NSPoint c2 = NSMakePoint(200, 0);

    // Constructing the path
    NSBezierPath *triangle = [NSBezierPath bezierPath];
    [triangle moveToPoint:p1];
    [triangle lineToPoint:p2];
    [triangle lineToPoint:p3];
    [triangle curveToPoint:p1 controlPoint1:c1 controlPoint2:c2];

    // Fill the path
    [[NSColor blueColor] set];
    [triangle fill];

    // Draw the outline
    [[NSColor redColor] set];
    [triangle stroke];
}

Remember that controlPoint1 defines the line from the current point, and controlPoint2 defines the line from the point given in the argument for curveToPoint:. Notice that in this example, we first filled the shape of the path with blue and then stroked the path using red, effectively creating a red border on the shape, as shown in the image below. When doing multiple stroke or fill commands such as we have done here, more recent strokes or fill will be drawn on top of previous calls to these methods.

Screen shot.
Figure 2.

With the basics of creating paths under our belts, we’ll now look at a few of the line and curve properties that we can change.

Changing the characteristics of paths

It's possible to change the characteristics of Bezier paths via several methods. For example, we can change the thickness of a stroked path using the method –setLineWidth:, where the argument is a float indicating the width of the line in points. We can change the thickness of the lines of our triangle by adding the following just before we send a stroke message to triangle:

[triangle setLineWidth:10];

This will make our drawing look like the following:

Screen shot.
Figure 3.

If you set the line width to 0 using this method, then the path will be stroked with the smallest width that can be displayed on the screen. I choose a thick line width here to bring out some features about the way path elements are joined to each other.

First, notice how the lower right vertex of the triangle is a very sharp point that extends far beyond what we originally intended for our triangle. The image below shows a thin green line superimposed on top of the thicker red line to highlight the effect I’m speaking of.

Screen shot.
Figure 4.

The reason for this is that by default, lines in a path are joined together using a so-called miter join where the outside edge of the two intersecting lines are extended to a point. This default join style is defined the constant NSMiterLineJoinStyle, which we use to identify the join style in code. We can fix this problem in several ways.

One, we can change the line join style to either NSRoundLineJoinStyle or NSBevelLineJoinStyle using the method -setLineJoinStyle. For example, if we wanted to make the vertices rounded, we would pass the constant NSRoundLineJoinStyle as an argument to setLineJoinStyle:

[triangle setLineJoinStyle:NSRoundLineJoinStyle];

Alternatively, we can flatten the corners by using NSBevelJoinStyle. In the image below, I show the three line join styles side-by-side with a representation of the original path drawn in green.

Screen shot.
Figure 5.

Line joining, however, is not the end of our woes here. Remember our discussion about closing paths and the line join issues associated with it? Well, here is a perfect example of what happens when the path is not officially closed, despite your shape having the appearance of being closed.

Notice in the lower left corner that the lines are never joined. The reason for this is simple enough: this corner is both the start of the path and the end of the path. We never say that these two points -- despite them being the same point -- should be joined. To fix this, we send a closePath message to triangle after our last construction command to close the path off, and now any join style will apply correctly, as shown below:

Screen shot.
Figure 6.

The final code the produced this image is as follows:


- (void)drawRect:(NSRect)rect {
    //The three vertices of the triangle
    NSPoint p1 = NSMakePoint(100, 100);
    NSPoint p2 = NSMakePoint(200, 300);
    NSPoint p3 = NSMakePoint(300, 100);
    NSPoint c1 = NSMakePoint(200, 200);
    NSPoint c2 = NSMakePoint(200, 0);
    
    // Constructing the path
    NSBezierPath *triangle = [NSBezierPath bezierPath];
    [triangle moveToPoint:p1];
    [triangle lineToPoint:p2];
    [triangle lineToPoint:p3];
    [triangle curveToPoint:p1 controlPoint1:c1 controlPoint2:c2];
    [triangle closePath];
    
    // Fill the path
    [[NSColor blueColor] set];
    [triangle fill];

    // Draw the outline
    [triangle setLineJoinStyle:NSRoundLineJoinStyle];
    [triangle setLineWidth:10];
    [[NSColor redColor] set];
    [triangle stroke];

    // Draw representation of original path
    [triangle setLineWidth:0];
    [[NSColor greenColor] set];
    [triangle stroke];
}

The end

So there you have it! This is how you can create any shape that you may need. NSBezierPath has many more methods that do useful things, many of which we will cover in future columns.

As always, I recommend that you become familiar with the NSBezierPath class reference if you haven’t done so already. For now, I leave you with these most useful tools with which you can do a lot. In the next column we will start to learn about how you can interact with elements of your drawing on screen using the mouse, so stay tuned for some good stuff. See you then!

Michael Beam is a software engineer in the energy industry specializing in seismic application development on Linux with C++ and Qt. He lives in Houston, Texas with his wife and son.


Read more Programming With Cocoa columns.

Return to the Mac DevCenter.

Copyright © 2009 O'Reilly Media, Inc.