We’ve seen that we can use clipping paths to draw gradients inside rounded rectangles, as in the example to the left. Now, suppose that we want to clip a gradient to a “hollow” shape: Will Core Graphics handle this properly? To my (mild) surprise, the answer is: Yes!
Goal
The goal is to produce the sort of images seen to the right. Unfortunately, it doesn’t seem possible to do this in the intuitive way, by adding shapes to (or subtracting shapes from) the clipping region. Fortunately, it is possible to construct the desired clipping region with a little trick.
Edit: Actually, it is possible to build up a complex clipping region by adding and subtracting simple shapes – a discussion of the matter can be found here.
The Trick
One way to produce a “hollow” clipping region is to place a zero-width break inside the clipping region. Stroked versions of clipping paths built with this technique can be seen to the left. (These paths are a little blurry because they actually run between pixels, and when such paths are stroked with 1-pixel wide brushes, the pixels on either side of the paths are filled in 50%.)
The pixels around the seam look fine. Whether the seam runs between pixels, or through the middle of a pixel, Core Graphics seems to properly calculate that all the relevant pixels near it are 100% visible, and render them appropriately.
Edit: It turns out that this hack is not necessary.
Code
Here’s now you might produce a clipping path for a hollow shape (in this case, a rounded rectangle):
+ (void)setPathToHollowRoundedRect:(CGRect)rect forInset:(NSUInteger)inset withWidth:(NSUInteger)width inContext:(CGContextRef)context
{
// Experimentally determined
static const NSUInteger cornerRadius = 10;
// Unpack size for compactness, find minimum dimension (outer path)
CGFloat ow = rect.size.width;
CGFloat oh = rect.size.height;
CGFloat om = ow<oh?ow:oh;
// Special case: Degenerate rectangles abort this method
if (om <= 0) return;
// Bounds (outer path)
CGFloat ob = rect.origin.y;
CGFloat ot = ob + oh;
CGFloat ol = rect.origin.x;
CGFloat or = ol + ow;
// Adjust radius for inset, and limit it to 1/2 of the rectangle's shortest axis (outer path)
CGFloat oR = (inset<cornerRadius)?(cornerRadius-inset):0;
oR = (oR>0.5*om)?(0.5*om):oR;
// Find minimum dimension of the inner bounding rectangle
CGFloat im = om - 2*width;
// Special case: Degenerate rectangles abort this method
if (im <= 0) return;
// Bounds (inner path)
CGFloat ib = ob + width;
CGFloat it = ot - width;
CGFloat il = ol + width;
CGFloat ir = or - width;
// Adjust inner radius for width
CGFloat iR = (oR>width)?(oR-width):0;
// Define a CW path in the CG co-ordinate system (origin at LL)
CGContextBeginPath(context);
CGContextMoveToPoint(context, (ol+or)/2, ot); // Begin outer loop at TDC
CGContextAddArcToPoint(context, or, ot, or, ob, oR); // UR corner
CGContextAddArcToPoint(context, or, ob, ol, ob, oR); // LR corner
CGContextAddArcToPoint(context, ol, ob, ol, ot, oR); // LL corner
CGContextAddArcToPoint(context, ol, ot, or, ot, oR); // UL corner
CGContextAddLineToPoint(context, (ol+or)/2, ot); // End outer loop at TDC
// Define a CCW path in the CG co-ordinate system (origin at LL)
CGContextAddLineToPoint(context, (il+ir)/2, it); // Begin inner loop at TDC
CGContextAddArcToPoint(context, il, it, il, ib, iR); // UL corner
CGContextAddArcToPoint(context, il, ib, ir, ib, iR); // LL corner
CGContextAddArcToPoint(context, ir, ib, ir, it, iR); // LR corner
CGContextAddArcToPoint(context, ir, it, il, it, iR); // UR corner
CGContextAddLineToPoint(context, (il+ir)/2, it); // End inner loop at TDC
// Connect tail of CCW path to head of CW path
CGContextClosePath(context); // End at (outer) TDC
}