There is not a day that goes by where I’m asked how brushes in bitmap graphics editors work. And I note this with quite some dismay. I mean, not even my fiancĂ©e asks me. She always wants to talk about our “relationship” and our “future,” never about brush stamping algorithms. Talk about insensitive.

Since none of you insensitive clods were thoughtful enough to ask me about brushes, I’ll ask myself:

So Andy, how do brushes in bitmap editors work?

Wow, what a great question. I can tell from the quality of the question that you are both smart and handsome.

Let’s get started.

Basic idea

The idea behind a bitmap brush is actually really simple. You take an image of the tip of the brush you want to draw with and repeatedly draw it, with enough frequency that it looks like you dragged the brush tip across the screen.

For example, let’s say you had a brush with a tip that looked like this:

Blue, round, 20px brush

As you can see that is a round, blue brush, 20 pixels in diameter. In Fireworks parlance, it would be a 20px hard round brush, with 0% softness.

To simulate brushing with this, you would draw the image above repeated between the two points the user dragged their mouse. A naive implementation might space out the drawing of the brush tip to the width of the brush tip:

Blue brush stamped 20 pixels apart

As you can see, there are gaps between the drawn images, so it doesn’t look so much like someone dragged a brush across the screen, as they dribbled a brush across it. An obvious improvement is to draw the brush tip only half the width of the brush apart:

Blue brush stamped 10 pixels apart

That is obviously better, but not quite it. If we increase the frequency of drawing to a quarter of the width of the brush tip, we get acceptable results:

Blue brush stamped 5 pixels apart

The method of repeated drawing an image repeatedly like this is commonly called stamping, although I’ve also heard it referred to as “dabbing.”

In general, I’m going to relate back to Fireworks for comparative functionality and parlance, although it should be similar to “that other image editor,” Photoshop. The sample code shows how to implement Fireworks four kinds of “basic” brushes, sans the texture support.

Code architecture

To go along with this article I have created some sample code that will demonstrate everything I talk about. It’s a mixture of Cocoa and Quartz, although the ideas should work in any environment; only the API calls will change. This is within reason of course: Quartz takes care of a lot of complicated things like alpha blending and antialiasing, and if your API (like QuickDraw, GDI) can’t deal with that, then you’ll have a lot more work to do.

There are three main classes that make up the sample code: CanvasView, Canvas, and Brush.

CanvasView is an NSView, and for the most part will be ignored in this article. It serves as a moderator between the Canvas and Brush objects, and doesn’t contain any real functionality. It passes on the drawRect: message to the Canvas object, and mouseDown:, mouseDragged:, and mouseUp: messages to the Brush object.

Canvas implements two graphics primitives: draw a brush at a specific point, and draw a line with a brush. i.e. It is the class that implements the stamping algorithm. It also has a method to transfer its rendered contents into an NSGraphicsContext. Just like the name implies, it represents the canvas, piece of paper, etc that the user draws onto.

The Brush class represents the tool the user draws with. As such, it takes user input (in our case only mouse events) and turns them into graphics primitives for the Canvas object. It is also responsible for generating the image of the tip of the brush, which, in this sample code, is somewhat configurable.

In this article, I’ll tackle the Canvas class first, then build on it with the Brush class. As I stated before, I’ll ignore the CanvasView class, but you can always see exactly what it does (nothing) by downloading the code.

Canvas

The Canvas class is implemented using a CGBitmapContext as its backing store. It has a –(id) initWithSize:(NSSize)size method, as well as a –(void) dealloc method. Nothing real exciting happens either place so I’ll just summarize them. The init method creates a 32-bit ARGB bitmap context at the specified size, the fills the entire context with an opaque white. The dealloc method simply releases the bitmap context.

Drawing the canvas onto a view

The Canvas class also has a drawRect:inContext: method that transfers the contents of the bitmap context into the NSGraphicsContext that the CanvasView passes in. Nothing complicated happens here either, but I’ll show it for completeness:

