123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206 |
- //
- // ConicalGradientLayer.swift
- // IBAnimatable
- //
- // Created by Eric Marchand on 22/03/2018.
- // Copyright © 2018 IBAnimatable. All rights reserved.
- //
- import UIKit
- final class ConicalGradientLayer: CALayer {
- required override init() {
- super.init()
- needsDisplayOnBoundsChange = true
- }
- required init(coder aDecoder: NSCoder) {
- super.init()
- }
- public var colors = [CGColor]() {
- didSet {
- setNeedsDisplay()
- }
- }
- /// Start angle in radians. Defaults to 0.
- public var startAngle: Double = 0.0 {
- didSet {
- setNeedsDisplay()
- }
- }
- /// End angle in radians. Defaults to 2 pi.
- public var endAngle: Double = 2 * .pi {
- didSet {
- setNeedsDisplay()
- }
- }
- public let centerPoint: CGPoint = CGPoint(x: 0.5, y: 0.5)
- public var startPoint: CGPoint {
- get {
- assertionFailure("not implemented")
- return .zero
- }
- set {
- let centeredPoint = CGPoint(x: newValue.x - centerPoint.x,
- y: newValue.y - centerPoint.y)
- var angle = Double(atan2(centeredPoint.y - self.frame.midY, centeredPoint.x - self.frame.midX))
- if angle < 0 {
- angle += 2 * .pi
- }
- self.startAngle = angle
- }
- }
- public var endPoint: CGPoint {
- get {
- assertionFailure("not implemented")
- return .zero
- }
- set {
- let centeredPoint = CGPoint(x: centerPoint.x - newValue.x,
- y: centerPoint.y - newValue.y)
- var angle = Double(atan2(centeredPoint.y - self.frame.midY, centeredPoint.x - self.frame.midX))
- if angle < 0 {
- angle += 2 * .pi
- }
- self.endAngle = angle + 2 * .pi
- }
- }
- /// List of computed color transitions.
- private var transitions = [UIColor.Transition]()
- public override func draw(in ctx: CGContext) {
- UIGraphicsPushContext(ctx)
- draw(in: ctx.boundingBoxOfClipPath)
- UIGraphicsPopContext()
- }
- private func draw(in rect: CGRect) {
- configureTransitions()
- let center = CGPoint(x: rect.width * centerPoint.x,
- y: rect.height * centerPoint.y)
- let longerSide = max(rect.width, rect.height)
- let radius = Double(longerSide) * 2.squareRoot()
- let step = (.pi / 2) / radius
- var angle = startAngle
- while angle <= endAngle {
- let pointX = radius * cos(angle) + Double(center.x)
- let pointY = radius * sin(angle) + Double(center.y)
- let startPoint = CGPoint(x: pointX, y: pointY)
- let line = UIBezierPath()
- line.move(to: startPoint)
- line.addLine(to: center)
- color(forAngle: angle - startAngle, on: (endAngle - startAngle)).setStroke()
- line.stroke()
- angle += step
- }
- }
- private func color(forAngle angle: Double, on arc: Double) -> UIColor {
- let percent = angle / arc
- guard let transition = transition(forPercent: percent) else {
- return UIColor(hue: CGFloat(percent), saturation: 1.0, brightness: 1.0, alpha: 1.0)
- }
- return transition.color(forPercent: percent)
- }
- private func configureTransitions() {
- transitions.removeAll()
- if colors.count > 1 {
- let transitionsCount = colors.count - 1
- let step = 1.0 / Double(transitionsCount)
- for i in 0 ..< transitionsCount {
- let fromLocation = step * Double(i)
- let toLocation = step * Double(i + 1)
- let fromColor = UIColor(cgColor: colors[i])
- let toColor = UIColor(cgColor: colors[i + 1])
- transitions.append(.init(fromLocation: fromLocation,
- toLocation: toLocation,
- fromColor: fromColor,
- toColor: toColor))
- }
- }
- }
- private func transition(forPercent percent: Double) -> UIColor.Transition? {
- let filtered = transitions.filter { percent >= $0.fromLocation && percent < $0.toLocation }
- if let first = filtered.first {
- return first
- }
- return percent <= 0.5 ? transitions.first : transitions.last // only to complete
- }
- }
- // MARK: UIColor
- private extension UIColor {
- struct RGBA {
- var red: CGFloat = 0.0
- var green: CGFloat = 0.0
- var blue: CGFloat = 0.0
- var alpha: CGFloat = 0.0
- init(color: UIColor) {
- color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
- }
- }
- var rgba: RGBA {
- return RGBA(color: self)
- }
- struct Transition {
- let fromLocation: Double
- let toLocation: Double
- let fromColor: UIColor
- let toColor: UIColor
- func color(forPercent percent: Double) -> UIColor {
- let normalizedPercent = percent.normalize(fromMin: fromLocation, max: toLocation)
- return UIColor.lerp(from: fromColor.rgba, to: toColor.rgba, percent: CGFloat(normalizedPercent))
- }
- }
- class func lerp(from: UIColor.RGBA, to: UIColor.RGBA, percent: CGFloat) -> UIColor {
- let red = from.red + percent * (to.red - from.red)
- let green = from.green + percent * (to.green - from.green)
- let blue = from.blue + percent * (to.blue - from.blue)
- let alpha = from.alpha + percent * (to.alpha - from.alpha)
- return UIColor(red: red, green: green, blue: blue, alpha: alpha)
- }
- }
- // MARK: - Double
- private extension Double {
- func normalize(fromMin min: Double, max: Double) -> Double {
- let range = max - min
- if range == 0 {
- return 0
- }
- return (self - min) / range
- }
- }
|