|
| 1 | +# Creating an inverted mask layer |
| 2 | + |
| 3 | +* Also known as removing a path from layer. |
| 4 | + |
| 5 | +There are several ways to accomplish this. In fact here are 5 of them: [http://www.cocoawithlove.com/2010/05/5-ways-to-draw-2d-shape-with-hole-in.html](http://www.cocoawithlove.com/2010/05/5-ways-to-draw-2d-shape-with-hole-in.html). Most solutions boil down to: |
| 6 | + |
| 7 | +* Create the whole path yourself and wrap around (this is a pain and can bug out if there are multiple regions like a star) |
| 8 | +* Do everything using a CGImageRef (this is just slow) |
| 9 | +* Use cool tricks with "even-odd" rendering (this would work great if there weren't multiple overlapping regions) |
| 10 | +* Just use rectangles (obvious limitations) |
| 11 | + |
| 12 | +My solution uses a subclass of a CAShapeLayer. |
| 13 | + |
| 14 | +When you subclass a CAShapeLayer the most difficult challenge is getting it to display correctly. For a subclass to call `drawInContext` you must set the `bounds`. When you set the `bounds` the position freaks out. When you want to update... you need to update the `bounds` (which means you have to *change* the value). |
| 15 | + |
| 16 | +Keeping this in mind, I created a subclass: |
| 17 | + |
| 18 | + @interface CAInvertedMaskLayer: CAShapeLayer |
| 19 | + |
| 20 | + @property (nonatomic, assign) CGPathRef invertedMask; |
| 21 | + |
| 22 | + @end |
| 23 | + |
| 24 | + @implementation CAInvertedMaskLayer |
| 25 | + |
| 26 | + - (void)drawInContext:(CGContextRef)ctx { |
| 27 | + // Custom drawing code goes here |
| 28 | + NSColor *color = [NSColor redColor]; |
| 29 | + |
| 30 | + // Draw the path of this layer first (draws everything) |
| 31 | + CGContextSetFillColorWithColor(ctx, color.CGColor); |
| 32 | + CGContextAddPath(ctx, self.path); |
| 33 | + CGContextFillPath(ctx); |
| 34 | + |
| 35 | + // Clear out the inverted portion |
| 36 | + if (self.invertedMask) { |
| 37 | + CGContextSetBlendMode(ctx, kCGBlendModeClear); |
| 38 | + CGContextSetFillColorWithColor(ctx, [NSColor clearColor].CGColor); |
| 39 | + CGPathRef path = self.invertedMask; |
| 40 | + CGContextAddPath(ctx, path); |
| 41 | + CGContextFillPath(ctx); |
| 42 | + } |
| 43 | + } |
| 44 | + |
| 45 | + - (BOOL)needsDisplayOnBoundsChange { |
| 46 | + return YES; |
| 47 | + } |
| 48 | + |
| 49 | + @end |
| 50 | + |
| 51 | +So far so good. Unfortunately when I added my special drawing code, the inherited `CAShapeLayer` was still performing some actions, because of this you want to make sure you haven't set things like `fillColor` on the layer to the wrong value. Generally you want the layer `fillColor` to be `clearColor` otherwise the blending may be confusing. |
| 52 | + |
| 53 | + self.regionLayer = [[CAInvertedMaskLayer alloc] init]; |
| 54 | + self.regionLayer.fillColor = [NSColor clearColor].CGColor; |
| 55 | + |
| 56 | +You have to set the position when we override `drawInRect`. Here I am just setting the position based on the bounds of the container (in this case my view). |
| 57 | + |
| 58 | + // Set the position, because the origin defaults to center, center |
| 59 | + [self.regionLayer setPosition:CGPointMake([self bounds].size.width/2, [self bounds].size.height/2)]; |
| 60 | + |
| 61 | +At this point you can simply set the `path` and update the `invertedMask` path. |
| 62 | + |
| 63 | + CGPathRef regPath = CGPathCreateWithRect(rect, &CGAffineTransformIdentity); |
| 64 | + self.regionLayer.path = regPath; |
| 65 | + CGPathRelease(regPath); |
| 66 | + |
| 67 | + CGMutablePathRef occlusionPath = CGPathCreateMutable(); |
| 68 | + CGAffineTransform transform = CGAffineTransformMakeScale(1, -1); |
| 69 | + transform = CGAffineTransformTranslate(transform, 0, - self.bounds.size.height); |
| 70 | + CGPathAddRect(occlusionPath, &transform, bounds); |
| 71 | + self.regionLayer.invertedMask = occlusionPath; |
| 72 | + CGPathRelease(occlusionPath); |
| 73 | + |
| 74 | +Now the tricky part: to make the render happen you need to set the bounds of the layer. Again, my layer is full width so I am just using the bounds of my view. I set it to an empty rectangle and set it back. |
| 75 | + |
| 76 | + self.regionLayer.bounds = CGRectNull; |
| 77 | + self.regionLayer.bounds = self.bounds; |
| 78 | + |
| 79 | +# Future directions |
| 80 | + |
| 81 | +Instead of using a path for the `invertedMask` you could use a whole `invertedMaskLayer`. Instead of rendering the path in your `drawInRect` code you could render the whole layer as clear. Using this you could end up with much more advanced masks. |
0 commit comments