- (void)drawRect:(NSRect)rect inContext:(NSGraphicsContext*)context
{
	// Here we simply want to render our bitmap context into the view's
	//	context. It's going to be a straight forward bit blit. First,
	//	create an image from our bitmap context.
	CGImageRef imageRef = CGBitmapContextCreateImage(mBitmapContext);

	// Grab the destination context
	CGContextRef contextRef = [context graphicsPort];
	CGContextSaveGState(contextRef);

	// Composite on the image at the bottom left of the context
	CGRect imageRect = CGRectMake(0, 0, CGBitmapContextGetWidth(mBitmapContext),
								  CGBitmapContextGetHeight(mBitmapContext));
	CGContextDrawImage(contextRef, imageRect, imageRef);

	CGImageRelease(imageRef);

	CGContextRestoreGState(contextRef);
}

As you can see, we simply create a CGImageRef from our bitmap context and then draw it right into the provided NSGraphicsContext. Like I said, nothing terribly exciting going on yet.

Rendering a single stamp

Things get a little more interesting with the simplest graphics primitive, stampMask:at:, which draws a CGImageRef centered on a specific point. It is used by the line drawing primitive on the Canvas object as well as the Brush object directly, when handling a mouseDown: message. It’s fairly straight forward:

- (void)stampMask:(CGImageRef)mask at:(NSPoint)point
{
	// When we stamp the image, we want the center of the image to be
	//	at the point specified.
	CGContextSaveGState(mBitmapContext);

	// So we can position the image correct, compute where the bottom left
	//	of the image should go, and modify the CTM so that 0, 0 is there.
	CGPoint bottomLeft = CGPointMake( point.x - CGImageGetWidth(mask) * 0.5,
									  point.y - CGImageGetHeight(mask) * 0.5 );
	CGContextTranslateCTM(mBitmapContext, bottomLeft.x, bottomLeft.y);

	// Now that it's properly lined up, draw the image
	CGRect maskRect = CGRectMake(0, 0, CGImageGetWidth(mask), CGImageGetHeight(mask));
	CGContextDrawImage(mBitmapContext, maskRect, mask);

	CGContextRestoreGState(mBitmapContext);
}

This works how you think it would. It determines where the bottom left of the image should be positioned, such that the image’s center is at the point passed in. It then translates the context so that 0, 0 is where the bottom left of the image should be, and draws the image.

Rendering a line of stamps

Alright, now that we have all the building blocks of Canvas done, we can move on to the meat of Canvas, stampMask:from:to:leftOverDistance:, which is the method that draws a line with the given brush image. This is a decent sized function, so I’m going to cover it in parts.

First, the declaration:

- (float)stampMask:(CGImageRef)mask from:(NSPoint)startPoint to:(NSPoint)endPoint leftOverDistance:(float)leftOverDistance

  • mask is the brush image that we’re going to stamp.
  • startPoint is the starting point of the line.
  • endPoint is the ending point of the line to draw.
  • leftOverDistance is the distance of the specified line that we did not render on the previous invocation (more on this later.) This will always be the return value from the previous invocation of this function.
  • The return value is the remainder of the line that we didn’t render.

The first thing we do in stampMask:to:from:leftOverDistance: is to determine the spacing between stamps of the image:

// Set the spacing between the stamps. By trail and error, I've
//	determined that 1/10 of the brush width (currently hard coded to 20)
//	is a good interval.
float spacing = CGImageGetWidth(mask) * 0.1;

// Anything less that half a pixel is overkill and could hurt performance.
if ( spacing < 0.5 )
	spacing = 0.5;

Initially, we compute the spacing between stamps to be 1/10 of the width of the brush. In the overview, I used 1/4 of the width, but after quite of bit of trail and error, I decided that 1/10 of the width looked better. It is rather subjective; feel free to play around with this value, it often varies based on what kind of brush you have. In fact, if this were a real system, we’d ask the brush for the spacing instead of computing it here.

We also cap the lower bound of the spacing. Anything less than half a pixel is pretty much overkill and won’t really gain us anything, except for slower performance. You might run into this if you play with some of the values in Brush, which I’ll get to later.

I should also note that this code makes a major assumption: that the brush bounding box is square. i.e. CGImageGetWidth() == CGImageGetHeight(). This doesn’t mean the brush has to be actually square (it could be a circle, and by default is), but it does mean the brush currently has to be symmetrical both vertically and horizontally.

The way we’re going to plot this line is to start at the startPoint passed in, and increment the x and y components such that we cover a distance of spacing on each iteration. On each iteration of the loop, we’ll draw the image at the newly computed point.

