Reputation: 13103
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
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:
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
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:
Upvotes: 3