Using Swift and CAShapeLayer() with masking, how can I avoid inverting the mask when masked regions intersect?

Viewed 790

This question was challenging to word, but explaining the situation further should help.

Using the code below, I'm essentially masking a circle on the screen wherever I tap to reveal what's underneath the black UIView. When I tap, I record the CGPoint in an array to keep track of the tapped locations. For every subsequent tap I make, I remove the black UIView and recreate each tapped point from the array of CGPoints I'm tracking in order to create a new mask that includes all the previous points.

The result is something like this:

Tapped regions

I'm sure you can already spot what I'm asking about... How can I avoid the mask inverting wherever the circles intersect? Thanks for your help!

Here's my code for reference:

class MiniGameShadOViewController: UIViewController {
    
    //MARK: - DECLARATIONS
 
    var revealRadius : CGFloat = 50
    var tappedAreas : [CGPoint] = []

    //Objects
    @IBOutlet var shadedRegion: UIView!
    
    //Gesture Recognizers
    @IBOutlet var tapToReveal: UITapGestureRecognizer!
    
    

    //MARK: - VIEW STATES

    override func viewDidLoad() {
        super.viewDidLoad()

    }
    
  
    //MARK: - USER INTERACTIONS
    @IBAction func regionTapped(_ sender: UITapGestureRecognizer) {
        
        let tappedPoint = sender.location(in: view)
        
        tappedAreas.append(tappedPoint) //Hold a list of all previously tapped points
       
        //Clean up old overlays before adding the new one
        for subview in shadedRegion.subviews {
            if subview.accessibilityIdentifier != "Number" {subview.removeFromSuperview()}
        }
            //shadedRegion.layer.mask?.removeFromSuperlayer()
        
        createOverlay()
    }

    
    //MARK: - FUNCTIONS
    
    func createOverlay(){
        
        //Create the shroud that covers the orbs on the screen
        let overlayView = UIView(frame: shadedRegion.bounds)
            overlayView.alpha = 1
            overlayView.backgroundColor = UIColor.black
            overlayView.isUserInteractionEnabled = false
        shadedRegion.addSubview(overlayView)
        
        let path = CGMutablePath()
        
        //Create the box that represents the inverse/negative area relative to the circles
        path.addRect(CGRect(origin: .zero, size: overlayView.frame.size))

        //For each point tapped so far, create a circle there
        for point in tappedAreas {
            path.addArc(center: point, radius: revealRadius, startAngle: 0.0, endAngle: 2.0 * .pi, clockwise: false)
            path.closeSubpath() //This is required to prevent all circles from being joined together with lines
          }
        
            //Fill each of my circles
            let maskLayer = CAShapeLayer()
                maskLayer.backgroundColor = UIColor.black.cgColor
                maskLayer.path = path;
                maskLayer.fillRule = .evenOdd
            
            //Cut out the circles inside that box
            overlayView.layer.mask = maskLayer
            overlayView.clipsToBounds = true
        }
}
1 Answers

You asked:

how can I avoid inverting the mask when masked regions intersect?

In short, do not use the .evenOdd fill rule.


You have specified a fillRule of .evenOdd. That results in intersections of paths to invert. Here is a red colored view with a mask consisting of a path with two overlapping circular arcs with the .evenOdd rule:

enter image description here

If you use .nonZero (which, coincidentally, is the default fill rule for shape layers), they will not invert each other:

enter image description here


E.g.

class ViewController: UIViewController {
    @IBOutlet weak var imageView: UIImageView!
    
    var maskLayer: CAShapeLayer = {
        let shapeLayer = CAShapeLayer()
        shapeLayer.fillColor = UIColor.white.cgColor
        return shapeLayer
    }()

    var points: [CGPoint] = [] // this isn't strictly necessary, but just in case you want an array of the points that were tapped
    var path = UIBezierPath()

    override func viewDidLoad() {
        super.viewDidLoad()
        imageView.layer.mask = maskLayer
    }

    @IBAction func handleTapGesture(_ gesture: UITapGestureRecognizer) {
        let point = gesture.location(in: gesture.view)
        points.append(point)

        path.move(to: point)
        path.addArc(withCenter: point, radius: 40, startAngle: 0, endAngle: .pi * 2, clockwise: true)

        maskLayer.path = path.cgPath
    }
}

Resulting in:

enter image description here

Related