To get the the x and y increments, we need to compute the deltas between the start and end points. This essentially computes the slope of the line:

// Determine the delta of the x and y. This will determine the slope
//	of the line we want to draw.
float deltaX = endPoint.x - startPoint.x;
float deltaY = endPoint.y - startPoint.y;

The problem is that the distance of the slope (x and y increments) computed here isn’t uniform: it could be any distance based on how far the user dragged the mouse. Since we want our increments to be spaced evenly (always a spacing distance apart), we need to normalize the x and y increments such that their distance is 1 (which is what normalization of a vector, by definition, does).

// Normalize the delta vector we just computed, and that becomes our step increment
//	for drawing our line, since the distance of a normalized vector is always 1
float distance = sqrt( deltaX * deltaX + deltaY * deltaY );
float stepX = 0.0;
float stepY = 0.0;
if ( distance > 0.0 ) {
	float invertDistance = 1.0 / distance;
	stepX = deltaX * invertDistance;
	stepY = deltaY * invertDistance;
}

Part of the computations for normalizing our slope includes computing the distance between the start and end points. We’ll need that next.

The next thing we do in the function is declare a couple of variables for calculating the offset for the next stamp. This is only used in the loop, so it’s not germane to the next part of the function we’re currently discussing, but I hate it when tutorials leave out important parts of the function, so here they are.

float offsetX = 0.0;
float offsetY = 0.0;

If you recall earlier, we were passed in the distance that our previous invocation did not cover. This time around, we want to get that part too, if possible. So add it to the total distance we want to cover in our stamping loop:

// We're careful to only stamp at the specified interval, so its possible
//	that we have the last part of the previous line left to draw. Be sure
//	to add that into the total distance we have to draw.
float totalDistance = leftOverDistance + distance;

The stamping loop is pretty simple. As stated before, it will simply cover the total distance (the left over distance from the previous invocation plus the new distance we got in the current invocation) going at increments of spacing. The basic stamping loop (and rest of the function) looks like:

// While we still have distance to cover, stamp
while ( totalDistance >= spacing ) {
	// ... increment the offset and stamp...

	// Remove the distance we just covered
	totalDistance -= spacing;
}

// Return the distance that we didn't get to cover when drawing the line.
//	It is going to be less than spacing.
return totalDistance;

I’ve included the return statement, which is directly after the end of the while statement, to make a point. Because our loop only exits if totalDistance < spacing, the return value is also always going to be less than spacing. Furthermore, because our parameter leftOverDistance is always the return value of the previous invocation, it is also always less than spacing. That will be important when we start digging around in the guts of our stamping loop.

The other thing to note here is that we stop if the next stamp would put us past the distance we were supposed to draw. i.e. We never overdraw, but we could underdraw. As you’ll see in the guts of the stamping loop, we take care to only draw at the specified spacing, so that our brush strokes are even and smooth.

Speaking of the guts of our stamping loop, let’s dig around in there. The first thing to do inside of the loop is determine the offset from the starting point to draw the next stamp at:

	// Increment where we put the stamp
	if ( leftOverDistance > 0 ) {
		// If we're making up distance we didn't cover the last
		//	time we drew a line, take that into account when calculating
		//	the offset. leftOverDistance is always < spacing.
		offsetX += stepX * (spacing - leftOverDistance);
		offsetY += stepY * (spacing - leftOverDistance);

		leftOverDistance -= spacing;
	} else {
		// The normal case. The offset increment is the normalized vector
		//	times the spacing
		offsetX += stepX * spacing;
		offsetY += stepY * spacing;
	}

As you’ll probably note, offsetX and offsetY are the two variables that we are calculating here. They accumulate as the loop continues.

The first thing we check is if we have any left over distance from the previous invocation of our function. If so, we need to handle it first. Recall that stepX and stepY have been normalized, so that their distance is one. Normally we’d multiple them by spacing so that the next stamp in the loop is spacing pixels from the previous stamp. But since we have distance we already skipped from the previous invocation, we don’t want to skip it again, so we subtract it from spacing, and multiple that by stepX and stepY.

As an example, suppose spacing was 5 pixels, and the previous invocation had 12 pixels of distance to cover. It would have 2 pixels left over, which we would receive in the current invocation as leftOverDistance. Since we already have 2 pixel distance between the last stamp point and startPoint, we only want to advance by 3 pixels in distance after startPoint before we stamp again. This keeps the distance between stamps the same, which is important. Otherwise the “ink” clumps where it’s not supposed to, and looks wrong.

