I want to revisit Tuesday’s discussion of clipping; when I wrote that post I was laboring under certain misconceptions related to Core Graphics paths, which I would like to discuss. As a consequence of correcting these misconceptions, I can see that Core Graphics clipping is much easier to work with than I had supposed.
Whoops
My basic mistake was to assume that a Core Graphics path was, essentially, a single closed loop. In fact, a path is a collection of line segments and loops; it is essentially a collection of subpaths, where a subpath corresponds to my original conception of an overall path.
In particular, I misunderstood the purpose of these functions:
I thought that CGContextBeginPath
and CGContextClosePath
formed a pair, and that a path was defined by Core Graphics calls made between these bookends. In fact, CGContextMoveToPoint
and CGContextClosePath
form the pair; CGContextMoveToPoint
establishes the first point on a subpath to which CGContextClosePath
will connect, if it is called.
Once this misapprehension was corrected, some other issues related to clipping became clear.
Winding vs. Non-Zero
I had been puzzled by this language in the Core Graphics documentation for CGContextClip
:
The function uses the nonzero winding number rule to calculate the intersection of the current path with the current clipping path. Quartz then uses the path resulting from the intersection as the new current clipping path for subsequent painting operations.
and CGContextEOClip
:
The function uses the even-odd rule to calculate the intersection of the current path with the current clipping path. Quartz then uses the path resulting from the intersection as the new current clipping path for subsequent painting operations.
When I was working on clipping to hollow shapes, I first tried this procedure:
- Define the “outer” hull
- Invoke
CGContext*Clip
- Define the “inner” hull
- Invoke
CGContext*Clip
However, no matter which CGContext*Clip
functions I called at which point, and no matter which winding I used when defining the hulls, I could not get the clipping paths to interact in the way I wanted. I now understand why: These functions effectively apply the “nonzero winding” or “even-odd” rule to the current path (i.e. collection of subpaths), and then take the intersection of the resulting clipping region with the current clipping region.
(Incidentally, the difference between the “nonzero winding” and “even-odd” rules is reasonably well explained here; it’s not Apple documentation, but it’s on-point.)
Examples
This function defines a relatively complex clipping region, used to create the image at the top of this post:
+ (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 diameter (and radius) of holes
CGFloat d = 0.8 * oh;
CGFloat r = d / 2;
// Special case: Degenerate rectangles abort this method
if (3*d > ow) return;
// Horizontal spacing
CGFloat s = (ow - 3*d)/4;
// Begin a new path
CGContextBeginPath(context);
// Define a CW path in the CG co-ordinate system (origin at LL)
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
CGContextClosePath(context); // End outer loop at TDC
// Define 3 CCW paths in the CG co-ordinate system (origin at LL)
// Note that the final arg must be 0 on both iPhone OS and Mac OS X (including the iPhone simulator)
// This suggests the documentation is in error
CGContextMoveToPoint(context, ol+s+2*r, (ob+ot)/2);
CGContextAddArc(context, ol+s+r, (ob+ot)/2, r, 0, 2*M_PI, 0);
CGContextMoveToPoint(context, ol+2*s+4*r, (ob+ot)/2);
CGContextAddArc(context, ol+2*s+3*r, (ob+ot)/2, r, 0, 2*M_PI, 0);
CGContextMoveToPoint(context, ol+3*s+6*r, (ob+ot)/2);
CGContextAddArc(context, ol+3*s+5*r, (ob+ot)/2, r, 0, 2*M_PI, 0);
}
Three points:
- This example was hacked together, so some of the arguments to the function don’t make much sense
- The documentation for
CGContextAddArc
states that the final argument (clockwise
) must be 1 on the iPhone to create a CCW arc; this does not appear to be true, although I may be misinterpreting my results - The
CGContextMoveToPoint()s
preceding the arcs are crucial; without them, Core Graphics will insert line segments between the last point of one arc and the first point of another, with undesirable results
With our new understanding of paths and clipping, we can simplify the code that we saw on Tuesday:
+ (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;
// Begin a new path
CGContextBeginPath(context);
// Define a CW path in the CG co-ordinate system (origin at LL)
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
CGContextClosePath(context); // End outer loop at TDC
// Define a CCW path in the CG co-ordinate system (origin at LL)
CGContextMoveToPoint(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
CGContextClosePath(context); // End inner loop at TDC
}
Pingback: Things that were not immediately obvious to me » Blog Archive » Hollow Clipping Paths
Pingback: Things that were not immediately obvious to me » Blog Archive » Timing (Clipping)