Quartz 2D is the advanced drawing API of both iOS and Mac OS X. However, it is missing direct support for drawing inner shadows. We can use some clever tricks to draw inner (inset) shadows that I uncovered while investigating how PaintCode’s generated inner shadow code works. The method involves some cunning use of some less known Quartz features.
Firstly, here’s a code snippet which draws an inset shadow around a path:
Let's try and go through the code to better understand how it works. The two key bits are the usage of transparency layers and source out blend mode (kCGBlendModeSourceOut).
Quartz uses standard compositing that allows you to choose the blend mode. The blend mode determines how pixels are drawn to the context. Each blend mode defines an equation for how to calculate the RGBA of the resulting pixel from the RGBA pixel that is being drawn over (the destination pixel) and the RGBA of the pixel that is being drawn with (the source pixel).
The default mode is kCGBlendModeNormal which uses the following equation (note that pixels are composed of 4 values: red, green, blue and alpha channel):
This can be understood somewhat intuitively: the result will mix in some of the destination pixel’s color if our source is not fully opaque, as we’d expect from normal drawing.
But for our drawing, we’re using kCGBlendModeSourceOut which is defined as:
There is no obvious intuition for this blend mode. The source pixel is multiplied by the inverse of the destination alpha. We’ll use this inversion property to our advantage later. This visualisation might help you understand the blend mode:
The alpha of the overlapping area is 0.8 because the destination alpha is 0.2, and the equation uses 1.0 - Destination Alpha.
To draw our inner shadow, we use Quartz’s normal shadow drawing. This shadow drawing is only useful to us because of the subtlety of how normal shadows in Quartz are drawn.
The shadow is drawn by blurring a version of the content that is colored with the shadow color. The key point to notice for our purposes is that this blur causes the parts of the shadow that are on the inside edge of the shape to have an alpha less than 1. That is, the blur extends inside the shape.
The trick to get our inner shadow is to invert the alpha of a normally drawn shadow on the inner part of the shape. With just the area of the shadow under the shape, it looks like:
Then all we need to do is clip the outside shadow, then we’ll have the inner shadow that we’ve been working towards.
Bringing it all together
We want to invert the alpha of the shadow in the area of our shape. We can do this using kCGBlendModeSourceOut if we simply fill our shape with an opaque version of the shadow color. When the shape is drawn, first the shadow will be composited (using normal blend mode, by definition), then the shape will be composited on top with the currnet blend mode (source out in our case). The blend mode inverts the alpha of the shadow, and since we filled with an opaque version of the shadow color, the color of the pixels will be what we want for our inner shadow.
Finally, in order to use this on opaque backgrounds, we must wrap the entire method with CGContextBeginTransparencyLayer and CGContextEndTransparencyLayer. These functions cause the drawing to be done in a temporary transparent space, before being composited normally. If we didn’t do this, then our alpha inversion trick wouldn’t work if the background we were drawing to was not transparent. Here’s a diagram of the whole procedure:
That completes the full process and the technique can be used on both iOS and OS X (in your UIView / NSView / CALayer). Turns out, Quartz does have inner shadow support after all!
If you found this explanation useful or if you have any questions, I’d love to hear them! I'm @CJoEmery on Twitter.