At the end of the first if clause, we subtract spacing from leftOverDistance, since we handled it. Recall that leftOverDistance is always less than spacing so leftOverDistance goes to less than 0, and we don’t ever hit the if clause again in the current invocation.

The else clause is the normal case, where we just multiply the normalized vector, stepX and stepY, by spacing and add it to the offset from the start point.

Now that we’ve computed the offset from the starting point, we compute the absolute position of the stamp. It’s straight forward:

	// Calculate where to put the current stamp at.
	NSPoint stampAt = NSMakePoint(startPoint.x + offsetX, startPoint.y + offsetY);

We now have all the information we need to actually stamp the image. So the last part of the loop is simply calling the other graphic primitive on Canvas, stampMask:at:

	// Ka-chunk! Draw the image at the current location
	[self stampMask:mask at: stampAt];

And that concludes both the stampMask:from:to:leftOverDistance: message and the Canvas class. As you can tell, its fairly straight forward, with the possible exception of the code to ensure the stamps are always evenly spaced. To summarize, the Canvas class provides the basic drawing primitives for drawing a single stamp and a line of stamps. It can then render itself to a view context.

Brush

The other interesting class in the sample code is the Brush class. Its primary purpose is to tell the Canvas object where to draw lines, and construct an image of the brush for the Canvas class to use to stamp with.

Parameters

Like the Canvas class, the Brush class has init and dealloc methods. However, the init method on the Brush class is actually interesting because it allows you to do a fair amount of customization to the brush instance. The init method looks like:

- (id) init
{
	self = [super init];

	if ( self ) {
		mRadius = 10.0;

		// Create the shape of the tip of the brush. Code currently assumes the bounding
		//	box of the shape is square (height == width)
		mShape = CGPathCreateMutable();
		CGPathAddEllipseInRect(mShape, nil, CGRectMake(0, 0, 2 * mRadius, 2 * mRadius));
		//CGPathAddRect(mShape, nil, CGRectMake(0, 0, 2 * mRadius, 2 * mRadius));

		// Create the color for the brush
		CGColorSpaceRef colorspace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);
		float components[] = { 0.0, 0.0, 1.0, 1.0 }; // I like blue
		mColor = CGColorCreate(colorspace, components);
		CGColorSpaceRelease(colorspace);

		// The "softness" of the brush edges
		mSoftness = 0.5;
		mHard = NO;

		// Initialize variables that will be used during tracking
		mMask = nil;
		mLastPoint = NSZeroPoint;
		mLeftOverDistance = 0.0;
	}

	return self;
}

The init method is interesting because of the member data it sets. There are five you can alter, which change how the brush looks, and thus draws.

  • mRadius This is simply how big the brush is, from the center to the outside edge. As stated before, this code assumes the shape bounding box is square. Here are some variations of the mRadius variable:

    mRadius = 10, 20 pixels
    mRadius = 5, 10 pixels
    mRadius = 20, 40 pixels

  • mShape This is a CGPathRef, which describes the shape of the brush tip. Because of my limited imagination, the two examples given here are a circle and square:

    mShape = circle, Shape is circle
    mShape = square, Shape is square

  • mColor Probably the easiest to understand of the variables, this is a CGColorRef that specifies the color of the brush. Here are the three easiest examples in the RGB colorspace:

    mColor = [1.0, 0.0, 0.0], Color is red
    mColor = [0.0, 1.0, 0.0], Color is green
    mColor = [0.0, 0.0, 1.0], Color is blue

  • mSoftness This is simply a percentage, represented by a float ranging from 0.0 to 1.0. It determines how much we soften the edges of a brush. It is usually related to mHard, but they can be used independently. Some examples of softness:

    mSoftness = 0.0, Softness is 0%
    mSoftness = 0.5, Softness is 50%
    mSoftness = 1.0, Softness is 100%

  • mHard This is a simple boolean that usually relates to mSoftness. If mHard is YES, then mSoftness is typically 0.0. It determines if the brush is fully opaque or 50% overall. The two options:

    mHard = YES, Hard is yes
    mHard = NO, Hard is no

