3

I'm implementing a drawing function for an iPad app. I'm using UIBezierPaths on CAShapeLayers for the drawing. By creating a new CAShapeLayer for each TouchesBegan event, I'm building an array of 'stacked up' CAShapeLayers that allow me to easily implement undo and redo by popping and pushing layers to and from the array. I'm also doing some interesting layer blending techniques by using some CAShapeLayer.compositingFilters. That's all working very well. My challenge is erasing.

I'm attempting to create a second array of CAShapeLayers and use them to mask the first group. I'm able to ADD TO the mask group using the same technique from above while drawing with an opaque color but I am not able to remove opaque areas from the mask group.

I thought I would be able to start the masking technique with a base layer that was opaque (black, white or whatever). Then, I had hoped to draw UIBezierPaths with UIColor.clear.cgColor and combine or composite my drawn clear path with the underlying opaque, base mask. This in effect, should "erase" that area of the mask, and hide the stacked up CAShapeLayers that I draw into. I didn't want to combine the mask layers into an image because I would lose the ability to easily undo and redo by popping and pushing on the mask array.

I've included some pseudo code below. Any pointers, help, or strategies for a solution would be much appreciated! I've been working on this for a number of weeks and I'm really stumped. I can't find any info on the strategy I'm working toward for this. Also, if I'm going at the drawing functionality incorrectly from the start and there's an easier way to draw while maintaining simple undo/redo, and add erase, please let me know. I'm totally open to adjusting my approach! Thanks in advance for any assistance.

// setup the layer hierarchy for drawing    
private func setupView() {
        self.mainDrawLayer = CAShapeLayer()
        self.mainDrawLayer.backgroundColor = UIColor.clear.cgColor
        self.layer.addSublayer(self.mainDrawLayer)

        // set up the mask. add an opaque background so everything shows through to start
        self.maskLayer = CALayer()
        let p = UIBezierPath.init(rect: self.bounds)
        self.maskShapeLayer = CAShapeLayer()
        self.maskShapeLayer?.fillColor = UIColor.black.cgColor
        self.maskShapeLayer?.path = p.cgPath
        self.maskLayer?.addSublayer(self.maskShapeLayer!)

        // apply the mask
        self.layer.mask = self.maskLayer
    }

override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)

        guard let touch = touches.first else {
            return
        }

        var erasing = false


        // setup the currentDrawLayer which captures the bezier path
        self.currentDrawLayer = CAShapeLayer()
        self.currentDrawLayer?.lineCap = CAShapeLayerLineCap.round
        self.currentDrawLayer?.fillColor = nil

        // set the ink color to use for drawing
        if let ink = UserDefaults.standard.string(forKey: "ink") {
            self.currentDrawLayer?.strokeColor = UIColor(hex: ink)?.cgColor
        } else {
            self.currentDrawLayer?.strokeColor = UIColor(hex: Constants.inkBlack3)?.cgColor
        }

        if UserDefaults.standard.string(forKey: "ink") == Constants.inkBlack5 {
            // removing the filter makes white overlay other colors
            // this is essentially erasing with a white background
            self.currentDrawLayer?.compositingFilter = nil
        } else {
            // this blend mode ads a whole different feeling to the drawing!
            self.currentDrawLayer?.compositingFilter = "darkenBlendMode"
        }

        // THIS IS THE ERASER COLOR!
        if UserDefaults.standard.string(forKey: "ink") == Constants.inkBlack4 {
            // trying erasing via drawing with clear
            self.currentDrawLayer?.strokeColor = UIColor.clear.cgColor
            // is there a compositingFilter available to 'combine' my clear color with the black opaque mask layer created above?
            self.currentDrawLayer?.compositingFilter = "iDontHaveADamnClueIfTheresOneThatWillWorkIveTriedThemAllItSeems:)"

            erasing = true

        }

        self.currentDrawLayer?.path = self.mainDrawPath.cgPath

        if erasing {
            // add the layer to the masks
            self.maskLayer!.addSublayer(self.currentDrawLayer!)
        } else {
            // add the layer to the drawings
            self.layer.addSublayer(self.currentDrawLayer!)
        }

        let location = touch.location(in: self)

        self.ctr = 0
        self.pts[0] = location
    }
eloehfelm
  • 31
  • 3

1 Answers1

0

I've only been able to accomplish something like this by appending to the path of the mask's shape layer.

        CALayer *penStroke = ... the layer that has all the pen strokes ...;

        UIBezierPath *eraserStroke = [[UIBezierPath bezierPathWithRect:[self bounds]] bezierPathByReversingPath];
        [eraserStroke appendPath:[UIBezierPath bezierPathWithRect:CGRectMake(110, 110, 200, 30)]];

        CAShapeLayer *maskFill = [CAShapeLayer layer];
        maskFill.path = [eraserStroke CGPath];
        maskFill.fillRule = kCAFillRuleEvenOdd;
        maskFill.fillColor = [[UIColor blackColor] CGColor];
        maskFill.backgroundColor = [[UIColor clearColor] CGColor];

        penStroke.mask = maskFill;

the above will mask all pen strokes by all eraser strokes. But a drawing app would want to be able to draw over previous eraser strokes. I believe that'd need to be handled by continually wrapping existing penStroke layer with another new penStroke layer, and always adding penStrokes to the outermost layer.

That would give something like:

 - Layer C: most recent pen strokes
   - mask: no mask, so that all strokes are pen strokes
   - Layer B: sublayer of layer C, contains some previous pen strokes
     - Layer B's mask: most recent eraser strokes
     - Layer A: sublayer on Layer B, contains pen strokes before the most recent eraser strokes
       - Layer A's mask: eraser strokes before layer B's pen strokes

Hopefully that makes sense. Basically, it'd be nesting layers/masks. Every time the user switches from pen -> eraser or eraser -> pen, a new layer would get wrapped around all the existing pen/eraser stroke layers.

adam.wulf
  • 2,149
  • 20
  • 27