Использование CATextLayer для маскирования от другого CALayer

По какой-то причине я не могу заставить это работать с CATextLayer. Я подозреваю, что это совершенно очевидно, и я не вижу леса за деревьями, но что мне нужно сделать, так это использовать CATextLayer, чтобы замаскировать «дыру» в CAGradientLayer (так что эффект представляет собой градиент с текстом «вырезать «из него»).

У меня это работает нормально, наоборот, но я подхожу к змеиным глазам, пытаясь замаскировать текст от градиента.

Вот код, который я использую (это класс UIButton, и это переопределение layoutSubviews()):

override func layoutSubviews() {
    super.layoutSubviews()
    layer.borderColor = UIColor.clear.cgColor
    if let text = titleLabel?.text,
       var dynFont = titleLabel?.font {
        let minimumFontSizeInPoints = (dynFont.pointSize * 0.5)
        let scalingStep = 0.025
        while dynFont.pointSize >= minimumFontSizeInPoints {
            let calcString = NSAttributedString(string: text, attributes: [.font: dynFont])
            let cropRect = calcString.boundingRect(with: CGSize.init(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
            if bounds.size.width >= cropRect.size.width {
                break
            }
            guard let tempDynFont = UIFont(name: dynFont.fontName, size: dynFont.pointSize - (dynFont.pointSize * scalingStep)) else { break }
            dynFont = tempDynFont
        }
        
        titleLabel?.font = dynFont
    }
    
    if let titleLabel = titleLabel,
       let font = titleLabel.font,
       let text = titleLabel.text {
        let textLayer = CATextLayer()
        textLayer.frame = titleLabel.frame
        textLayer.rasterizationScale = UIScreen.main.scale
        textLayer.contentsScale = UIScreen.main.scale
        textLayer.alignmentMode = .left
        textLayer.fontSize = font.pointSize
        textLayer.font = font
        textLayer.isWrapped = true
        textLayer.truncationMode = .none
        textLayer.string = text
        self.textLayer = textLayer
        titleLabel.textColor = .clear

        let gradient = CAGradientLayer()
        gradient.colors = [gradientStartColor.cgColor, gradientEndColor.cgColor]
        gradient.startPoint = CGPoint(x: 0.5, y: 0)
        gradient.endPoint = CGPoint(x: 0.5, y: 1.0)
        var layerFrame = textLayer.frame

        if !reversed {
            if 0 < layer.borderWidth {
                let outlineLayer = CAShapeLayer()
                outlineLayer.frame = bounds
                outlineLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
                outlineLayer.lineWidth = layer.borderWidth
                outlineLayer.strokeColor = UIColor.white.cgColor
                outlineLayer.fillColor = UIColor.clear.cgColor
                layerFrame = bounds
                textLayer.masksToBounds = false
                if let compositingFilter = CIFilter(name: "CIAdditionCompositing") {
                    textLayer.compositingFilter = compositingFilter
                    outlineLayer.addSublayer(textLayer)
                }
                layer.mask = outlineLayer
            } else {
                layer.mask = textLayer
            }
        } else {
            let outlineLayer = CAShapeLayer()
            outlineLayer.frame = bounds
            textLayer.foregroundColor = UIColor.white.cgColor
            outlineLayer.backgroundColor = UIColor.white.cgColor
            layerFrame = bounds
            textLayer.masksToBounds = false
            if let compositingFilter = CIFilter(name: "CISourceOutCompositing") {
                outlineLayer.compositingFilter = compositingFilter
                outlineLayer.addSublayer(textLayer)
            }
            layer.mask = outlineLayer
        }
        
        gradient.frame = layerFrame
        layer.addSublayer(gradient)
    }
}

Проблема в этой части кода:

            let outlineLayer = CAShapeLayer()
            outlineLayer.frame = bounds
            textLayer.foregroundColor = UIColor.white.cgColor
            outlineLayer.backgroundColor = UIColor.white.cgColor
            layerFrame = bounds
            textLayer.masksToBounds = false
            if let compositingFilter = CIFilter(name: "CISourceOutCompositing") {
                outlineLayer.compositingFilter = compositingFilter
                outlineLayer.addSublayer(textLayer)
            }
            layer.mask = outlineLayer

Обратная часть работает нормально. Я получаю градиент, замаскированный через текст, и, возможно, контур.

Что мне нужно, так это получить градиент для заполнения кнопки с текстом «вырезать», чтобы фон просвечивал.

Как я уже сказал, это кажется совершенно очевидным, и у меня, кажется, есть блок.

Есть предположения, что я мог накосячить?

Я мог бы, вероятно, превратить это в игровую площадку, но, может быть, этого достаточно.

Спасибо!

ОБНОВИТЬ:

Вот она как детская площадка:

//: A UIKit based Playground for presenting user interface
  
import UIKit
import PlaygroundSupport

@IBDesignable
class Rcvrr_GradientTextMaskButton: UIButton {
    /* ################################################################## */
    /**
     This contains our text
     */
    var textLayer: CALayer?
    
    /* ################################################################## */
    /**
     The starting color for the gradient.
     */
    @IBInspectable var gradientStartColor: UIColor = .white

    /* ################################################################## */
    /**
     The ending color.
     */
    @IBInspectable var gradientEndColor: UIColor = .black

    /* ################################################################## */
    /**
     The angle of the gradient. 0 (default) is top-to-bottom.
     */
    @IBInspectable var gradientAngleInDegrees: CGFloat = 0

    /* ################################################################## */
    /**
     If true, then the label is reversed, so the background is "cut out" of the foreground.
     */
    @IBInspectable var reversed: Bool = false
}

/* ###################################################################################################################################### */
// MARK: Base Class Overrides
/* ###################################################################################################################################### */
extension Rcvrr_GradientTextMaskButton {
    /* ################################################################## */
    /**
     If the button is "standard" (the text is filled with the gradient), then this method takes care of that.
     */
    override func layoutSubviews() {
        super.layoutSubviews()
        layer.borderColor = UIColor.clear.cgColor
        if let text = titleLabel?.text,
           var dynFont = titleLabel?.font {
            let minimumFontSizeInPoints = (dynFont.pointSize * 0.5)
            let scalingStep = 0.025
            while dynFont.pointSize >= minimumFontSizeInPoints {
                let calcString = NSAttributedString(string: text, attributes: [.font: dynFont])
                let cropRect = calcString.boundingRect(with: CGSize.init(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
                if bounds.size.width >= cropRect.size.width {
                    break
                }
                guard let tempDynFont = UIFont(name: dynFont.fontName, size: dynFont.pointSize - (dynFont.pointSize * scalingStep)) else { break }
                dynFont = tempDynFont
            }
            
            titleLabel?.font = dynFont
        }
        
        if let titleLabel = titleLabel,
           let font = titleLabel.font,
           let text = titleLabel.text {
            let textLayer = CATextLayer()
            textLayer.frame = titleLabel.frame
            textLayer.rasterizationScale = UIScreen.main.scale
            textLayer.contentsScale = UIScreen.main.scale
            textLayer.alignmentMode = .left
            textLayer.fontSize = font.pointSize
            textLayer.font = font
            textLayer.isWrapped = true
            textLayer.truncationMode = .none
            textLayer.string = text
            self.textLayer = textLayer
            titleLabel.textColor = .clear

            let gradient = CAGradientLayer()
            gradient.colors = [gradientStartColor.cgColor, gradientEndColor.cgColor]
            gradient.startPoint = CGPoint(x: 0.5, y: 0)
            gradient.endPoint = CGPoint(x: 0.5, y: 1.0)
            var layerFrame = textLayer.frame

            if !reversed {
                if 0 < layer.borderWidth {
                    let outlineLayer = CAShapeLayer()
                    outlineLayer.frame = bounds
                    outlineLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
                    outlineLayer.lineWidth = layer.borderWidth
                    outlineLayer.strokeColor = UIColor.white.cgColor
                    outlineLayer.fillColor = UIColor.clear.cgColor
                    layerFrame = bounds
                    textLayer.masksToBounds = false
                    if let compositingFilter = CIFilter(name: "CIAdditionCompositing") {
                        textLayer.compositingFilter = compositingFilter
                        outlineLayer.addSublayer(textLayer)
                    }
                    layer.mask = outlineLayer
                } else {
                    layer.mask = textLayer
                }
            } else {
                let outlineLayer = CAShapeLayer()
                outlineLayer.frame = bounds
                textLayer.foregroundColor = UIColor.white.cgColor
                outlineLayer.backgroundColor = UIColor.white.cgColor
                layerFrame = bounds
                textLayer.masksToBounds = false
                if let compositingFilter = CIFilter(name: "CISourceOutCompositing") {
                    outlineLayer.compositingFilter = compositingFilter
                    outlineLayer.addSublayer(textLayer)
                }
                layer.mask = outlineLayer
            }
            
            gradient.frame = layerFrame
            layer.addSublayer(gradient)
        }
    }
}

class MyViewController : UIViewController {
    override func loadView() {
        let view = UIView()
        view.backgroundColor = .yellow

        let button = Rcvrr_GradientTextMaskButton()
        button.frame = CGRect(x: 10, y: 200, width: 300, height: 50)
        button.setTitle("HI", for: .normal)
        button.gradientStartColor = .green
        button.gradientEndColor = .blue
        button.reversed = true
        
        view.addSubview(button)
        self.view = view
    }
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
Формы c голосовым вводом в React с помощью Speechly
Формы c голосовым вводом в React с помощью Speechly
Пытались ли вы когда-нибудь заполнить веб-форму в области электронной коммерции, которая требует много кликов и выбора? Вас попросят заполнить дату,...
Стилизация и валидация html-формы без использования JavaScript (только HTML/CSS)
Стилизация и валидация html-формы без использования JavaScript (только HTML/CSS)
Будучи разработчиком веб-приложений, легко впасть в заблуждение, считая, что приложение без JavaScript не имеет права на жизнь. Нам становится удобно...
Flatpickr: простой модуль календаря для вашего приложения на React
Flatpickr: простой модуль календаря для вашего приложения на React
Если вы ищете пакет для быстрой интеграции календаря с выбором даты в ваше приложения, то библиотека Flatpickr отлично справится с этой задачей....
В чем разница между Promise и Observable?
В чем разница между Promise и Observable?
Разберитесь в этом вопросе, и вы значительно повысите уровень своей компетенции.
Что такое cURL в PHP? Встроенные функции и пример GET запроса
Что такое cURL в PHP? Встроенные функции и пример GET запроса
Клиент для URL-адресов, cURL, позволяет взаимодействовать с множеством различных серверов по множеству различных протоколов с синтаксисом URL.
Четыре эффективных способа центрирования блочных элементов в CSS
Четыре эффективных способа центрирования блочных элементов в CSS
У каждого из нас бывали случаи, когда нам нужно отцентрировать блочный элемент, но мы не знаем, как это сделать. Даже если мы реализуем какой-то...
0
0
31
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Непосредственная проблема заключается в том, что вы помещаете слой градиента Впереди в слой, который маскируется. Ваша маскировка работает, но вы ее скрываете! Это не self.layer вы хотите замаскировать, если хотите увидеть, что здесь что-то происходит; это gradient. Замените layer.mask = на gradient.mask = везде, и вы увидите реальный видимый результат.

Затем вы, вероятно, поймете, что ваша маска сама по себе неисправна, но, по крайней мере, вы не будете просто смотреть на настоящий градиент, задаваясь вопросом, куда делась маска!

Спасибо! Не удивлен, но я в деле. Я опубликую свое окончательное решение.

Chris Marshall 09.04.2022 20:13

Хе. Понятно. Я использовал Ответ, который... кто-то... разместил здесь около пяти лет назад. Я опубликую детскую площадку, как только я уберу ее. Спасибо!

Chris Marshall 09.04.2022 22:02

Я, вероятно, проделаю еще кучу работы над этим, но вот то, что работает сейчас (и отвечает на вопрос).

Спасибо @мат!

//: A UIKit based Playground for presenting user interface
  
import UIKit
import PlaygroundSupport

/* ###################################################################################################################################### */
// MARK: - A Special Button Class That Can Be Filled With A Gradient -
/* ###################################################################################################################################### */
/**
 This class can be displayed with either the text filled with a gradient, or the background filled, and the text "cut out" of it.
 All behavior is the same as any other UIButton.
 
 This allows you to specify a border, which will be included in the gradient fill.
 If the borderWidth value is anything greater than 0, there will be a border, with corners specified by cornerRadius.
 The border will be filled with the gradient, as well as the text.
 
 This is a very, very simple control. I'll probably gussy it up, down the line, but it fills a need, right now.
 */
@IBDesignable
class Rcvrr_GradientTextMaskButton: UIButton {
    /* ################################################################## */
    /**
     This caches the gradient layer.
     */
    private var _gradientLayer: CAGradientLayer?
    
    /* ################################################################## */
    /**
     This caches the mask layer.
     */
    private var _outlineLayer: CAShapeLayer?
    
    /* ################################################################## */
    /**
     The starting color for the gradient.
     */
    @IBInspectable var gradientStartColor: UIColor = .white

    /* ################################################################## */
    /**
     The ending color.
     */
    @IBInspectable var gradientEndColor: UIColor = .black

    /* ################################################################## */
    /**
     The angle of the gradient. 0 (default) is top-to-bottom.
     */
    @IBInspectable var gradientAngleInDegrees: CGFloat = 0

    /* ################################################################## */
    /**
     If true, then the label is reversed, so the background is "cut out" of the foreground.
     */
    @IBInspectable var reversed: Bool = false { didSet { setNeedsLayout() }}
}

/* ###################################################################################################################################### */
// MARK: Computed Properties
/* ###################################################################################################################################### */
extension Rcvrr_GradientTextMaskButton {
    /* ################################################################## */
    /**
     This returns the background gradient layer, rendering it, if necessary.
     */
    var gradientLayer: CALayer? { makeGradientLayer() }
    
    /* ################################################################## */
    /**
     This returns the mask layer, rendering it, if necessary.
     */
    var outlineLayer: CALayer? { makeOutlineLayer() }
}

/* ###################################################################################################################################### */
// MARK: Instance Methods
/* ###################################################################################################################################### */
extension Rcvrr_GradientTextMaskButton {
    /* ################################################################## */
    /**
     This creates the gradient layer, using our specified start and stop colors.
     */
    func makeGradientLayer() -> CALayer? {
        guard nil == _gradientLayer else { return _gradientLayer }
        
        _gradientLayer = CAGradientLayer()
        _gradientLayer?.frame = bounds
        _gradientLayer?.colors = [gradientStartColor.cgColor, gradientEndColor.cgColor]
        _gradientLayer?.startPoint = CGPoint(x: 0.5, y: 0) // .rotated(around: CGPoint(x: 0.5, y: 0.5), byDegrees: gradientAngleInDegrees)
        _gradientLayer?.endPoint = CGPoint(x: 0.5, y: 1.0) // .rotated(around: CGPoint(x: 0.5, y: 0.5), byDegrees: gradientAngleInDegrees)
        
        return _gradientLayer
    }
    
    /* ################################################################## */
    /**
     This uses our text to generate a mask layer.
     */
    func makeOutlineLayer() -> CALayer? {
        guard nil == _outlineLayer else { return _outlineLayer }
        
        if let text = titleLabel?.text,
           var dynFont = titleLabel?.font {
            let minimumFontSizeInPoints = (dynFont.pointSize * 0.5)
            let scalingStep = 0.025
            while dynFont.pointSize >= minimumFontSizeInPoints {
                let calcString = NSAttributedString(string: text, attributes: [.font: dynFont])
                let cropRect = calcString.boundingRect(with: CGSize.init(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
                if bounds.size.width >= cropRect.size.width {
                    break
                }
                guard let tempDynFont = UIFont(name: dynFont.fontName, size: dynFont.pointSize - (dynFont.pointSize * scalingStep)) else { break }
                dynFont = tempDynFont
            }
            
            titleLabel?.font = dynFont
        }
        
        let foreColor = reversed ? UIColor.black.cgColor : UIColor.white.cgColor
        let backColor = reversed ? UIColor.white.cgColor : UIColor.black.cgColor
        
        if let titleLabel = titleLabel,
           let font = titleLabel.font,
           let text = titleLabel.text {
            let textLayer = CATextLayer()
            textLayer.frame = titleLabel.frame
            textLayer.rasterizationScale = UIScreen.main.scale
            textLayer.contentsScale = UIScreen.main.scale
            textLayer.alignmentMode = .left
            textLayer.fontSize = font.pointSize
            textLayer.font = font
            textLayer.isWrapped = true
            textLayer.truncationMode = .none
            textLayer.string = text
            textLayer.foregroundColor = foreColor

            let outlineLayer = CAShapeLayer()
            outlineLayer.frame = bounds
            outlineLayer.strokeColor = foreColor
            outlineLayer.fillColor = backColor

            outlineLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: layer.cornerRadius).cgPath
            outlineLayer.lineWidth = layer.borderWidth
            
            if let compositingFilter = CIFilter(name: "CIAdditionCompositing") {
                textLayer.compositingFilter = compositingFilter
                outlineLayer.addSublayer(textLayer)
                
                self._outlineLayer = outlineLayer
            }
        }
        
        return _outlineLayer
    }
}

/* ###################################################################################################################################### */
// MARK: Base Class Overrides
/* ###################################################################################################################################### */
extension Rcvrr_GradientTextMaskButton {
    /* ################################################################## */
    /**
     We call this, when it's time to layout the control.
     We subvert the standard rendering, and replace it with our own rendering.
     Some of this comes from [this SO answer](https://stackoverflow.com/questions/42238603/reverse-a-calayer-mask/42238699#42238699)
     */
    override func layoutSubviews() {
        super.layoutSubviews()
        // This sets up the baseline.
        _outlineLayer = nil
        backgroundColor = .clear
        layer.borderColor = UIColor.clear.cgColor
        tintColor = .clear
        titleLabel?.textColor = .clear
        _gradientLayer?.removeFromSuperlayer()
        layer.mask = nil
        
        // Create a mask, and apply that to our background gradient.
        if let gradientLayer = gradientLayer,
           let outlineLayer = outlineLayer,
           let filter = CIFilter(name: "CIMaskToAlpha") {
            layer.addSublayer(gradientLayer)
            let renderedCoreImage = CIImage(image: UIGraphicsImageRenderer(size: bounds.size).image { context in return outlineLayer.render(in: context.cgContext) })
            filter.setValue(renderedCoreImage, forKey: "inputImage")
            if let outputImage = filter.outputImage {
                let coreGraphicsImage = CIContext().createCGImage(outputImage, from: outputImage.extent)
                let maskLayer = CALayer()
                maskLayer.frame = bounds
                maskLayer.contents = coreGraphicsImage
                layer.mask = maskLayer
            }
        }
    }
}

class MyViewController : UIViewController {
    var button1: Rcvrr_GradientTextMaskButton!
    var button2: Rcvrr_GradientTextMaskButton!

    override func loadView() {
        let view = UIView()
        view.backgroundColor = .yellow

        button1 = Rcvrr_GradientTextMaskButton()
        button1.frame = CGRect(x: 10, y: 100, width: 300, height: 50)
        button1.setTitle("HI", for: .normal)
        button1.gradientStartColor = .green
        button1.gradientEndColor = .blue
        button1.reversed = true
        button1.addTarget(self, action: #selector(buttonHit), for: .primaryActionTriggered)

        view.addSubview(button1)

        button2 = Rcvrr_GradientTextMaskButton()
        button2.frame = CGRect(x: 10, y: 200, width: 300, height: 50)
        button2.setTitle("BYE", for: .normal)
        button2.gradientStartColor = .green
        button2.gradientEndColor = .blue
        button2.reversed = false
        button2.addTarget(self, action: #selector(buttonHit), for: .primaryActionTriggered)

        view.addSubview(button2)

        self.view = view
    }
    
    @objc func buttonHit(_ inButton: Rcvrr_GradientTextMaskButton) {
        print("Button is\(inButton.reversed ? "" : " not") reversed.")
        if button1 == inButton {
            print("HI!")
            button2.reversed = !inButton.reversed
        } else {
            print("Bye!")
            button1.reversed = !inButton.reversed
        }
    }
}

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

Другие вопросы по теме