12

Dear stack overflow community,

I have a problem concerning the mask property of a CAShapeLayer in iOS (Swift).

What I am trying to achieve is an eraser that erases parts of a image-layer by masking it. The problem comes in, when I try to invert it.

I found some good answers on inverting a path, but these were only useful when using a filled path. What I try to do is stroke a path and use the inverted one to mask an image. The line width on the stroke should by around 30.0 so it looks like an eraser.

I tried different things. My current version looks like this:

  1. Create a CAShapeLayer which holds the path of the eraser-stroke
  2. Set the fill-color of the layer to nil
  3. Set the stroke-color and line width
  4. Add the layer as the mask of the image-layer

This is working fine, but it only makes the parts of the image visible that are within the stroke. I want to do it reversed. I thought of a black and white mask, but this does not work, because the mask is delivered through the alpha channel.

Does anyone has an idea how to solve the problem?

Lars Petersen
  • 602
  • 9
  • 21
  • can you add also any photos which describe what you want to achieve? – Neil Galiaskarov Sep 09 '16 at 16:38
  • I just can speak for myself but I have a path which is a line stroked. And wan to clip away the circle (shown in the screenshot). I want to clip away the end of the line. (I don't want to retouch the path itself) screenshot: https://d17oy1vhnax1f7.cloudfront.net/items/0b3A3D2H3B3v01221G1I/Screen%20Shot%202016-09-09%20at%2018.43.19.png?v=1db0e296 – Christian 'fuzi' Orgler Sep 09 '16 at 16:45
  • Keen to help but I'd be grateful if you could clarify - are you looking for a way to create an eraser by any means possible, or is it important that you implement the eraser in the way that you have outlined? – Marcus Sep 13 '16 at 21:29
  • I would need an eraser that would function what I described above. The eraser itself can not have a fillColor. Maybe you could have an invisible layer which has a fillColor and that one masks the layer. Not sure, will try. – Christian 'fuzi' Orgler Sep 15 '16 at 15:53

1 Answers1

6

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:

enter image description here

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.

clemens
  • 16,716
  • 11
  • 50
  • 65