How to crop the shape of an image out of a caShape layer,
Assuming your "P image (or any image)" has transparent (alpha) areas such as this (the P is surrounded by transparency):
so it would look like this in a UIImageView
:
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):
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):
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),
])
}
}