Search code examples
ioscore-graphicscashapelayercgimage

How to crop the shape of an image out of a caShape layer,


I have a cashape layer which looks like this

triangle

I have this P Image

P image

I want the center of the triangle to be see-through in the same shape of P image (or any image) kind of like this like this

I have tried (among many many other things) this... which seems close, but it ONLY shows the P, I need it to show everything but the P

 let viewTheTriangleIsIn = UIView()
    let pImage = UIImageView()

    let maskLayer = CAShapeLayer()
    maskLayer.frame.origin = CGPoint(x: 0, y: 0)
    maskLayer.frame.size = viewTheTriangleIsIn.bounds.size
    
    maskLayer.contents = pImage.image!.cgImage
    
    maskLayer.fillRule = .evenOdd
    viewTheTriangleIsIn.layer.mask = maskLayer

Solution

  • Assuming your "P image (or any image)" has transparent (alpha) areas such as this (the P is surrounded by transparency):

    enter image description here

    so it would look like this in a UIImageView:

    enter image description here

    You can create an "inverted transparency" image:

        guard let pImage = UIImage(named: "pImage") else {
            print("Could not load mask image!")
            return
        }
        
        // size of view you want to mask
        let sz: CGSize = CGSize(width: 200, height: 200)
        // rect to draw the mask image (where we want the "P")
        let maskRect:CGRect = CGRect(x: 50, y: 100, width: 100, height: 80)
        
        let renderer = UIGraphicsImageRenderer(size: sz)
        let iMaskImage = renderer.image { ctx in
            // fill with black (any color other than clear will do)
            ctx.cgContext.setFillColor(UIColor.black.cgColor)
            ctx.fill(CGRect(origin: .zero, size: sz))
            // draw the image in maskRect with .xor blendMode
            //  this will make all non-transparent pixels in the image transparent
            pImage.draw(in: maskRect, blendMode: .xor, alpha: 1.0)
        }
    

    At that point, iMaskImage will be this (the white "P" shape is not white... it's transparent):

    enter image description here

    You can then use that image as a mask on any other view.

    Here's an example MaskedTriangleView -- it uses a CAShapeLayer for the triangle, and then uses the "P" image as a layer mask:

    class MaskedTriangleView: UIView {
        
        let shapeLayer = CAShapeLayer()
        var maskImage: UIImage?
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() {
            layer.addSublayer(shapeLayer)
            shapeLayer.fillColor = UIColor.systemYellow.cgColor
            shapeLayer.strokeColor = UIColor.blue.cgColor
            shapeLayer.lineWidth = 1
        }
        override func layoutSubviews() {
            super.layoutSubviews()
            
            // create a triangle shape path
            let bez: UIBezierPath = UIBezierPath()
            bez.move(to: CGPoint(x: bounds.minX, y: bounds.maxY))
            bez.addLine(to: CGPoint(x: bounds.midX, y: bounds.minY))
            bez.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
            bez.close()
            shapeLayer.path = bez.cgPath
            
            if let img = maskImage {
                // let's make our mask image
                //  50% of the height of self
                let h: CGFloat = bounds.height * 0.5
                //  40% from the Top (leaving 10% space at the bottom)
                let y: CGFloat = bounds.height * 0.4
                //  keep it proportionally sized
                let ratio: CGFloat = h / img.size.height
                //  width is proportional to height
                let w: CGFloat = img.size.width * ratio
                //  center horizontally
                let x: CGFloat = (bounds.width - w) * 0.5
                
                // rect to draw the mask image
                let maskRect:CGRect = CGRect(x: x, y: y, width: w, height: h)
                
                let renderer: UIGraphicsImageRenderer = UIGraphicsImageRenderer(size: bounds.size)
                let iMaskImage: UIImage = renderer.image { ctx in
                    // fill with black (any color other than clear will do)
                    ctx.cgContext.setFillColor(UIColor.black.cgColor)
                    ctx.fill(CGRect(origin: .zero, size: bounds.size))
                    // draw the image in maskRect with .xor blendMode
                    //  this will make all non-transparent pixels in the image transparent
                    img.draw(in: maskRect, blendMode: .xor, alpha: 1.0)
                }
                // create a layer
                let maskLayer: CALayer = CALayer()
                // set the new image as its contents
                maskLayer.contents = iMaskImage.cgImage
                // same frame as self
                maskLayer.frame = bounds
                // use it as the mask for the shape layer
                shapeLayer.mask = maskLayer
            }
        }
        
    }
    

    and it will look like this (the top image is overlaid on an image view, the bottom image is added as a subview of the main view, with the green background showing through):

    enter image description here

    Here's the example controller:

    class ImageMaskingVC: UIViewController {
        
        var bkgImageView: UIImageView!
        var triangleView1: MaskedTriangleView!
        var triangleView2: MaskedTriangleView!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            view.backgroundColor = .systemGreen
            
            // make sure we can load the images
            guard let bkimg = UIImage(named: "samplePic") else {
                print("Could not load background image!")
                return
            }
            
            guard let pImage = UIImage(named: "pImage") else {
                print("Could not load mask image!")
                return
            }
            
            // create the image view and set its image
            bkgImageView = UIImageView()
            bkgImageView.image = bkimg
            
            // create a MaskedTriangleView and set its maskImage
            triangleView1 = MaskedTriangleView()
            triangleView1.maskImage = pImage
            
            // create another MaskedTriangleView and set its maskImage
            triangleView2 = MaskedTriangleView()
            triangleView2.maskImage = pImage
            
            // add the views
            [bkgImageView, triangleView1, triangleView2].forEach {
                if let v = $0 {
                    v.translatesAutoresizingMaskIntoConstraints = false
                    view.addSubview(v)
                }
            }
    
            let g = view.safeAreaLayoutGuide
            
            NSLayoutConstraint.activate([
                
                // constrain background image view
                //  Top / Leading / Trailing at 20-pts
                bkgImageView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                bkgImageView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
                bkgImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
                
                // height proportional to background image size
                bkgImageView.heightAnchor.constraint(equalTo: bkgImageView.widthAnchor, multiplier: bkimg.size.height / bkimg.size.width),
    
                // constrain first MaskedTriangleView exactly on top of the background image view
                triangleView1.topAnchor.constraint(equalTo: bkgImageView.topAnchor, constant: 20.0),
                triangleView1.leadingAnchor.constraint(equalTo: bkgImageView.leadingAnchor, constant: 20.0),
                triangleView1.trailingAnchor.constraint(equalTo: bkgImageView.trailingAnchor, constant: -20.0),
                triangleView1.bottomAnchor.constraint(equalTo: bkgImageView.bottomAnchor, constant: -20.0),
    
                // constrain the second MaskedTriangleView below the background image view
                //  with same width and height as the first MaskedTriangleView
                triangleView2.topAnchor.constraint(equalTo: bkgImageView.bottomAnchor, constant: 20.0),
                triangleView2.widthAnchor.constraint(equalTo: triangleView1.widthAnchor, constant: 0.0),
                triangleView2.heightAnchor.constraint(equalTo: triangleView1.heightAnchor, constant: 0.0),
                
                // centered horizontally
                triangleView2.centerXAnchor.constraint(equalTo: triangleView1.centerXAnchor, constant: 0.0),
    
            ])
            
        }
        
    }