These parameters can obviously be used in conjunction with each other, in many different combinations.

Creating the brush tip image

Now let’s delve into how these parameters are implemented by dissecting the createShapeImage function, which creates the image the Canvas uses to stamp with. It is a fairly involved function, so we’ll take it piece by piece, starting with the declaration:

- (CGImageRef) createShapeImage

It doesn’t take any parameters, because it pulls from the member data, and returns a CGImageRef. The returned image is cached for the duration of the mouse tracking, then released.

The first thing we do is create a bitmap context to draw into:

// Create a bitmap context to hold our brush image
CGContextRef bitmapContext = [self createBitmapContext];

We do this by calling a member function, createBitmapContext, which I won’t detail here, because its uninteresting. It simply creates a bitmap context the size of the brush bounding box, and clears it to transparent.

Next we implement the part of the function that handles the mHard parameter. If mHard is yes, we want to render the brush with full opaqueness, otherwise, we want render the brush tip at 50% transparency. Since we’ll potentially be doing several drawing operations that should be treated as a whole, we need to group them using a transparency layer:

// If we're not going to have a hard edge, set the alpha to 50% (using a
//	transparency layer) so the brush strokes fade in and out more.
if ( !mHard )
	CGContextSetAlpha(bitmapContext, 0.5);
CGContextBeginTransparencyLayer(bitmapContext, nil);

mHard works because making the brush tip 50% transparent means the very edges of the line are no more than 50% opaque, which gives them a softer look.

Now that we’re inside the transparency layer, we want to start setting up the layer for drawing. The first thing to do is set the color, using mColor.

// I like a little color in my brushes
CGContextSetFillColorWithColor(bitmapContext, mColor);

That’s pretty self explanatory, but the next part isn’t. We want to handle the softness of the brush edges, which are specified in mSoftness as a percentage. The general idea is to “terrace” the shape at different transparency levels. So at the outer edges of the brush, we draw the shape at full size, but almost fully transparent. As we move towards the center of the brush, the shape should be drawn more and more opaque, at smaller and smaller sizes.

The mSoftness variable determines how soon we reach fully opaque as we draw from the outside in. At 0.0 mSoftness, we’re fully opaque at the outside radius. At 1.0 mSoftness everything but the very center pixel is somewhat transparent.

Since we’re working from the outside in, we know we’re going to start at the outside radius, but we need to compute at what radius the shape becomes fully opaque (after it’s fully opaque, it doesn’t make sense to keep drawing).

// The way we achieve "softness" on the edges of the brush is to draw
//	the shape full size with some transparency, then keep drawing the shape
//	at smaller sizes with the same transparency level. Thus, the center
//	builds up and is darker, while edges remain partially transparent.

// First, based on the softness setting, determine the radius of the fully
//	opaque pixels.
int innerRadius = (int)ceil(mSoftness * (0.5 - mRadius) + mRadius);
int outerRadius = (int)ceil(mRadius);
int i = 0;

Here innerRadius is the radius at which the brush is fully opaque. outerRadius is always the same as mRadius, but we cast it to an int so we can use it in a for loop. i is just the loop counter that I declared here because stupid C won’t let me declare it in the for loop initialization statement.

The last thing we do before we go into our loop to render the soft brush edges, is set the alpha. The nice thing is that the alpha channel builds up, so as you repeatedly draw over a transparent shape, it becomes more opaque. Since we’re working from the outside in, we only have to set the alpha once, outside the loop, and the brush will automatically become more opaque as we continue drawing.

// The alpha level is always proportial to the difference between the inner, opaque
//	radius and the outer, transparent radius.
float alphaStep = 1.0 / (outerRadius - innerRadius + 1);

// Since we're drawing shape on top of shape, we only need to set the alpha once
CGContextSetAlpha(bitmapContext, alphaStep);

Next is our edges loop, which also handles the case of mSoftness = 0.0.

