Перевернутая маска swiftui с системным изображением

Я пытаюсь вырезать символ SF из формы круга в swiftUI.

В настоящее время у меня есть следующий код:

Circle()
   .fill(Color.white)
   .frame(width: 50, height: 50)
   .mask(
       Image(systemName: "play.fill")
           .font(.system(size: 24))
           .foregroundColor(Color.black)
           .frame(width: 50, height: 50)
   )

Что генерирует:

Однако я хочу инвертировать эффект маски: символ вырезается из круга, как на изображении ниже:

Обратите внимание, что фактический фон не красный, как на изображении, а будет загруженным пользователем изображением, поэтому установка красного цвета символа foregroundColor невозможна.

Есть ли способ инвертировать маску символа / изображения так, чтобы в круге было отверстие с формой изображения?

2 ответа

Решение

Этот ответ Влада Лего работает, когда применяется здесь.

Вы создаете свою маску, используя .black для деталей, которые вы хотите вырезать, и .white для частей, которые вы хотите сохранить.

(при условии, что фон красный, это будет выглядеть так)

Rectangle()
  .foregroundColor(Color.white)
  .mask(
    ZStack {
      Circle()
        .fill(Color.white)
        .frame(width: 50, height: 50)
      Image(systemName: "play.fill")
        .font(.system(size: 24))
        .foregroundColor(Color.black)
    }
      .compositingGroup()
      .luminanceToAlpha()
  )

Для вырезов просто используйте .blendMode(.destinationOut) и .compositingGroup(). Не нужно повторять форму, маскировать ее и определять белый / черный цвет.

       var buttonCutOut: some View {
        ZStack {
            shape
            mask.blendMode(.destinationOut)
        }.compositingGroup()
    }

Вот пример многоразового компонента для маскированной формы или формы с маской яркости.

ht tps://stackru.com/images/882a052f5fa3c06b108a71110f7b10b257b62cc3.png

       
struct Demo: View {
    var body: some View {
        ZStack {
            userImage.blur(radius: 5)
            playButtons
        }
    }

    var playButtons: some View {
        VStack {
            Mask(thisShape: Circle(),
                 with: maskImage,
                 offset: maskImageOffset,
                 maskToShapeRatio: maskImageToShapeRatio)
                .frame(width: square, height: square)
                .foregroundColor(colorDestinationOut)

            LuminanceMask(thisShape: Circle(),
                          with: maskImage,
                          offset: maskImageOffset,
                          maskToShapeRatio: maskImageToShapeRatio)
                .frame(width: square, height: square)
                .foregroundColor(colorLuminanceAlpha)
        }
        .font(font)
    }
}

struct Mask<S: Shape>: View {
    init(thisShape: S,
         with mask: Image,
         offset: CGSize,
         maskToShapeRatio: CGFloat) {
        self.shape = thisShape
        self.mask = mask
        self.offset = offset
        self.scale = maskToShapeRatio
    }

    let shape: S
    let mask: Image
    let offset: CGSize
    let scale: CGFloat

    var body: some View {
        ZStack(alignment: .center) {
            shape.fill()
            mask
                .resizable()
                .offset(offset)
                .aspectRatio(contentMode: .fit)
                .blendMode(.destinationOut)
                .scaleEffect(scale)
        }.compositingGroup()
    }
}

struct LuminanceMask<S: Shape>: View {
    init(thisShape: S,
         with mask: Image,
         offset: CGSize,
         maskToShapeRatio: CGFloat) {
        self.shape = thisShape
        self.mask = mask
        self.offset = offset
        self.scale = maskToShapeRatio
    }

    let shape: S
    let mask: Image
    let offset: CGSize
    let scale: CGFloat

    let keep = Color.white
    let remove = Color.black

    var body: some View {
            shape.mask(maskView)
    }

    var maskView: some View {
        ZStack(alignment: .center) {
            shape
                .foregroundColor(keep)

            mask
                .resizable()
                .offset(offset)
                .aspectRatio(contentMode: .fit)
                .scaleEffect(scale)
                .foregroundColor(remove)

        }.compositingGroup()
        .luminanceToAlpha()

    }
}

Вот мое решение, вдохновленное этим сообщением:

Сначала сделаем модификатор вида .inverseMask():

       extension View {
    // view.inverseMask(_:)
    public func inverseMask<M: View>(_ mask: M) -> some View {
        // exchange foreground and background
        let inversed = mask
            .foregroundColor(.black)  // hide foreground
            .background(Color.white)  // let the background stand out
            .compositingGroup()       // ⭐️ composite all layers
            .luminanceToAlpha()       // ⭐️ turn luminance into alpha (opacity)
        return self.mask(inversed)
    }
}

Во-вторых, сделаем демонстрационный просмотр:

Обратите внимание, что я использовал собственное расширение в следующем коде для LinearGradient, он указан внизу.

       import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    var body: some View {
        
        // lightbulb image
        let lightbulb = Image(systemName: "lightbulb")
            .resizable().scaledToFit().padding(24)
        // rounded rect (shape)
        let roundedRect = RoundedRectangle(cornerRadius: 20)
        // rounded rect (stroked border)
        let border = roundedRect
            .stroke(
                Gradient.diagonal(.white, .black),  // my custom extension
                lineWidth: 2
        )
        
        // container
        return ZStack {
            // background color
            Color.white.grayscale(0.3)
                
            // rounded card
            Gradient.horizontal(.gray, .black)      // my custom extension 
                
                // ⭐️ inverse mask
                .inverseMask(lightbulb)
                
                .shadow(color: Color.black.opacity(0.6), radius: 4, x: 4, y: 4)
                .frame(width: 150, height: 200)
                .clipShape(roundedRect)
                .overlay(border)
                .shadow(color: Color.white.opacity(0.9), radius: 18, x: -18, y: -18)
                .shadow(color: Color.black.opacity(0.3), radius: 14, x: 14, y: 14)
            
        }
    }
}

PlaygroundPage.current.setLiveView(ContentView())

и результат:

---- [Отредактировано] ----

Вот мое собственное расширение для Gradient если вам интересно:

       import SwiftUI

extension Gradient {
    
    // general linear gradient ---------------------------
    
    public static func linear(
        from start: UnitPoint, 
        to     end: UnitPoint, 
        colors    : [Color]       // use array
    ) -> LinearGradient 
    {
        LinearGradient(
            gradient  : Gradient(colors: colors), 
            startPoint: start, 
            endPoint  : end
        )
    }
    
    public static func linear(
        from start: UnitPoint, 
        to     end: UnitPoint, 
        colors    : Color...     // use variadic parameter
    ) -> LinearGradient 
    {
        linear(from: start, to: end, colors: colors)
    }
    
    // specialized linear gradients ------------------------
    
    // top to bottom
    public static func vertical(_ colors: Color...) -> LinearGradient {
        linear(from: .top, to: .bottom, colors: colors)
    }
    
    // leading to trailing
    public static func horizontal(_ colors: Color...) -> LinearGradient {
        linear(from: .leading, to: .trailing, colors: colors)
    }
    
    // top leading to bottom trailing
    public static func diagonal(_ colors: Color...) -> LinearGradient {
        linear(from: .topLeading, to: .bottomTrailing, colors: colors)
    }
    
    // top leading to bottom trailing
    public static func diagonal2(_ colors: Color...) -> LinearGradient {
        linear(from: .bottomLeading, to: .topTrailing, colors: colors)
    }
}

О, кстати, может быть, это то, чего хотел ОП:

       import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    var body: some View {
        
        // background image
        let background = Image("red stars.PNG")
        
        // mask
        let mask = Image(systemName: "play.circle.fill")
            .font(.system(size: 100))
            .scaledToFit()               // center the mask
        
        // container
        return ZStack {
            
            // background color
            Color.white.grayscale(0.3)
            
            // card
            background
                .inverseMask(mask)                    // ⭐️ cut out the mask
                // shadow for hole
                .shadow(color: Color.black.opacity(0.7), radius: 3, x: 3, y: 3)
                // highlight & shadow for border
                .shadow(color: Color.white.opacity(0.9), radius: 18, x: -18, y: -18)
                .shadow(color: Color.black.opacity(0.3), radius: 14, x:  14, y:  14)
        }
    }
}

PlaygroundPage.current.setLiveView(ContentView())

и результат:

Вы можете использовать другое изображение:

 Circle()
    .fill(Color.white)
    .frame(width: 50, height: 50)
    .mask(
        Image(systemName: "play.circle.fill")
            .font(.system(size: 24))
            .foregroundColor(Color.black)
            .frame(width: 50, height: 50)
    )
Другие вопросы по тегам