Skip to content

Commit 8957ae2

Browse files
committed
Inverted masks
1 parent 786be0f commit 8957ae2

File tree

2 files changed

+82
-0
lines changed

2 files changed

+82
-0
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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.

setting_up_a_new_dev_machine.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* Turn off auto-correct (System Preferences | Keyboard | Text)
2121
* Turn up key repeat
2222
* Screen auto-off in 5 minutes
23+
* Karabiner: (customize everything) https://pqrs.org/osx/karabiner/document.html.en
2324

2425
## 1Password?
2526

0 commit comments

Comments
 (0)