ConicalGradientLayer.swift 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. //
  2. // ConicalGradientLayer.swift
  3. // IBAnimatable
  4. //
  5. // Created by Eric Marchand on 22/03/2018.
  6. // Copyright © 2018 IBAnimatable. All rights reserved.
  7. //
  8. import UIKit
  9. final class ConicalGradientLayer: CALayer {
  10. required override init() {
  11. super.init()
  12. needsDisplayOnBoundsChange = true
  13. }
  14. required init(coder aDecoder: NSCoder) {
  15. super.init()
  16. }
  17. public var colors = [CGColor]() {
  18. didSet {
  19. setNeedsDisplay()
  20. }
  21. }
  22. /// Start angle in radians. Defaults to 0.
  23. public var startAngle: Double = 0.0 {
  24. didSet {
  25. setNeedsDisplay()
  26. }
  27. }
  28. /// End angle in radians. Defaults to 2 pi.
  29. public var endAngle: Double = 2 * .pi {
  30. didSet {
  31. setNeedsDisplay()
  32. }
  33. }
  34. public let centerPoint: CGPoint = CGPoint(x: 0.5, y: 0.5)
  35. public var startPoint: CGPoint {
  36. get {
  37. assertionFailure("not implemented")
  38. return .zero
  39. }
  40. set {
  41. let centeredPoint = CGPoint(x: newValue.x - centerPoint.x,
  42. y: newValue.y - centerPoint.y)
  43. var angle = Double(atan2(centeredPoint.y - self.frame.midY, centeredPoint.x - self.frame.midX))
  44. if angle < 0 {
  45. angle += 2 * .pi
  46. }
  47. self.startAngle = angle
  48. }
  49. }
  50. public var endPoint: CGPoint {
  51. get {
  52. assertionFailure("not implemented")
  53. return .zero
  54. }
  55. set {
  56. let centeredPoint = CGPoint(x: centerPoint.x - newValue.x,
  57. y: centerPoint.y - newValue.y)
  58. var angle = Double(atan2(centeredPoint.y - self.frame.midY, centeredPoint.x - self.frame.midX))
  59. if angle < 0 {
  60. angle += 2 * .pi
  61. }
  62. self.endAngle = angle + 2 * .pi
  63. }
  64. }
  65. /// List of computed color transitions.
  66. private var transitions = [UIColor.Transition]()
  67. public override func draw(in ctx: CGContext) {
  68. UIGraphicsPushContext(ctx)
  69. draw(in: ctx.boundingBoxOfClipPath)
  70. UIGraphicsPopContext()
  71. }
  72. private func draw(in rect: CGRect) {
  73. configureTransitions()
  74. let center = CGPoint(x: rect.width * centerPoint.x,
  75. y: rect.height * centerPoint.y)
  76. let longerSide = max(rect.width, rect.height)
  77. let radius = Double(longerSide) * 2.squareRoot()
  78. let step = (.pi / 2) / radius
  79. var angle = startAngle
  80. while angle <= endAngle {
  81. let pointX = radius * cos(angle) + Double(center.x)
  82. let pointY = radius * sin(angle) + Double(center.y)
  83. let startPoint = CGPoint(x: pointX, y: pointY)
  84. let line = UIBezierPath()
  85. line.move(to: startPoint)
  86. line.addLine(to: center)
  87. color(forAngle: angle - startAngle, on: (endAngle - startAngle)).setStroke()
  88. line.stroke()
  89. angle += step
  90. }
  91. }
  92. private func color(forAngle angle: Double, on arc: Double) -> UIColor {
  93. let percent = angle / arc
  94. guard let transition = transition(forPercent: percent) else {
  95. return UIColor(hue: CGFloat(percent), saturation: 1.0, brightness: 1.0, alpha: 1.0)
  96. }
  97. return transition.color(forPercent: percent)
  98. }
  99. private func configureTransitions() {
  100. transitions.removeAll()
  101. if colors.count > 1 {
  102. let transitionsCount = colors.count - 1
  103. let step = 1.0 / Double(transitionsCount)
  104. for i in 0 ..< transitionsCount {
  105. let fromLocation = step * Double(i)
  106. let toLocation = step * Double(i + 1)
  107. let fromColor = UIColor(cgColor: colors[i])
  108. let toColor = UIColor(cgColor: colors[i + 1])
  109. transitions.append(.init(fromLocation: fromLocation,
  110. toLocation: toLocation,
  111. fromColor: fromColor,
  112. toColor: toColor))
  113. }
  114. }
  115. }
  116. private func transition(forPercent percent: Double) -> UIColor.Transition? {
  117. let filtered = transitions.filter { percent >= $0.fromLocation && percent < $0.toLocation }
  118. if let first = filtered.first {
  119. return first
  120. }
  121. return percent <= 0.5 ? transitions.first : transitions.last // only to complete
  122. }
  123. }
  124. // MARK: UIColor
  125. private extension UIColor {
  126. struct RGBA {
  127. var red: CGFloat = 0.0
  128. var green: CGFloat = 0.0
  129. var blue: CGFloat = 0.0
  130. var alpha: CGFloat = 0.0
  131. init(color: UIColor) {
  132. color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
  133. }
  134. }
  135. var rgba: RGBA {
  136. return RGBA(color: self)
  137. }
  138. struct Transition {
  139. let fromLocation: Double
  140. let toLocation: Double
  141. let fromColor: UIColor
  142. let toColor: UIColor
  143. func color(forPercent percent: Double) -> UIColor {
  144. let normalizedPercent = percent.normalize(fromMin: fromLocation, max: toLocation)
  145. return UIColor.lerp(from: fromColor.rgba, to: toColor.rgba, percent: CGFloat(normalizedPercent))
  146. }
  147. }
  148. class func lerp(from: UIColor.RGBA, to: UIColor.RGBA, percent: CGFloat) -> UIColor {
  149. let red = from.red + percent * (to.red - from.red)
  150. let green = from.green + percent * (to.green - from.green)
  151. let blue = from.blue + percent * (to.blue - from.blue)
  152. let alpha = from.alpha + percent * (to.alpha - from.alpha)
  153. return UIColor(red: red, green: green, blue: blue, alpha: alpha)
  154. }
  155. }
  156. // MARK: - Double
  157. private extension Double {
  158. func normalize(fromMin min: Double, max: Double) -> Double {
  159. let range = max - min
  160. if range == 0 {
  161. return 0
  162. }
  163. return (self - min) / range
  164. }
  165. }