J. Doe
J. Doe

Reputation: 13103

Draw radial background on a UIBezierPath

enter image description here

Above picture shows how the following code runs:

extension Int {
    var degreesToRadians: Double { return Double(self) * .pi / 180 }
    var radiansToDegrees: Double { return Double(self) * 180 / .pi }
}
extension FloatingPoint {
    var degreesToRadians: Self { return self * .pi / 180 }
    var radiansToDegrees: Self { return self * 180 / .pi }
}


class SunBurstView: UIView {
    override func draw(_ rect: CGRect) {
        let radius: CGFloat = rect.size.width / 2
        UIColor.yellow.setFill()
        let bezierPath = UIBezierPath()
        let centerPoint = CGPoint(x: rect.origin.x + radius, y: rect.size.height / 2)
        var thisPoint = CGPoint(x: centerPoint.x + radius, y: centerPoint.y + radius)
        bezierPath.move(to: centerPoint)
        var thisAngle: CGFloat = 0.0
        let sliceDegrees: CGFloat = 360.0 / 20 / 2.0
        for _ in 0..<20 {
            let x = radius * CGFloat(cosf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.x
            let y = radius * CGFloat(sinf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.y
            thisPoint = CGPoint(x: x, y: y)
            bezierPath.addLine(to: thisPoint)
            thisAngle += sliceDegrees
            let x2 = radius * CGFloat(cosf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.x
            let y2 = radius * CGFloat(sinf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.y
            thisPoint = CGPoint(x: x2, y: y2)
            bezierPath.addLine(to: thisPoint)
            bezierPath.addLine(to: centerPoint)
            thisAngle += sliceDegrees
        }
        bezierPath.close()
        bezierPath.lineWidth = 0

        let colors = [UIColor.green.cgColor, UIColor.blue.cgColor] as CFArray
        let gradient = CGGradient(colorsSpace: nil, colors: colors, locations: nil)
        let endPosition = min(frame.width, frame.height) / 2
        let center = CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2)
        UIGraphicsGetCurrentContext()?.drawRadialGradient(gradient!, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: endPosition, options: .drawsAfterEndLocation)

        bezierPath.fill()
        bezierPath.stroke()
    }
}

I want that the radial background only fills the UIBezierPath. Normally this could be accomplished with a mask layer, but the gradient does not have a property mask. Any help on how to draw a gradient radial background only on the UIBezierPath is appreciated! It should be transparent on the UIView where there is no UIBezierPath.

Full code with a linear gradient (copy-paste will work):

class SunBurstView: UIView {
    override func draw(_ rect: CGRect) {
        let radius: CGFloat = rect.size.width / 2
        UIColor.yellow.setFill()
        let bezierPath = UIBezierPath()
        let centerPoint = CGPoint(x: rect.origin.x + radius, y: rect.size.height / 2)
        var thisPoint = CGPoint(x: centerPoint.x + radius, y: centerPoint.y + radius)
        bezierPath.move(to: centerPoint)
        var thisAngle: CGFloat = 0.0
        let sliceDegrees: CGFloat = 360.0 / 20 / 2.0
        for _ in 0..<20 {
            let x = radius * CGFloat(cosf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.x
            let y = radius * CGFloat(sinf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.y
            thisPoint = CGPoint(x: x, y: y)
            bezierPath.addLine(to: thisPoint)
            thisAngle += sliceDegrees
            let x2 = radius * CGFloat(cosf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.x
            let y2 = radius * CGFloat(sinf(Float((thisAngle + sliceDegrees).degreesToRadians))) + centerPoint.y
            thisPoint = CGPoint(x: x2, y: y2)
            bezierPath.addLine(to: thisPoint)
            bezierPath.addLine(to: centerPoint)
            thisAngle += sliceDegrees
        }
        bezierPath.close()
//        let colors = [UIColor.green.cgColor, UIColor.blue.cgColor] as CFArray
//        let gradient = CGGradient(colorsSpace: nil, colors: colors, locations: nil)
//        let endPosition = min(frame.width, frame.height) / 2
//        let center = CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2)
//        UIGraphicsGetCurrentContext()?.drawRadialGradient(gradient!, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: endPosition, options: .drawsAfterEndLocation)


        //linear
        let shape = CAShapeLayer()
        shape.path = bezierPath.cgPath
        shape.lineWidth = 0.0
        shape.strokeColor = UIColor.black.cgColor
        self.layer.addSublayer(shape)

        let gradient = CAGradientLayer()
        gradient.frame = bezierPath.bounds
        gradient.colors = [UIColor.blue.cgColor, UIColor.green.cgColor]

        let shapeMask = CAShapeLayer()
        shapeMask.path = bezierPath.cgPath
        gradient.mask = shapeMask

        self.layer.addSublayer(gradient)


        bezierPath.lineWidth = 0
        bezierPath.fill()
        bezierPath.stroke()
    }
}

Upvotes: 2

Views: 714

Answers (2)

Nikolai Ruhe
Nikolai Ruhe

Reputation: 81878

Here's how you can make your original code work with minimal changes.

override func draw(_ rect: CGRect) {
    // [...]
    // your code untouched until this line:
    bezierPath.close()

    // change the rest of the function to:
    bezierPath.fill()

    UIGraphicsGetCurrentContext()!.setBlendMode(.sourceIn)
    let colors = [UIColor.green.cgColor, UIColor.blue.cgColor] as CFArray
    let gradient = CGGradient(colorsSpace: nil, colors: colors, locations: nil)
    let endPosition = min(frame.width, frame.height) / 2
    let center = CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2)
    UIGraphicsGetCurrentContext()?.drawRadialGradient(gradient!, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: endPosition, options: .drawsAfterEndLocation)
}

Result:

Result

The idea is to first draw the bezier with the rays. The color does not matter here, this is only to draw alpha.

Then we draw the gradient on top of the alpha using one of Quartz' peculiar blend modes: kCGBlendModeSourceIn (see Porter-Duff alpha compositing).

This mode draws anything on top of existing alpha, replacing only the pixel's color, leaving alpha as it was. It's basically like using the current drawing's alpha as a mask.

Upvotes: 2

DonMag
DonMag

Reputation: 77690

Here is one implementation of a Radial Gradient Layer:

class RadialGradientLayer: CALayer {

    var center: CGPoint {
        return CGPoint(x: bounds.width/2, y: bounds.height/2)
    }

    var radius: CGFloat {
        return min(bounds.width / 2.0, bounds.height / 2.0)
    }

    var colors: [UIColor] = [UIColor.black, UIColor.lightGray] {
        didSet {
            setNeedsDisplay()
        }
    }

    var cgColors: [CGColor] {
        return colors.map({ (color) -> CGColor in
            return color.cgColor
        })
    }

    override init() {
        super.init()
        needsDisplayOnBoundsChange = true
    }

    required init(coder aDecoder: NSCoder) {
        super.init()
    }

    override func draw(in ctx: CGContext) {
        ctx.saveGState()
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        guard let gradient = CGGradient(colorsSpace: colorSpace, colors: cgColors as CFArray, locations: nil) else {
            return
        }
        ctx.drawRadialGradient(gradient, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: radius, options: CGGradientDrawingOptions(rawValue: 0))
    }

}

So, in your latest code, replace:

    let gradient = CAGradientLayer()
    gradient.frame = bezierPath.bounds
    gradient.colors = [UIColor.blue.cgColor, UIColor.green.cgColor]

with:

    let gradient = RadialGradientLayer()
    gradient.frame = bezierPath.bounds
    gradient.colors = [UIColor.blue, UIColor.green]

Resulting in:

enter image description here

Upvotes: 3

Related Questions