How to draw Beads

by Peter Hosey


Preface

I was an entrant in the zeroth Iron Coder. My entry is named “Beads”; it hangs a shellacked string of beads onto the frontmost window.

I'm very proud of the look of the beads. I'm going to tell you how I created them, which translates to how you can draw them yourself in your own app. (Aside from ganking my code, which is BSD-licensed. That's the other way to have them in your app.)

Each bead is drawn at a diameter of 1 cm (a little over 28+13 pt). They are drawn into the overlay window at a diameter of 16 pt.


Step 1: Draw a circle

The basic shape of the bead is a colored circle, so that's what we start with:

Plain red circle.Plain red circle.

That's fine, but it's just a flat circle. Beads are spheres. Our bead needs to look like it has some depth.


Step 2: Light and shadow

The Aqua Human Interface Guidelines (specific page: Icon Perspectives and Materials) say “All Aqua interface elements have a common light source from directly above…”, so we need a highlight on top and a shadow on the bottom.

I use a CGShading to achieve this affect. The shading function takes a value t ranging from 0 (bottom) to 1 (top). The first step in this function is:

sin((t - 0.5) π) 0.5

This value is a saturation delta: it is added to all of the components (R, G, B) of the color of the bead. We're making a red bead, so these components start at (1, 1, 0) and become:

I don't know what inspired me to use a sine wave, but that's what I used, and it works well. Here's the result:

Shaded circle; highlight on top, pure red in the middle, shadow on the bottom.Same thing, but 16-by-16 rather than 28-by-28.


Step 3: The sheen

I knew before I started coding the beads that just a gradient wouldn't cut it. I needed to fake a reflection on top of the beads. So I wrote a PostScript file (left it on my RAM disk; sorry) that printed out two rulers (one per axis), each 5 cm long and divided into tenths. I then printed this out and drew in pencil what I wanted the sheen to look like. Here it is, the first draft:

First draft of the bead sheen.

I soon found out that I wouldn't be able to work out a Bézier path for this one. I'm not good with Bézier curves. I soon came up with a simpler sheen, used in the submission, that works well and is much simpler to draw:

Second/final draft of the bead sheen.

The top curve of the sheen begins at 35 from the bottom and peaks at 910. The bottom curve of the sheen peaks at 710. The left corner is at 110 and the right corner is at 910.

To draw it in PostScript:

0.9 0.6 moveto 0.5 1.0 0.1 0.6 curveto 0.5 0.77 0.9 0.6 curveto closepath sheenColor setfill fill

The above is a back-port to PostScript from the Quartz source in the submission.

And here is the product of all that hard work:

Final bead, with sheen.Final bead, with sheen, at 16 points.
Final bead, with sheen.


Outtake: The beads you didn't see

The beads that you see in the submission look great. But you probably didn't notice that they contain a mistake. I didn't.

You see, my original intent (which you might notice if you look at the Beads 1.0 source code) was to draw a border (called a stroke) within the circumference of the circle. Unfortunately, I messed up. Here's the submitted code:

CGContextScaleCTM(pdfContext, BEAD_SIZE, BEAD_SIZE); Fill the bead. CGContextSaveGState(pdfContext); [SNIP] CGContextClip(pdfContext); CGContextSetFillColorWithColor(pdfContext, beadColor); if(fillShading) CGContextDrawShading(pdfContext, fillShading); else CGContextFillRect(pdfContext, (struct CGRect){ ZERO_CGPOINT, mediaBox.size }); CGContextRestoreGState(pdfContext); Draw a sheen on the bead, using two Bézier curves. We move counter-clockwise, of course. CGContextMoveToPoint(pdfContext, 0.9f, 0.6f); Top curve. CGContextAddQuadCurveToPoint(pdfContext, 0.5f, 1.0f, 0.1f, 0.6f); Bottom curve. CGContextAddQuadCurveToPoint(pdfContext, 0.5f, 0.77f, 0.9f, 0.6f); CGContextClosePath(pdfContext); CGContextSetFillColorWithColor(pdfContext, sheenColor); CGContextFillPath(pdfContext); Draw a black outline on the bead. CGContextSetLineWidth(pdfContext, 3.0f); CGContextSetStrokeColorWithColor(pdfContext, blackColor); CGContextStrokeEllipseInRect(pdfContext, mediaBox);

The problems are:

  1. The clip is protected by the gsave/grestore.
  2. The line width is set to 3.
  3. The rect given is the media box.

The clip is necessary to keep the stroke within the circumference of the bead; otherwise, half of the stroke width would fall outside the media box at each of the cardinal points. So the gsave and grestore must go, so that the clip applies to the stroke as well.

But those last two problems are more critical.

The problem is caused by the scale earlier. Because of that, 1 user-space unit = 1 bead, rather than 1 pt. So our line width is 3 beads across, and the rect is about 28 by 28 beads (remember earlier that BEAD_SIZE = 1 cm = >28 pt).

Diagram showing the 28:1 size ratio of the stroke to the bead.

The stroke should only be 1 bead in diameter, not 28.

The fix is threefold:

  1. Let the clipping path apply to all the steps, including the stroke.
  2. Change the line width to 2 / BEAD_SIZE. I'd only intended to have a 1-pt stroke. The clipping path cuts the line width in half, so a 2-pt line width gets me a 1-pt border within the clip.
  3. Instead of passing the media box (whose size is about 28 pt on both axes), pass { 1, 1 } for the size.

These fixes have been rolled into Beads 1.1. But I didn't even notice that the code was wrong until I started writing this article. The beads do look great without the stroke.

Nonetheless, here's the director's cut of the beads — how they were supposed to look. On the left are the submitted (strokeless) beads; on the right are the director's cut (stroked) beads:

Plain red circle, no stroke, 28-pt version.Plain red circle, no stroke, 16-pt version.
Red bead before sheen, no stroke, 28-pt version.Red bead before sheen, no stroke, 16-pt version.
Red bead with sheen, no stroke, 28-pt version.Red bead with sheen, no stroke, 16-pt version.
Plain red circle, with stroke, 28-pt version.Plain red circle, with stroke, 16-pt version.
Red bead before sheen, with stroke, 28-pt version.Red bead before sheen, with stroke, 16-pt version.
Red bead with sheen and stroke, 28-pt version.Red bead with sheen and stroke, 16-pt version.
Plain red circle, no stroke, 28-pt version.Plain red circle, no stroke, 16-pt version.
Red bead before sheen, no stroke, 28-pt version.Red bead before sheen, no stroke, 16-pt version.
Red bead with sheen, no stroke, 28-pt version.Red bead with sheen, no stroke, 16-pt version.
Plain red circle, with stroke, 28-pt version.Plain red circle, with stroke, 16-pt version.
Red bead before sheen, with stroke, 28-pt version.Red bead before sheen, with stroke, 16-pt version.
Red bead with sheen and stroke, 28-pt version.Red bead with sheen and stroke, 16-pt version.

Version 1.1 has been digitally remastered to include the stroke.


DVD Extras

The making of the Beads How-to

All of the examples shown above were created using a program called mkbead, which uses code excerpted from Beads. I commented out parts of the program to generate each step of the graphic. The output is a PDF, of course (this is also what Beads generates internally).

mkbead.tbz
Objective-C source code and an Xcode 2.2 project, in a bzip2ed tarball.
128-bit FNV1 hash: 6f5d1f87a1c5b112dbb3cd55fd6cba34
MD5 hash: 9f55b5db796939c24c4cd4692d2cae84
SHA-1 hash: 0ef0d2db2f97c0f10695d16965c7a3741ea617c5

I converted the PDFs to PNGs, with alpha channels using Preview. Naturally, I then ran pngcrush over them to conserve bandwidth.

The backgrounds you see above are provided by CSS. For example, in the director's-cut section above, both columns of images actually use the same image files. The style-sheet in this page is what gives the columns different backgrounds.


2007-03-18 http://boredzo.org/beads/howto
Valid XHTML 1.0! Valid CSS!