for (i = outerRadius; i >= innerRadius; --i) {
	CGContextSaveGState(bitmapContext);

As you can see, we’re just working from the outside in.

Now that we’re inside the edges loop, we want to center and scale the context so the image shows up at the right location with the right size.

	// First, center the shape onto the context.
	CGContextTranslateCTM(bitmapContext, outerRadius - i, outerRadius - i);

	// Second, scale the the brush shape, such that each successive iteration
	//	is two pixels smaller in width and height than the previous iteration.
	float scale = (2.0 * (float)i) / (2.0 * (float)outerRadius);
	CGContextScaleCTM(bitmapContext, scale, scale);

The centering is pretty self explanatory. Since we’re shrinking the radius one pixel at a time, just move the origin in by the number of times we’ve been through the loop. Scaling works the same way, except we have to remember that we were dealing with the radius, so we need to double it to get the diameter.

Now that we have the current iteration of the edges loop centered and scaled, we just have to fill the shape specified in mShape.

	// Finally, actually add the path and fill it
	CGContextAddPath(bitmapContext, mShape);
	CGContextEOFillPath(bitmapContext);

The last final bit is to just restore the graphics state for the next loop iteration:

	CGContextRestoreGState(bitmapContext);
}

We’re done rendering the brush tip into the transparency layer, so it’s time to end the transparency layer and composite it back onto our bitmap context at the alpha level determined by mHard:

// We're done drawing, composite the tip onto the context using whatever
//	alpha we had set up before BeginTransparencyLayer.
CGContextEndTransparencyLayer(bitmapContext);

The rest of the function is housekeeping. We convert the bitmap context into a CGImageRef, free up the bitmap context, and return the image:

// Create the brush tip image from our bitmap context
CGImageRef image = CGBitmapContextCreateImage(bitmapContext);

// Free up the offscreen bitmap
[self disposeBitmapContext:bitmapContext];

return image;

Like it’s counterpart, disposeBitmapContext isn’t that interesting (it just frees up the bitmap context), so I’ll skip it here.

And that covers the creating of the brush’s image, which is the most interesting part of the Brush class. It is pretty straight forward, except for maybe the render of the “soft” edges.

Handling mouse events

The remainder of the Brush class just takes input from the user (via the CanvasView class) and translates it into primitives for the Canvas class. These enter through methods for mouse down, mouse dragged, and mouse up.

The first method that is invoked during user interaction is mouseDown:inView:onCanvas:. This function needs to initialize the tracking data, and then ask the Canvas to render the first stamp.

- (void) mouseDown:(NSEvent *)theEvent inView:(NSView *)view onCanvas:(Canvas *)canvas
{
	// Translate the event point location into a canvas point
	NSPoint currentPoint = [self canvasLocation:theEvent view:view];

	// Initialize all the tracking information. This includes creating an image
	//	of the brush tip
	mMask = [self createShapeImage];
	mLastPoint = currentPoint;
	mLeftOverDistance = 0.0;

	// Since this is a mouse down, we want to stamp the brush's image not matter
	//	what.
	[canvas stampMask:mMask at:currentPoint];

	// This isn't very efficient, but we need to tell the view to redraw. A better
	//	version would have the canvas itself to generate an invalidate for the view
	//	(since it knows exactly where the bits changed).
	[view setNeedsDisplay:YES];
}

The first thing we do in mouseDown:, and all the mouse functions, is convert the mouse location into something relative to the canvas. We use a helper function, canvasLocation:view:, to do this, but in a real system, the CanvasView would do the conversion for us, since we shouldn’t know details about how the Canvas is located in the CanvasView. However, I didn’t want to replicate most of the information in NSEvent just so it could be done that way.

Since we’re in the mouseDown:, we need to initialize the tracking data, which includes the brush tip image (calling createShapeImage, which we just covered), remembering the current point for the next time we’re called, and initializing the left over distance from the last time we asked the Canvas to render a line.

The mouseDown: is also unique in that we don’t ask the Canvas to render a line, but instead, render a single point. If you were paying attention during stampMask:from:to:leftOverDistance: function, you’ll note that it never stamps at the start point. So we have to do that manually now, which is also good because it gives the user immediate feedback as soon as the mouse goes down.

Finally, we have to tell the view to refresh itself. Here we just tell it to redraw the entire view, which isn’t very efficient. In a real system, the Canvas would determine the bounds that changed, inform the CanvasView in Canvas coordinates, the CanvasView would convert the Canvas coordinates to view coordinates, and invalidate that part of the view. i.e. The tool wouldn’t be involved in the invalidation loop.

After we kick off the tracking loop, we’ll start getting mouseDragged: messages. This function has a simple function: tell the Canvas to draw a line from the last point we got, to the current point.

