Monday, January 21, 2013

More Objective-C Drawing Context Pleasantries

It's been a little over half a year since I first made my pleasant Objective-C drawing contextpublic, and I haven't been idle. In the process of retrofitting my own code to use MPWDrawingContext and adding more and more graphics (for example, I now do my icons in code), I've discovered a lot about making drawing with code a more pleasant experience.


Blocks seem to be a really wonderful match for a graphics context, and most of the changes involve blocks in some way.

Bracketing operations such as gsave/grestore now have block versions, so the Objective-C block structure reflects the nesting:

    [context ingsave:^(Drawable c ){
        [c translate:@[ @130 ,@140]];
        [c setFont:[context fontWithName:@"ArialMT" size:345]];
        [c setTextPosition:NSMakePoint(0, 0)];
        [c show:@"\u2766"];
This is somewhat more compact than the plain code, which for correctness should also have a @try/@finally block wrapped around the basic drawing so exceptions don't mess up the graphics state stack.
    [context gsave];
    [context translate:@[ @130 ,@140]];
    [context setFont:[context fontWithName:@"ArialMT" size:345]];
    [context setTextPosition:NSMakePoint(0, 0)];
    [context show:@"\u2766"];
    [context grestore];
Similar for drawing shadows:
    [context withShadowOffset:NSMakeSize(0, -8 * scale) blur:12 * scale  color:[context  colorGray:0 alpha: 0.75] draw:^(Drawable c ){
        [[[c setFillColorGray:0.9 alpha:1.0] ellipseInRect:ellipseRect] fill];
Again, this seems a little clearer than having to explicitly set and unset, makes it harder to miss the end of the bracket when moving code around and remains exception-safe.
    [context sethadowOffset:NSMakeSize(0, -8 * scale) blur:12 * scale  color:[context  colorGray:0 alpha: 0.75]];
    [[[context setFillColorGray:0.9 alpha:1.0] ellipseInRect:ellipseRect] fill];
    [context clearShadow];

Stored, delayed and repeated drawing

You can create an object for later drawing by sending the -laterWithSize:(NSSize)size content:(DrawingBlock)commands message. For example, here is a simple diamond shape:
    NSSize diamondSize=NSMakeSize(16,16);
        id diamond = [context laterWithSize:diamondSize
                              content:^(id  context){
            id red = [context colorRed:1.0 green:0.0 blue:0.0 alpha:1.0];
            [context setFillColor:red];
            [[context moveto:diamondSize.width/2 :2] 
				lineto:diamondSize.width-2 :diamondSize.height/2];
            [[context lineto:diamondSize.width/2 :diamondSize.height-2]
				lineto:2 :diamondSize.height/2];
            [[context closepath] fill];
We can now draw this anywhere we want, and at any scale or orientation, using the -drawImage: message.
    [context drawImage:diamond];
You also have layerWitSize:content: and bitmapWithSize:content: messages if you want to specifically use CGLayer or CGImage instead, but using laterWithSize:content: preserves maximum quality, and it will automatically switch to a CGLayer when rendering to a PDF context in order to minimize PDF file size.


I talked about patterns earlier. What I didn't mention then was that this is just the ability to use a stored set of drawing commands (see previous section) as a color:
    [context setColor:diamond];
I am not going to post the comparison to plain CG here, you can read it in the original Apple documentation.

I should note that this currently works for colored patterns, not for uncolored patterns, due to the fact that I haven't yet exposed color spaces. The basic process will be very similar.

Polymorphic object arguments

Path construction and graphics state messages with point arguments are now available in a version that takes a single object argument, in addition to the format with anonymous float arguments (moveto:(float)x :(float)y).
  • moveto:
  • lineto:
  • translate:
  • scale:
The single argument can be an Objective-C array:
    [context moveto:@[ @10, @20]];
Alternatively, any custom object that responds to count and either objectAtIndex:, (float)realAtIndex: or getReals:(float*)buffer length:(int)maxLen can be used. The scale: message can also take a single NSNumber and will treat that as uniform x and y scale.

Linecap parameters

Linecap parameters can now be set using distinct message:
  • setlinecapRound
  • setlinecapButt
  • setlinecapSquare
Having multiple messages rather than a single message with a parameter probably seems odd, but it actually reduces the number of names involved, and these names are nicely scoped to this context. The constant strings or enums that are typically used have global scope and therefore tend to need ugly and hard-to-remember prefixes:
    [context setlinecapRound];
    [context setLinecap: kCGContextLinecapRound];


Another reason to be as purely message-based as possible is that it makes bridging to other languages easier, for example for interactive drawing environments: Creating a badge (youtube).

I've also started experimenting with other outputs, for example creating a version of the same badge composed of CALayer objects using the same drawing commands. Other output should follow, for example web with SVG or HTML 5 Canvas or direct OpenGL textures.

I also want to finally add image processing operations both stand-alone and as chained drawing contexts, as well as getting more complex text layout options in there.

p.s.: now on hacker news

No comments: