Invert Calayer Mask That Is Based on a Stroke (No Fill)

Invert CALayer mask that is based on a stroke (no fill)

You can achieve this by drawing with transparent colors to a non-opaque layer. This can be done by using another blend mode for drawing. Unfortunately CAShapeLayer doesn't support this. Thus, you must wrote your own shape layer class:

@interface ShapeLayer : CALayer

@property(nonatomic) CGPathRef path;
@property(nonatomic) CGColorRef fillColor;
@property(nonatomic) CGColorRef strokeColor;
@property(nonatomic) CGFloat lineWidth;

@end

@implementation ShapeLayer

@dynamic path;
@dynamic fillColor;
@dynamic strokeColor;
@dynamic lineWidth;

- (void)drawInContext:(CGContextRef)inContext {
CGContextSetGrayFillColor(inContext, 0.0, 1.0);
CGContextFillRect(inContext, self.bounds);
CGContextSetBlendMode(inContext, kCGBlendModeSourceIn);
if(self.strokeColor) {
CGContextSetStrokeColorWithColor(inContext, self.strokeColor);
}
if(self.fillColor) {
CGContextSetFillColorWithColor(inContext, self.fillColor);
}
CGContextSetLineWidth(inContext, self.lineWidth);
CGContextAddPath(inContext, self.path);
CGContextDrawPath(inContext, kCGPathFillStroke);
}

@end

Creating a layer with a transparent path:

ShapeLayer *theLayer = [ShapeLayer layer];

theLayer.path = ...;
theLayer.strokeColor = [UIColor clearColor].CGColor;
theLayer.fillColor = [UIColor colorWithWhite:0.8 alpha:0.5];
theLayer.lineWith = 3.0;
theLayer.opaque = NO; // Important, otherwise you will get a black rectangle

I've used this code to draw a semi-transparent circle with transparent border in front of a green background:

Sample Image

Edit: Here is the corresponding code for the layer in Swift:

public class ShapeLayer: CALayer {
@NSManaged var path : CGPath?
@NSManaged var fillColor : CGColor?
@NSManaged var strokeColor : CGColor?
@NSManaged var lineWidth : CGFloat

override class func defaultValue(forKey inKey: String) -> Any? {
return inKey == "lineWidth" ? 1.0 : super.defaultValue(forKey: inKey)
}

override class func needsDisplay(forKey inKey: String) -> Bool {
return inKey == "path" || inKey == "fillColor" || inKey == "strokeColor" || inKey == "lineWidth" || super.needsDisplay(forKey: inKey)
}

override public func draw(in inContext: CGContext) {
if let thePath = path {
inContext.setFillColor(gray: 0.0, alpha: 1.0)
inContext.fill(self.bounds)
inContext.setBlendMode(.sourceIn)
if let strokeColor = self.strokeColor {
inContext.setStrokeColor(strokeColor)
}
if let fillColor = self.fillColor {
inContext.setFillColor(fillColor)
}
inContext.setLineWidth(self.lineWidth)
inContext.addPath(thePath)
inContext.drawPath(using: .fillStroke)
}
}
}

Note: By marking the properties with @NSManaged you can easily make the properties animatable by implementing needsDisplay(forKey inKey:) in Swift or needsDisplayForKey: in Objective C, respectively. I've adapted the Swift code accordingly.

But even if you don't need animations, it is better to mark the properties with @NSManaged, because QuartzCore makes copies of layers and should also copy all properties with it. @NSManaged in Swift is the counterpart to @dynamic in Objective C, because it avoids the creation of a property implementation. Instead CALayer gets and sets property values with value(forKey:) and setValue(_:forKey:), respectively.

Reverse a CALayer mask

What I would do is construct the mask in real time. This is easy if you have a black image of the logo. Using standard techniques, you can draw the logo image into an image that you construct in real time, so that you are in charge of the size of the image and the size and placement of logo within it. Using a "Mask To Alpha" CIFilter, you can then convert the black to transparent for use as a layer mask.

So, to illustrate. Here's the background image: this is what we want to see wherever we punch a hole in the foreground:

Sample Image

Here's the foreground image, lying on top of the background and completely hiding it:

Sample Image

Here's the logo, in black (ignore the grey, which represents transparency):

Sample Image

Here's the logo drawn in code into a white background of the correct size:

Sample Image

And finally, here's that same image converted into a mask with the Mask To Alpha CIFilter and attached to the foreground image view as its mask:

Sample Image

Okay, I could have chosen my images a little better, but this is what I had lying around. You can see that wherever there was black in the logo, we are punching a hole in the foreground image and seeing the background image, which I believe is exactly what you said you wanted to do.

The key step is the last one, namely the conversion of the black-on-white image of the logo (im) to a mask; here's how I did that:

    let cim = CIImage(image:im)
let filter = CIFilter(name:"CIMaskToAlpha")!
filter.setValue(cim, forKey: "inputImage")
let out = filter.outputImage!
let cgim = CIContext().createCGImage(out, from: out.extent)
let lay = CALayer()
lay.frame = self.iv.bounds
lay.contents = cgim
self.iv.layer.mask = lay

CALayer create transparent round mask

Yeah, kCAFillRuleEvenOdd didn't do it's magic without adding a rect first, here is a working snippet though:

CAShapeLayer *shape = [CAShapeLayer layer];

shape.frame = self.bounds;

CGMutablePathRef pathRef = CGPathCreateMutable();
CGPathAddRect(pathRef, NULL, self.bounds);
CGPathAddEllipseInRect(pathRef, NULL, self.bounds);

shape.fillRule = kCAFillRuleEvenOdd;
shape.path = pathRef;

self.layer.mask = shape;

iOS invert mask in drawRect

With even odd filling on the shape layer (maskLayer.fillRule = kCAFillRuleEvenOdd;) you can add a big rectangle that covers the entire frame and then add the shape you are masking out. This will in effect invert the mask.

CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
CGMutablePathRef maskPath = CGPathCreateMutable();
CGPathAddRect(maskPath, NULL, someBigRectangle); // this line is new
CGPathAddPath(maskPath, nil, myPath.CGPath);
[maskLayer setPath:maskPath];
maskLayer.fillRule = kCAFillRuleEvenOdd; // this line is new
CGPathRelease(maskPath);
self.layer.mask = maskLayer;

How do I mask and erase a CAShapeLayer?

You can use a bezier path mask to "erase" the path by creating a custom CALayer subclass and overriding draw(in ctx: CGContext):

class MyCustomLayer: CALayer {

var myPath: CGPath?
var lineWidth: CGFloat = 24.0

override func draw(in ctx: CGContext) {

// fill entire layer with solid color
ctx.setFillColor(UIColor.gray.cgColor)
ctx.fill(self.bounds);

// we want to "clear" the stroke
ctx.setStrokeColor(UIColor.clear.cgColor);
// any color will work, as the mask uses the alpha value
ctx.setFillColor(UIColor.white.cgColor)
ctx.setLineWidth(self.lineWidth)
ctx.setLineCap(.round)
ctx.setLineJoin(.round)
if let pth = self.myPath {
ctx.addPath(pth)
}
ctx.setBlendMode(.sourceIn)
ctx.drawPath(using: .fillStroke)

}

}

Here's a complete example... we'll create a UIView subclass and, since the effect will be "scratching off the image" we'll call it ScratchOffImageView:

