July 15th , 2009 : Beta 6 version is now available for download.
Cocoa and Quartz provide some powerful fundamental tools for doing drawing and graphics, but they are very low level. Contrast this with Cocoa's NSTextView system - it's almost a complete word processor. Thus Cocoa's approach to different developers' needs vary a lot from low-level expert tools to almost writing your app for you.
DrawKit attempts to do for drawing applications what Cocoa's text system does for text applications. It provides a set of interrelated classes which implement a complete vector-drawing framework, while trying to maximise flexibility and remaining agnostic about the final end use of the application. To this end, it provides:
*requires general polygon clipper (GPC) library which has licensing terms different from DrawKit, so must be downloaded separately.
† uses potrace, an open-source tracer. At the time of writing licensing issues have not been examined; may require a separate download.
DrawKit is comprehensive, so you won't be able to learn it all in half an hour. However, it will work more or less out of the box. Taking its cue from the text system, if you simply create a DKDrawingView in Interface Builder, and do nothing else, you get a complete fully-working drawing system! However, it's more usual and gives you much greater flexibility to build the back-end yourself and attach views to it. Usually this will be in the form of a custom document class which owns a DKDrawing data object. To this you add layers, and to the layers you add drawable objects, which is usually done interactively by letting the user choose a drawing tool and clicking and dragging in the drawing.
What the draw kit doesn't provide is a user interface, beyond direct manipulation of objects with the mouse. It provides lots of standard action procedures however, so accessing its features using menus is trivial. It also provides lots of notifications so hooking up to all kinds of controllers for making inspectors and other interfaces is straightforward. The DrawKit download includes a complete demo application that acts as both a testbed for DrawKit's features and provides a developer-oriented user interface (example shown right). You can make use of this code if you wish, or just examine it to see how a user interface can be hooked up. The core of DK makes no assumptions about its user interface - so while a classic "photoshop" like UI is easily implemented, there is no reason why something completely different and more up to date could not be created.
DrawKit is the basis on which Ortelius is built, and also some other more long-term projects in the works.
The vector objects themselves fall into two basic types - shapes and paths. A shape is a closed path such as a rectangle, circle, etc. and is defined by its bounding rectangle and rotation angle. A path is a (usually) open path such as a line or curve, and is defined by its control points. However, there is some crossover between them - for example an irregular polygon is usually created as a path whereas a regular polygon is created as a shape. Each type interconverts to the other.
A standard built-in subtype of shape is one that can recalculate its path when its size changes, rather than simply scaling a fixed path to fit. This is used for simple shapes such as a round-cornered rectangle where the corner radii should remain constant, but also opens up the powerful ability for a shape to be completely altered according to its size. For example, a CAD application might want to draw a bolt with a fixed head size but with a different length of thread as the user extends it. And of course, any type can be subclassed for really unusual types of object behaviour or editing methods.
Because shapes can be converted to paths and vice versa, editing can be done at any level. At the shape level, a shape is simply resized and rotated by dragging its "knobs". At the path level, the actual control points along the path have draggable knobs which change the curves. A text object can be edited simply by double-clicking and typing - but convert it to a path to change the shapes of the letters individually, or to a basic shape to allow you to scale and stretch the text how you like (a standard text shape rewraps the text as you resize the object). You can also convert any stroke to its outline.
DrawKit does not limit you to the set of shapes it provides by default. It includes a "shape factory" that can deliver paths on demand, either created using code you supply or by simply storing any path you give it, which could come from anywhere - a path you have defined in the drawing for example, or a text glyph. You can create a tool to draw any shape in the factory, so you can extend the repertoire of drawing tools to suit your application - you can even add tools on the fly at runtime if you want. The standard shapes/tools set consists of: Rects, Round-cornered rects, Ovals, Rings, Regular polygons from 3 to n sides, Irregular polygons, Lines, Bezier paths, Freehand bezier paths, Text block, Speech balloon, Arcs and Wedges.
A powerful and key part of the architecture is the separation of a vector object's geometry from its appearance. Appearance is handled by a class called DKStyle, which is a tree of graphical attributes and drawing behaviours that can be attached to vector objects. Objects can share styles if this makes sense for your app, so changing the style alters the appearance of all objects sharing that style. Alternatively a 1:1 relationship between objects and styles would allow your app to adopt the more conventional vector drawing paradigm. Styles go well beyond the stroke and fill of a path, as any number of components are supported. Styles define both what is drawn and in what order. For example the path shown here has three strokes with differing dash attributes, widths and colours. Custom style attributes are also easy to make - for example you can define a custom style attribute that will extract a piece of user data from the object and display it, perhaps as a label attached to the object. The possibilities are endless, because the architecture is totally flexible and open-ended.
The standard style components include: Strokes, Fills (both of which support gradients and shadows as well as flat colours), Pattern fill, Path decorator (see below), Text Labeler (which provides text on a path as well as block text), Hatching, Core Image effect group, plain group (provides no rendering functionality but allows elements to be grouped together and managed as a set), Arrowed strokes (including dimensioning lines), Zig-zag fills and strokes.
The DKFillPattern is also interesting. It's an attribute of a style, so can be used with any object. What it does is allow you to specify any path, PDF or image as the repeating "motif" of the pattern. This is separate from Quartz patterns, which are also supported. The advantage of DrawKit's fill patterns is that they give you a number of additional placement options above the standard Quartz tiling patterns - the degree of control over the pattern is very high. A similar class called DKPathDecorator provides a similar functionality for strokes - any existing shape, PDF or image can be arrayed along a path to give a wide range of spectacular effects. And because in both cases you can use any existing shape as the pattern or stroke element, you can effectively nest these effects one inside another in any combination. The only limit is your creativity!
Core Image effects are supported as an attribute of a style. It is implemented as a style sub-group, so any style elements placed in the Core Image group will have the effect applied to them. CI effects are inherently pixel-level operations, so the result is applied at rendering time - objects remain fully editable with effects applied.
Every object supports a completely open meta-data dictionary that you can use to attach any arbitrary data to any object. This means that simply to associate an object with some data requires no subclassing, but it goes further than that. Style elements can be created that pull out meta data and make use of this to control rendering. This would allow you to implement a very flexible system with very few classes - while a style can be shared among many objects, each individual attachment to an object could make use of custom information from the metadata. The built-in renderer for text labels is already able to use a standard metadata item for the content of the text in this way. Of course metadata has many more uses than this - it's yours to do with as you see fit in your application.
The standard selection paradigm is built-in to the main vector object layer class. This supports a list of the objects that are "selected", and the usual user interactions work as expected - drag a "marquee" to select objects it touches, click, shift-click and command-click all work as expected. When an object is asked to draw itself, its selection state is passed along from the owning layer. Most objects simply hand off this to the ancestor class to draw the selection "knobs", but you can intercept this (as with all things) to customise it your way. The selection knobs themselves are handled by another helper class, which can be customised or overridden wholesale to tailor your selection appearance for the entire application. Each type of object is free to define its knobs any way it likes, and to do what it likes in response when the user drags it. While the standard shapes and path stick to established conventions in this regard, there is nothing to stop you making a subclass to do anything you like. Go wild!
Another advantage to DrawKit's design compared to some others is that a shape's selection rotates with the object, so it remains fully scalable at any orientation. Many other programs reset the selection after a rotation so distortion-free scaling is no longer possible. In DrawKit, this "resetting" is possible as an explicit operation, but is not done by default.
The tool model is very simple, yet powerful. In general, most vector objects know how to edit themselves. When the user clicks on an object's knob, the object is responsible for detecting that hit and assigning it a unique "partcode". Partcodes are mostly private to the object. By the time mouse events are passed down to the object, along with the private partcode, the object will know what to do with the event - dragging a knob will do the right thing according to its logic. This frees the layer from having to do a lot of special casing according to the selected tool. The case of creating a new object is hardly any different - it's just editing starting from nothing. Thus the tool itself is little more than a factory class that generates a specific type of object on demand. From then on, the object itself knows what to do - it proceeds as for editing. This makes extending the drawing system to provide a range of new drawing tools very easy.
In addition, the tool protocol allows you to create tool subclasses that do things other than create objects - for example a zoom tool or a colour sampling tool. A standard tool for adding and deleting points on a path is provided.
Drawings are viewed and manipulated through a DKDrawingView which you will arrange to make in a window, usually within a scrollview. This view is able to be the first responder. Within the drawing itself, one layer can be nominated at a time to be the "active layer" - the one that commands and mouse clicks are directed to. You can set the active layer through a suitable user interface of your own devising (a table can work well), and also allow mouse clicks to automatically switch layers according to what the user clicked. Layers are also responders, and DKDrawingView automatically forwards any commands it doesn't handle to the active layer. This allows you to implement actions at the layer level where that makes sense. Menu items "just work", including dimming when the relevant layer isn't active. However, it doesn't stop there. Vector objects themselves take this one step further, so that the active layer can forward commands to the selected object. This allows you to implement commands right down at the object level where it makes sense to do so. Thus menu commands automatically reflect the context as objects are selected and deselected in different layers, as they should. It also makes extending and customizing objects really easy, because you don't have to worry about what sort of layer they are owned by, or any of the hierarchy above.
Undo just works. All changes to the states of objects and styles are undoable. Selection changes are optionally undoable - some apps may prefer not to treat selection changes as a real undoable change. The undo manager is usually "borrowed" from the document that the drawing is owned by (you have to set this up), so changes in the drawing automatically dirty the document.
Unusually, you can attach any number of views (or more correctly, their associated controllers) to a DKDrawing. All views display the same drawing, but of course can display it in different ways - different zoom scales for example. Views can be in the same or different windows. Most apps will probably want to stick to one view per drawing, but the flexibility is there. Any change you make in any view is automatically updated in all views, in real time. Views also automatically maintain the rulers in the enclosing scrollview, if there is one. This includes setting and moving ruler markers to show the position of the selected objects - this all just works without any work on the part of the objects themselves. If you subclass objects to customise their behaviour, you should find that all the standard stuff such as selection, grid snapping, constrain, and rulers just works.
Much of DrawKit's functionality is implemented as categories on standard Cocoa classes such as NSBezierPath. Thus even if DrawKit as a whole may not be what you're looking for, you may be able to find a use for some of its lower-level functionality. For example, drawing text on a path is as simple as calling [myBezierPath drawTextOnPath:myAttributedString yOffset:0]; As far as possible classes are designed to avoid to much dependency on other parts of Drawkit, and may be used "standalone" in many situations. A good example is GCGradient, which can be used to provide a wide range of gradient fills independently of DrawKit. Developers are free to use any parts of DrawKit they want (with due credit, of course).
DrawKit is being developed as part of the Ortelius development, but constructed in such a way as not tied to that application alone. In fact there is another key app in the pipeline with a very different end use and feature set that is based on the same engine. Thus as many dependencies as possible have been kept out of DK, and so it will form an ideal basis for any vector drawing application that needs the usual (and many unusual) features. By using DK, you can save yourself hundreds of hours of work in setting up a usable and functional architecture on which to build your app's unique features.
So what about open source efforts such as Inkscape? While highly laudable, these code bases represent a fairly high barrier to entry. They are complex and hard to understand quickly, even for experienced developers. For the end user, Inkscape is a capable and worthwhile drawing program, though its X11 interface will deter many Mac users. However for developers, if you need something as a base on which to build, it's very hard to quickly pull classes and code out of the Inkscape base and start building. The Cocoa "way" is meant to be easier than that - simple objects that can be understood with a few minutes' study. Objects that can be used in isolation if need be without the whole infrastructure being needed to support it. That is also an important aim of this effort - something that fits with the spirit of Cocoa as we know and love it. This will be seen by some open source devotees as reinventing the wheel, as well as propagating a non-portable, proprietary technology. However, the goal here is simply making building blocks towards usable, cool apps for the Mac. This isn't to knock what open source is all about; but here the focus is different and the code design entirely reflects that.
If you are planning to use DrawKit in your own Cocoa applications, you will probably want to join the mailing list. This is the main way to get help, technical assistance or just to chat about DK-related issues. To join the list, please go to the main download page to subscribe. The list is active from June 21, 2007. Announcements regarding availability and status of Drawkit will be made both on this page and on the mailing list.
If DrawKit interests you and you feel you might be able to contribute, please contact me. The development model is open source, but until a 1.0 release is made, it will not be fully public open source, due to the necessary changes as the architecture matures. Development is using Xcode 3.1 on Leopard and a svn repository. Currently there is no detailed API documentation (I am also keen to hear from anyone able to assist with the documentation effort). On the programming side, you'll need to be familiar with Cocoa and its idioms, and able to write well-structured, solid, maintainable code without too much hand-holding. Help will obviously be freely on tap with DrawKit itself, but if you need to ask how to properly retain and release objects, you are not experienced enough; wanabees need not apply.
© 2006-2008 Graham Cox