Drawing with Cappuccino

Well displaying pictures in your web application is nice, but cappuccino has more to offer. You can draw lines, rects, bezier paths in all colors (and using alpha channel too).

Using CPColor to choose your color

cappuccino comes with some black, white and some shade of gray colors.

var blackColor = [CPColor blackColor]; var darkGrayColor = [CPColor darkGrayColor]; var grayColor = [CPColor grayColor]; var lightGrayColor = [CPColor lightGrayColor]; var whiteColor = [CPColor whiteColor];

You have also some predefined colors.

var blueColor = [CPColor blueColor]; var greenColor = [CPColor greenColor]; var redColor = [CPColor redColor]; var yellowColor = [CPColor yellowColor]; var shadowColor = [CPColor shadowColor];

You can create a color in the RGB color space, with an alpha value too. Each component should be between the range of 0.0 to 1.0. For the alpha component, a value of 1.0 is opaque, and 0.0 means completely transparent

var calibratedRgbColor = [CPColor colorWithCalibratedRed:0.0 / 255.0 green:236.0 / 255.0 blue:250.0 / 255.0 alpha:1.0]; var calibratedWhite = [CPColor colorWithCalibratedWhite:1.0 alpha:1.0];

If you prefer to work in the HSB color space, it is also available. The hue vale should be comprise between 0.0 and 360.0, the saturation between 0.0 and 100.0 and the brightness in the range 0.0 to 100.0.

var hsbColor = [CPColor colorWithHue:360.0 saturation:100.0 brightness:100.0];

You can also create an RGB color from a hexadecimal string. For example, the a string of "FFFFFF" would return a white CPColor. "FF0000" would return a pure red, "00FF00" would return a pure blue, and "0000FF" would return a pure green.

var rgbColor = [CPColor colorWithHexString:@"0000FF"];

The same applies to valid CSS RGB string. They can be used to create a CPColor. For example: "rgb(32,64,129)" or "rgb(32,64,129,255)" if you want an alpha channel.

var cssColor = [CPColor colorWithCSSString:@"rgb(32,64,129)"];

More surprising, you can create a color using a tile pattern (coming from an image).

var anImage = [[CPImage alloc] initWithContentsOfFile:[[CPBundle mainBundle] pathForResource:@"pattern1.png"]]; var patternColor = [CPColor colorWithPatternImage:anImage];

If you want to modify the alpha channel of one of your color, you can do it like this:

var rgbColor = [CPColor colorWithHexString:@"0000FF"]; var color = [rgbColor colorWithAlphaComponent:0.5]

If you want to give your user some kind of feedback, it should be possible using these methods:

var components= [color components]; var hsbComponents = [color hsbComponents]; var cssString = [color cssString]; var hexString = [color hexString];

Drawing directly in a CPView

So what, I want to draw something ! Just subclass an CPView and implement the drawRect: method. First, you need to get the current graphic context. Afterwards you will use one of the Core Graphic functions (their name start with CGContext). The key idea is to create path (it can be lines, rects, polygons, bezier curves) and either stroke (CGContextStrokePath) or fill the path (CGContextFillPath). You will reuse your brand new expertise in colors with the functions CGContextSetFillColor or CGContextSetStrokeColor.

To create a path, you use the pair of functions CGContextBeginPath and CGContextClosePath. Of course in the between, you will add the lines, rects or curve points.

@implementation MyView : CPView { } - (void)drawRect:(CPRect)aRect { var bounds = [self bounds]; var context = [[CPGraphicsContext currentContext] graphicsPort]; CGContextBeginPath(context); CGContextMoveToPoint(context, 10.0, 10.0); CGContextAddLineToPoint(context, 10.0, 40.0); CGContextAddLineToPoint(context, 40.0, 40.0); CGContextAddLineToPoint(context, 20.0, 30.0); CGContextClosePath(context); CGContextSetFillColor(context, [CPColor yellowColor]); CGContextFillPath(context); CGContextSetStrokeColor(context, [CPColor greenColor]); CGContextStrokePath(context); }

You have a lot of primitive available, just browse the html documentation or the source code. Some examples can be found below.

// Ellipse CGContextFillEllipseInRect(context, aRect); CGContextStrokeEllipseInRect(context, aRect); // Rect CGContextFillRect(context, CGRectInset(aRect, 10.0, 10.0)); CGContextStrokeRect(context, CGRectInset(CGRectIntegral(bounds), 0.5, 0.5)); // Multiples rects CGContextBeginPath(context); var rect1 = CGRectMake(50, 50, 20, 20); var rect2 = CGRectMake(100, 100, 40, 40); var rects = [CPArray arrayWithObjects:rect1, rect2, nil]; CGContextAddRects(context, rects, [rects count]); CGContextClosePath(context); CGContextSetFillColor(context, [CPColor colorWithCalibratedRed:0.0 green:0.0 blue:0.8 alpha:0.4]); CGContextFillPath(context);

Drawing in a CPView with some CALayer

Well if you have large amount of information to draw, it can be interesting to use some layers (CALayer) that are optimized for drawing (and for smooth animation).

In order to understand how a CALayer draw itself, you can refer to the source code of the drawInContext: method. As you can see below, if you setup a background color with setBackgroundColor: it will be used to fill the bounds of the layer. If you don't setup a background color, the CALayer will be transparent.

If you attach a delegate to the CALayer that implement the drawLayer:inContext: method, it will be used to draw the layer content. So you have two ways to draw in a CALayer. You can subclass CALayer and override the drawInContext: method or attach a delegate and code the drawLayer:inContext: method.

- (void)drawInContext:(CGContext)aContext { if (_backgroundColor) { CGContextSetFillColor(aContext, _backgroundColor); CGContextFillRect(aContext, _bounds); } if ([_delegate respondsToSelector:@selector(drawLayer:inContext:)]) [_delegate drawLayer:self inContext:aContext]; } - (void)setBackgroundColor:(CPColor)aColor { _backgroundColor = aColor; [self setNeedsDisplay]; }

How to indicate that the drawing should be handled by a layer

Lets start using CALayer. The view instance indicate with setWantsLayer: that it will use a layer to draw itself. We create the layer (_rootLayer) and associate the layer with the view by using setLayer:. We will use the builtin behaviour of the layer to fill its bounds in dark gray (by using setBackgroundColor:). We need to tell the layer to draw itself by using setNeedsDisplay. Thats all, we now have a view full of dark gray.

@implementation MyViewWithLayer : CPView { CALayer _rootLayer; } - (id)initWithFrame:(CGRect)aFrame { self = [super initWithFrame:aFrame]; if (self) { [self setWantsLayer:YES]; _rootLayer = [CALayer layer]; [self setLayer:_rootLayer]; [_rootLayer setBackgroundColor:[CPColor darkGrayColor]]; [_rootLayer setNeedsDisplay]; } return self; }

Drawing in a layer by using its delegate

We can build on this example. We will first use a delegate to draw an ellipse in the layer. The initWithFrame: method is almost the same of the one from previous example. We just just add the setDelegate: call.

[_rootLayer setBackgroundColor:[CPColor grayColor]]; [_rootLayer setDelegate:self];

We have now to implement the method drawLayer:inContext: of the CALayer delegate.

- (void)drawLayer:(CALayer)layer inContext:(CGContext)context { var bounds = [layer bounds]; CGContextSetFillColor(context, [CPColor yellowColor]); CGContextSetStrokeColor(context, [CPColor greenColor]); CGContextFillEllipseInRect(context, bounds); CGContextStrokeEllipseInRect(context, bounds); }

Drawing in a layer by subclassing

If you decide to draw everything yourself, it may more simple to subclass a CALayer and to override its drawInContext: method. The initWithFrame: method will be changed to create our subclass layer.

_rootLayer = [[MyCALayer alloc] init];

So lets implement our MyCALayer class that disable the transparency (we are sure its bound rect will be draw in gray). I am sure that you will find a more valuable thing to do in your implementation.

@implementation MyCALayer : CALayer { } - (void)drawInContext:(CGContext)aContext { var bounds = [self bounds]; CGContextSetFillColor(aContext, [CPColor grayColor]); CGContextFillRect(aContext, bounds); if (_backgroundColor) { CGContextSetFillColor(aContext, _backgroundColor); CGContextFillRect(aContext, CGRectInset(bounds, 10.0, 10.0)); } if ([[self delegate] respondsToSelector:@selector(drawLayer:inContext:)]) [[self delegate] drawLayer:self inContext:aContext]; } }

More examples

Another introduction to drawing can be found in the first part of the scrapbook tutorial. You will have an example of a more complicated view (combining several layers) and an example of a layer delegating it's drawing (the drawing is not done by layer subclassing). The reading of this tutorial should be now easier.

If you'd like to see the complete code listing from the tutorial, you can download it all in a single file: Tutorial-Drawing.zip. The web application is available online: Tutorial Drawing.

Copyright © 2009 - Philippe Laval. Cappuccino and Objective-J are registered Trademarks of 280 North.