- (void) mouseDragged:(NSEvent *)theEvent inView:(NSView *)view onCanvas:(Canvas *)canvas
{
	// Translate the event point location into a canvas point
	NSPoint currentPoint = [self canvasLocation:theEvent view:view];

	// Stamp the brush in a line, from the last mouse location to the current one
	[self stampStart:mLastPoint end:currentPoint inView:view onCanvas:canvas];

	// Remember the current point, so that next time we know where to start
	//	the line
	mLastPoint = currentPoint;
}

Like mouseDown: we convert the event point into a canvas point. Then we call a helper function on Brush that will tell the Canvas where to draw the line. Finally, we remember where the current point was.

The last function in the tracking loop is mouseUp: which will end it. It tells the Canvas the last line to draw, and cleans up all the tracking information.

- (void) mouseUp:(NSEvent *)theEvent inView:(NSView *)view onCanvas:(Canvas *)canvas
{
	// Translate the event point location into a canvas point
	NSPoint currentPoint = [self canvasLocation:theEvent view:view];

	// Stamp the brush in a line, from the last mouse location to the current one
	[self stampStart:mLastPoint end:currentPoint inView:view onCanvas:canvas];

	// This is a mouse up, so we are done tracking. Use this opportunity to clean
	//	up all the tracking information, including the brush tip image.
	CGImageRelease(mMask);
	mMask = nil;
	mLastPoint = NSZeroPoint;
	mLeftOverDistance = 0.0;
}

The first part of mouseUp: is identical to mouseDragged:, so I’ll skip the explanation. The last part simply does clean up: freeing the brush tip image, and resetting the last point and left over distance for the line rendering.

Helper functions for tracking

The only functions left for the tracking is a couple of helper functions that mouseDown:, mouseDragged:, and mouseUp: call.

The first one is canvasLocation:view: which we called earlier to convert an NSEvent mouse location into a point relative to the Canvas:

- (NSPoint) canvasLocation:(NSEvent *)theEvent view:(NSView *)view
{
	// Currently we assume that the NSView here is a CanvasView, which means
	//	that the view is not scaled or offset. i.e. There is a one to one
	//	correlation between the view coordinates and the canvas coordinates.
	NSPoint eventLocation = [theEvent locationInWindow];
	return [view convertPoint:eventLocation fromView:nil];
}

This function simply converts the mouse event location into a point relative to the view. It makes the assumption that the canvas is positioned at the origin of the view and is not scaled. As stated before, in a real system this would be handled in the CanvasView, and not here.

Finally, the last helper function used in tracking is stampStart:end:inView:onCanvas:, which simply tells the Canvas where to draw a line.

- (void) stampStart:(NSPoint)startPoint end:(NSPoint)endPoint inView:(NSView *)view onCanvas:(Canvas *)canvas
{
	// We need to ask the canvas to draw a line using the brush. Keep track
	//	of the distance left over that we didn't draw this time (so we draw
	//	it next time).
	mLeftOverDistance = [canvas stampMask:mMask from:startPoint to:endPoint leftOverDistance:mLeftOverDistance];

	// This isn't very efficient, but we need to tell the view to redraw. A better
	//	version would have the canvas itself to generate an invalidate for the view
	//	(since it knows exactly where the bits changed).
	[view setNeedsDisplay:YES];
}

We wrap the call to the Canvas up here for simplification reasons. We pass in and maintain the mLeftOverDistance member variable, as well as the brush image in mMask. Also, we do our inefficient view invalidation so that the line shows up on screen.

That’s the complete Brush class. As review, it has two main functions:

  1. Create the brush tip image that it will pass to the Canvas to use to stamp with.
  2. Tracking the user’s mouse, and tell the Canvas where to draw lines.

Conclusion

Hopefully this has been a fun and interesting tutorial for you. If not, it turns out I still enjoyed it anyway.

There’s still lots of things that could be improved on in the sample code, which include, but are not limited to:

  • Implementing texture support
  • Modifying the code to handle non-square brushes
  • Implementing some sort of UI to configure the brush
  • Implementing some sort of UI feedback for the brush size and shape
  • Implementing tablet support, such as pressure, tilt, and rotation
  • Handling velocity

So if you’re looking for things to play around with in the sample code, there are some ideas.

Download Sample Code