Objective-C: Drawing with a texture (chalk)

the Chalk Circle

the Chalk Circle (no, nothing to do with Brecht)

The background:

While putting the finishing touches to my latest iPhone App (more news soon), I needed to draw outlines with a chalk texture as opposed to simply using a colour. Fine, should be easy enough, I thought. Well, it wasn’t so easy to work out after all and a web search, post on Stack Overflow and polling a few friends didn’t help much either. Finally, after wading through numerous drawing API examples and an amount of trial and error, I found a solution – which, once you know it, is actually simple.

The solution:

To save you the search, here’s a example based upon my final solution. I’ve detailed all steps of this, including setting up a simple UIView and the Interface Builder steps needed – skip these if you already know this :

Drawing a circle with a chalk texture:

1. Fire up XCode and create a Window-based Application using the built-in template. Call it “chalkCircle”.

2. XCode will create the classes needed and include the relevant frameworks. Feel free to browse the folder structure and marvel at how much code you’ve already written ;-)

3. We’re going to add a new Class file to the App to take care of the drawing for us. Click with the right mouse button on the Classes folder, choose Add -> New File… and select Objective-C Class from the Cocoa Touch Class templates. Make sure you select UIView in the “Subclass of” dropdown:

Creating a UIView Class

Creating a UIView Class

4. Save the new Class as Circle.m (and ensure that you opt for the .h file to be generated for you by default). We’ll come back to this in a short while…

5. Open the chalkCircleViewController.m file and change the viewDidLoad method to add the following code:

- (void)viewDidLoad {
[super viewDidLoad];
Circle *view = [[Circle alloc] initWithFrame:self.view.frame];
[circleView addSubview:view];
[view release];
}

6. Add the following import and synthesize statements to the file also:

#import "Circle.h"
@implementation chalkCircleViewController
@synthesize circleView;

7. Swap over the the chalkCircleViewController.h file (use a three.finger upwards scroll gesture to do this quickly) and ensure that it looks like the following:

#import <UIKit/UIKit.h>

@interface chalkCircleViewController : UIViewController {
IBOutlet UIImageView *circleView;
}
@property (nonatomic, retain) UIImageView *circleView;

@end

A quick recap of what we’ve done: We’ve created the basic App and added a new Class called Circle to do the actual drawing. We haven’t coded this up yet but have made some preparations for it. Steps 5 and 6 added the code needed to ensure that the Circle class is instantiated and the circle itself is drawn on our default view once it loads. The UIImageView Object circleView is where we’ll be drawing the circle. Step 7 then added the code we need in the header file to link up to the view (using an IBOutlet).

Now let’s create the view in Interface Builder…

8. Double-click on the chalkCircleViewController.xib file found in the Resources folder to open Interface Builder.

9. Drag an Image View Object on to the main View in Interface Builder and then click on the “Image View Connections” tab of the Attribute Inspector panel – the one with the blue circle and white arrow. The Attribute Inspector panel will be opened just to the right of the View itself by default once you’ve opened Interface Builder.

10. To link this Image View Object up to the UIImageView circleView IBOutlet, we simply need to click on “New Referencing Outlet” under the “Image View Connections” tab and drag the line that appears over to the “File’s Owner” icon in the Object Inspector as below:

Linking the UIImageView up to the code

Linking the UIImageView up to the code (click to view)

Upon releasing the mouse a small grey popup will appear with two entries – click circleView to select it. You will now see the following under “Image View Connections”:

The connection is made!

The connection is made!

11. Save the changes you’ve made or it won’t work… (this is not an uncommon mistake!)

12. Right. We now have our view built. Back to the code. The main view contains a UIImageView into which we will draw our circle by creating an instance of the Circle class. Let’s code up the Circle class:

13. The difference between drawing with a coloured fill and drawing with a textured fill is that we need to use a callback to define the fill properties – to identify the image we’re going to use as a fill in this case. To accomplish this, the following code should be inserted at the top of the Circle.m file, just after the “@implementation Circle” line:

const float kPatternWidth = 8;
const float kPatternHeight = 8;

void DrawPatternCellCallback(void *info, CGContextRef cgContext)
{
UIImage *patternImage = [UIImage imageNamed:@"chalk_brush.png"];
CGContextDrawImage(cgContext, CGRectMake(0, 0, kPatternWidth, kPatternHeight), patternImage.CGImage);
}

The two constants will also be used later and represent the actual width and height of the chalk_brush.png image used:

chalk_brush.png

chalk_brush.png

The callback will simply return this image for use in drawing when called.

14. To perform any custom drawing in a UIView you must override the drawRect method. This will then cause the code in that method to be invoked upon drawing the UIView. That’s where we’ll add our code to draw the circle:

