Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI add inverted mask

Tags:

I'm trying to add a mask to two shapes such that the second shape masks out the first shape. If I do something like Circle().mask(Circle().offset(…)), this has the opposite affect: preventing anything outside the first circle from being visible.

For UIView the answer is here: iOS invert mask in drawRect

However, trying to implement this in SwiftUI without UIView eludes me. I tried implementing an InvertedShape with I could then use as a mask:

struct InvertedShape<OriginalType: Shape>: Shape {
    let originalShape: OriginalType

    func path(in rect: CGRect) -> Path {
        let mutableOriginal = originalShape.path(in: rect).cgPath.mutableCopy()!
        mutableOriginal.addPath(Path(rect).cgPath)
        return Path(mutableOriginal)
            .evenOddFillRule()
    }
}

Unfortunately, SwiftUI paths do not have addPath(Path) (because they are immutable) or evenOddFillRule(). You can access the path's CGPath and make a mutable copy and then add the two paths, however, evenOddFillRule needs to be set on the CGLayer, not the CGPath. So unless I can get to the CGLayer, I'm unsure how to proceed.

This is Swift 5.

like image 723
Garrett Motzner Avatar asked Jan 09 '20 01:01

Garrett Motzner


2 Answers

Here is a demo of possible approach of creating inverted mask, by SwiftUI only, (on example to make a hole in view)

SwiftUI hole mask, reverse mask

func HoleShapeMask(in rect: CGRect) -> Path {
    var shape = Rectangle().path(in: rect)
    shape.addPath(Circle().path(in: rect))
    return shape
}

struct TestInvertedMask: View {

    let rect = CGRect(x: 0, y: 0, width: 300, height: 100)
    var body: some View {
        Rectangle()
            .fill(Color.blue)
            .frame(width: rect.width, height: rect.height)
            .mask(HoleShapeMask(in: rect).fill(style: FillStyle(eoFill: true)))
    }
}
like image 85
Asperi Avatar answered Oct 02 '22 03:10

Asperi


Here's another way to do it, which is more Swiftly.

The trick is to use:

YourMaskView()
   .compositingGroup()
   .luminanceToAlpha() 

maskedView.mask(YourMaskView())

Just create your mask with Black and White shapes, black will be transparent, white opaque, anything in between is going to be semi-transparent.

.compositingView(), similar to .drawingGroup(), rasterises the view (converts it to a bitmap texture). By the way, this also happens when you .blur or do any other pixel-level operations.

.luminanceToAlpha() takes the RGB luminance levels (I guess by averaging the RGB values), and maps them to the Alpha (opacity) channel of the bitmap.

like image 38
Vlad Lego Avatar answered Oct 02 '22 02:10

Vlad Lego