class ScratchOffImageView: UIView {

public var image: UIImage? {
didSet {
self.scratchOffImageLayer.contents = image?.cgImage
}
}

// adjust drawing-line-width as desired
// or set from
public var lineWidth: CGFloat = 24.0 {
didSet {
maskLayer.lineWidth = lineWidth
}
}

private class MyCustomLayer: CALayer {

var myPath: CGPath?
var lineWidth: CGFloat = 24.0

override func draw(in ctx: CGContext) {

// fill entire layer with solid color
ctx.setFillColor(UIColor.gray.cgColor)
ctx.fill(self.bounds);

// we want to "clear" the stroke
ctx.setStrokeColor(UIColor.clear.cgColor);
// any color will work, as the mask uses the alpha value
ctx.setFillColor(UIColor.white.cgColor)
ctx.setLineWidth(self.lineWidth)
ctx.setLineCap(.round)
ctx.setLineJoin(.round)
if let pth = self.myPath {
ctx.addPath(pth)
}
ctx.setBlendMode(.sourceIn)
ctx.drawPath(using: .fillStroke)

}

}

private let maskPath: UIBezierPath = UIBezierPath()
private let maskLayer: MyCustomLayer = MyCustomLayer()
private let scratchOffImageLayer: CALayer = CALayer()

override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {

// Important, otherwise you will get a black rectangle
maskLayer.isOpaque = false

// add the image layer
layer.addSublayer(scratchOffImageLayer)
// assign the layer mask
scratchOffImageLayer.mask = maskLayer

}

override func layoutSubviews() {
super.layoutSubviews()

// set frames for mask and image layers
maskLayer.frame = bounds
scratchOffImageLayer.frame = bounds

// triggers drawInContext
maskLayer.setNeedsDisplay()
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let currentPoint = touch.location(in: self)
maskPath.move(to: currentPoint)
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let currentPoint = touch.location(in: self)
// add line to our maskPath
maskPath.addLine(to: currentPoint)
// update the mask layer path
maskLayer.myPath = maskPath.cgPath
// triggers drawInContext
maskLayer.setNeedsDisplay()
}

}

and, an example view controller:

class ScratchOffViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

view.backgroundColor = .systemBackground

guard let img = UIImage(named: "test.jpg") else {
fatalError("Could not load image!!!!")
}

let scratchOffView = ScratchOffImageView()

// set the "scratch-off" image
scratchOffView.image = img

// default line width is 24.0
// we can set it to a different width here
//scratchOffView.lineWidth = 12

// let's add a light-gray label with red text
// we'll overlay the scratch-off-view on top of the label
// so we can see the text "through" the image
let backgroundLabel = UILabel()
backgroundLabel.font = .italicSystemFont(ofSize: 36)
backgroundLabel.text = "This is some text in a label so we can see that the path is clear -- so it appears as if the image is being \"scratched off\""
backgroundLabel.numberOfLines = 0
backgroundLabel.textColor = .red
backgroundLabel.backgroundColor = UIColor(white: 0.9, alpha: 1.0)

[backgroundLabel, scratchOffView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}

let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
backgroundLabel.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.7),
backgroundLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
backgroundLabel.centerYAnchor.constraint(equalTo: g.centerYAnchor),

scratchOffView.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.8),
scratchOffView.heightAnchor.constraint(equalTo: scratchOffView.widthAnchor, multiplier: 2.0 / 3.0),
scratchOffView.centerXAnchor.constraint(equalTo: backgroundLabel.centerXAnchor),
scratchOffView.centerYAnchor.constraint(equalTo: backgroundLabel.centerYAnchor),
])

}

}

It will look like this to start - I used a 3:2 image, and overlaid it on a light-gray label with red text so we can see that we are "scratching off" the image:

Sample Image

then, after a little bit of "scratching":

Sample Image

and after a lot of "scratching":

Sample Image

SwiftUI add inverted mask

Here is a demo of possible approach of creating inverted mask, by SwiftUI only, (on example to make a hole in view)

SwiftUI hole mask, reverse mask

func HoleShapeMask(in rect: CGRect) -> Path {
var shape = Rectangle().path(in: rect)
shape.addPath(Circle().path(in: rect))
return shape
}

struct TestInvertedMask: View {

let rect = CGRect(x: 0, y: 0, width: 300, height: 100)
var body: some View {
Rectangle()
.fill(Color.blue)
.frame(width: rect.width, height: rect.height)
.mask(HoleShapeMask(in: rect).fill(style: FillStyle(eoFill: true)))
}
}


Related Topics



Leave a reply



Submit