- (void)drawRect:(CGRect)rect {
float startDeg = 0; // where to start drawing
float endDeg = 360; // where to stop drawing
int x = self.center.x;
int y = self.center.y;
int radius = (self.bounds.size.width > self.bounds.size.height ? self.bounds.size.height : self.bounds.size.width) / 2 * 0.8;
CGContextRef ctx = UIGraphicsGetCurrentContext();


const CGRect patternBounds = CGRectMake(0, 0, kPatternWidth, kPatternHeight);
const CGPatternCallbacks kPatternCallbacks = {0, DrawPatternCellCallback, NULL};


CGAffineTransform patternTransform = CGAffineTransformIdentity;
CGPatternRef strokePattern = CGPatternCreate(
NULL,
patternBounds,
patternTransform,
kPatternWidth, // horizontal spacing
kPatternHeight,// vertical spacing
kCGPatternTilingNoDistortion,
true,
&kPatternCallbacks);
CGFloat color1[] = {1.0, 1.0, 1.0, 1.0};


CGColorSpaceRef patternSpace = CGColorSpaceCreatePattern(NULL);
CGContextSetStrokeColorSpace(ctx, patternSpace);


CGContextSetStrokePattern(ctx, strokePattern, color1);


CGContextSetLineWidth(ctx, 4.0);


CGContextMoveToPoint(ctx, x, y - radius);
CGContextAddArc(ctx, x, y, radius, (startDeg-90)*M_PI/180.0, (endDeg-90)*M_PI/180.0, 0);
CGContextClosePath(ctx);
CGContextDrawPath(ctx, kCGPathStroke);


CGPatternRelease(strokePattern);
strokePattern = NULL;
CGColorSpaceRelease(patternSpace);
patternSpace = NULL;

}

Let’s go through that code:

We start off by defining a few important variables:

float startDeg = 0; // where to start drawing
float endDeg = 360; // where to stop drawing
int x = self.center.x;
int y = self.center.y;
int radius = (self.bounds.size.width > self.bounds.size.height ? self.bounds.size.height : self.bounds.size.width) / 2 * 0.8;

startDeg and endDeg mark the start and end positions in degrees – and could easily be changed to enable you to draw an arc instead of a complete circle. x and y mark the centre position of the circle and radius is, well, the radius to draw.

CGContextRef ctx = UIGraphicsGetCurrentContext();


const CGRect patternBounds = CGRectMake(0, 0, kPatternWidth, kPatternHeight);


CGAffineTransform patternTransform = CGAffineTransformIdentity;
CGPatternRef strokePattern = CGPatternCreate(
NULL,
patternBounds,
patternTransform,
kPatternWidth, // horizontal spacing
kPatternHeight,// vertical spacing
kCGPatternTilingNoDistortion,
true,
&kPatternCallbacks);
CGFloat color1[] = {1.0, 1.0, 1.0, 1.0};


CGColorSpaceRef patternSpace = CGColorSpaceCreatePattern(NULL);
CGContextSetStrokeColorSpace(ctx, patternSpace);

This code defines the characteristics of the fill pattern, using our callback, and creates the pattern within the current graphics drawing context. The patternTransform variable could be used to define transformations for the pattern but it’s not used here.

You may find that you want to play with the vertical and horizontal spacing of the pattern you use to make it look more realistic, allowing it to overlap in both dimensions. The pattern I’ve used here is not the most realistic chalk effect and the key to making this look as good as possible is really in finding the best pattern image to start with – over to you on that one!

CGFloat color1[] = {1.0, 1.0, 1.0, 1.0};

Simply sets the fill colour to white, with an alpha value of 1 (solid).

Now we’re ready to draw!

CGContextSetStrokePattern(ctx, strokePattern, color1);


CGContextSetLineWidth(ctx, 4.0);


CGContextMoveToPoint(ctx, x, y - radius);
CGContextAddArc(ctx, x, y, radius, (startDeg-90)*M_PI/180.0, (endDeg-90)*M_PI/180.0, 0);
CGContextClosePath(ctx);
CGContextDrawPath(ctx, kCGPathStroke);

This code will set our stroke pattern (as opposed to using CGContextSetRGBStrokeColor to draw using a fill colour) and line width, then draw an arc path from our start point to the end point. That’s it!

All you need to do now is tidy up after yourself to avoid any nasty memory leaks and you’re done:

CGPatternRelease(strokePattern);
strokePattern = NULL;
CGColorSpaceRelease(patternSpace);
patternSpace = NULL;

All sorted.

Given that simply drawing a circle with a coloured fill is pretty easy, this seems like a lot of code just to draw it with a texture. It is, but it’s all needed. As I wrote above though: Once you know how it’s done, it seems fairly simple. You can, of course, also use any other texture to draw with.

You can download the XCode project here and play with it as you wish. I hope it helps you save some time and searching!

As usual, this tutorial and the source are available under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License and a credit is always appreciated.

 

tags: , , , , , , ,