UIBezierPathExtension.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. //
  2. // Created by phimage on 18/05/2017.
  3. // Copyright © 2017 IBAnimatable. All rights reserved.
  4. //
  5. import UIKit
  6. extension UIBezierPath {
  7. // MARK: - Circle
  8. /**
  9. Create a Bezier path for a circle shape.
  10. - Parameter bounds: The bounds of shape.
  11. */
  12. convenience init(circleIn bounds: CGRect) {
  13. let diameter = ceil(min(bounds.width, bounds.height))
  14. let origin = CGPoint(x: (bounds.width - diameter) / 2.0, y: (bounds.height - diameter) / 2.0)
  15. let size = CGSize(width: diameter, height: diameter)
  16. self.init(ovalIn: CGRect(origin: origin, size: size))
  17. }
  18. // MARK: - Triangle
  19. /**
  20. Create a Bezier path for a triangle shape.
  21. */
  22. convenience init(triangleIn bounds: CGRect) {
  23. self.init()
  24. move(to: CGPoint(x: bounds.width / 2.0, y: bounds.origin.y))
  25. addLine(to: CGPoint(x: bounds.width, y: bounds.height))
  26. addLine(to: CGPoint(x: bounds.origin.x, y: bounds.height))
  27. close()
  28. }
  29. // MARK: - Polygon
  30. /**
  31. Create a Bezier path for a polygon shape with provided sides.
  32. - Parameter bounds: The bounds of shape.
  33. - Parameter sides: The number of the polygon sides.
  34. */
  35. convenience init(polygonIn bounds: CGRect, with sides: Int) {
  36. self.init()
  37. let center = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
  38. var angle: CGFloat = -.pi / 2
  39. let angleIncrement = .pi * 2 / CGFloat(sides)
  40. let length = min(bounds.width, bounds.height)
  41. let radius = length / 2.0
  42. move(to: point(from: angle, radius: radius, offset: center))
  43. for _ in 1...sides - 1 {
  44. angle += angleIncrement
  45. self.addLine(to: point(from: angle, radius: radius, offset: center))
  46. }
  47. close()
  48. }
  49. // MARK: - Parallelogram
  50. /**
  51. Create a Bezier path for a parallelogram shape with provided top-left angle.
  52. - Parameter bounds: The bounds of shape.
  53. - Parameter topLeftAngle: The top-left angle of the parallelogram shape.
  54. */
  55. convenience init(parallelogramIn bounds: CGRect, with topLeftAngle: Double) {
  56. self.init()
  57. let topLeftAngleRad = topLeftAngle * .pi / 180
  58. let offset = abs(CGFloat(tan(topLeftAngleRad - .pi / 2)) * bounds.height)
  59. if topLeftAngle <= 90 {
  60. move(to: CGPoint(x: 0, y: 0))
  61. addLine(to: CGPoint(x: bounds.width - offset, y: 0))
  62. addLine(to: CGPoint(x: bounds.width, y: bounds.height))
  63. addLine(to: CGPoint(x: offset, y: bounds.height))
  64. } else {
  65. move(to: CGPoint(x: offset, y: 0))
  66. addLine(to: CGPoint(x: bounds.width, y: 0))
  67. addLine(to: CGPoint(x: bounds.width - offset, y: bounds.height))
  68. addLine(to: CGPoint(x: 0, y: bounds.height))
  69. }
  70. close()
  71. }
  72. // MARK: - Wave
  73. /**
  74. Create a Bezier path for a parallelogram wave with provided prameters.
  75. - Parameter bounds: The bounds of shape.
  76. - Parameter isUp: The flag to indicate whether the wave is up or not.
  77. - Parameter width: The width of the wave shape.
  78. - Parameter offset: The offset of the wave shape.
  79. */
  80. convenience init(waveIn bounds: CGRect, with isUp: Bool, width: CGFloat, offset: CGFloat) {
  81. self.init()
  82. let originY = isUp ? bounds.maxY : bounds.minY
  83. let halfWidth = width / 2.0
  84. let halfHeight = bounds.height / 2.0
  85. let quarterWidth = width / 4.0
  86. var isUp = isUp
  87. var startX = bounds.minX - quarterWidth - (offset.truncatingRemainder(dividingBy: width))
  88. var endX = startX + halfWidth
  89. move(to: CGPoint(x: startX, y: originY))
  90. addLine(to: CGPoint(x: startX, y: bounds.midY))
  91. repeat {
  92. addQuadCurve(
  93. to: CGPoint(x: endX, y: bounds.midY),
  94. controlPoint: CGPoint(
  95. x: startX + quarterWidth,
  96. y: isUp ? bounds.maxY + halfHeight : bounds.minY - halfHeight)
  97. )
  98. startX = endX
  99. endX += halfWidth
  100. isUp = !isUp
  101. } while startX < bounds.maxX
  102. addLine(to: CGPoint(x: currentPoint.x, y: originY))
  103. }
  104. // MARK: - Star
  105. /**
  106. Create a Bezier path for a star shape with provided points.
  107. - Parameter bounds: The bounds of shape.
  108. - Parameter sides: The number of the star points.
  109. */
  110. convenience init(starIn bounds: CGRect, with points: Int, borderWidth: CGFloat = 0) {
  111. self.init()
  112. // Stars must has at least 3 points.
  113. var starPoints = points
  114. if points <= 2 {
  115. starPoints = 5
  116. }
  117. let radius = min(bounds.size.width, bounds.size.height) / 2 - borderWidth
  118. let starExtrusion = radius / 2
  119. let angleIncrement = .pi * 2 / CGFloat(starPoints)
  120. let center = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
  121. var angle: CGFloat = -.pi / 2
  122. for _ in 1...starPoints {
  123. let aPoint = point(from: angle, radius: radius, offset: center)
  124. let nextPoint = point(from: angle + angleIncrement, radius: radius, offset: center)
  125. let midPoint = point(from: angle + angleIncrement / 2.0, radius: starExtrusion, offset: center)
  126. if isEmpty {
  127. move(to: aPoint)
  128. }
  129. addLine(to: midPoint)
  130. addLine(to: nextPoint)
  131. angle += angleIncrement
  132. }
  133. close()
  134. }
  135. /**
  136. Create a Bezier path for a heart shape.
  137. - Parameter bounds: The bounds of shape.
  138. */
  139. convenience init(heartIn bounds: CGRect) {
  140. self.init()
  141. let (x, y, width, height) = bounds.centeredSquare.flatten()
  142. let lowerPoint = CGPoint(x: x + width / 2, y: (y + height ))
  143. move(to: lowerPoint)
  144. addCurve(to: CGPoint(x: x, y: (y + (height / 4))),
  145. controlPoint1: CGPoint(x: (x + (width / 2)), y: (y + (height * 3 / 4))),
  146. controlPoint2: CGPoint(x: x, y: (y + (height / 2))))
  147. addArc(withCenter: CGPoint(x: (x + (width / 4)), y: (y + (height / 4))),
  148. radius: (width / 4),
  149. startAngle: .pi,
  150. endAngle: 0,
  151. clockwise: true)
  152. addArc(withCenter: CGPoint(x: (x + (width * 3 / 4)), y: (y + (height / 4))),
  153. radius: (width / 4),
  154. startAngle: .pi,
  155. endAngle: 0,
  156. clockwise: true)
  157. addCurve(to: lowerPoint,
  158. controlPoint1: CGPoint(x: (x + width), y: (y + (height / 2))),
  159. controlPoint2: CGPoint(x: (x + (width / 2)), y: (y + (height * 3 / 4))))
  160. }
  161. /**
  162. Create a Bezier path for a ring shape.
  163. - Parameter bounds: The bounds of shape.
  164. - Parameter radius: The radius of the shape.
  165. */
  166. convenience init(ringIn bounds: CGRect, radius: CGFloat) {
  167. let center = bounds.center
  168. let (innerRadius, outerRadius) = bounds.radii(for: radius)
  169. self.init()
  170. addArc(withCenter: .zero, radius: innerRadius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
  171. move(to: CGPoint(x: outerRadius, y: 0))
  172. addArc(withCenter: .zero, radius: outerRadius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
  173. self.translate(to: center)
  174. usesEvenOddFillRule = true
  175. }
  176. /**
  177. Create a Bezier path for a gear shape.
  178. - Parameter bounds: The bounds of shape.
  179. - Parameter radius: The radius of the shape.
  180. - Parameter cogs: The number of cogs (min: 2)
  181. */
  182. convenience init(gearIn bounds: CGRect, radius: CGFloat, cogs: Int) {
  183. let center = bounds.center
  184. let (innerRadius, outerRadius) = bounds.radii(for: radius)
  185. self.init()
  186. guard cogs > 2 else {
  187. return
  188. }
  189. let angle: CGFloat = .pi / CGFloat(cogs)
  190. var radius = (outerRadius, innerRadius)
  191. addArc(withCenter: .zero, radius: innerRadius / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true)
  192. move(to: CGPoint(x: radius.0, y: 0))
  193. for _ in 0..<cogs * 2 {
  194. addArc(withCenter: .zero, radius: radius.0, startAngle: 0, endAngle: -angle, clockwise: false)
  195. apply(CGAffineTransform(rotationAngle: angle))
  196. swap(&radius.0, &radius.1)
  197. }
  198. self.translate(to: center)
  199. }
  200. /**
  201. Create a Bezier path for a super ellipse shape.
  202. https://en.wikipedia.org/wiki/Superellipse
  203. - Parameter bounds: The bounds of shape.
  204. - Parameter n: The super ellipse main parameter.
  205. */
  206. convenience init(superEllipseInRect bounds: CGRect, n: CGFloat = CGFloat.𝑒) {
  207. let a = bounds.width / 2
  208. let b = bounds.height / 2
  209. let n_2 = 2 / n
  210. let center = bounds.center
  211. let centerLeft = CGPoint(x: bounds.origin.x, y: bounds.midY)
  212. let x = { (t: CGFloat) -> CGFloat in
  213. let cost = cos(t)
  214. return center.x + cost.sign() * a * pow(abs(cost), n_2)
  215. }
  216. let y = { (t: CGFloat) -> CGFloat in
  217. let sint = sin(t)
  218. return center.y + sint.sign() * b * pow(abs(sint), n_2)
  219. }
  220. self.init()
  221. move(to: centerLeft)
  222. let factor = max((a + b) / 10, 32)
  223. for t in stride(from: (-CGFloat.pi), to: CGFloat.pi, by: CGFloat.pi / factor) {
  224. addLine(to: CGPoint(x: x(t), y: y(t)))
  225. }
  226. close()
  227. }
  228. /**
  229. Create a Bezier path for a drop shape.
  230. - Parameter bounds: The bounds of shape.
  231. */
  232. convenience init(dropInRect bounds: CGRect) {
  233. self.init()
  234. let (x, y, width, height) = bounds.centeredSquare.flatten()
  235. let topPoint = CGPoint(x: x + width / 2, y: 0)
  236. move(to: topPoint)
  237. addCurve(to: CGPoint(x: x + width / 8, y: (y + (height * 5 / 8))),
  238. controlPoint1: CGPoint(x: x + width / 2, y: height / 8),
  239. controlPoint2: CGPoint(x: x + width / 8, y: (y + (height * 3 / 8))))
  240. addArc(withCenter: CGPoint(x: (x + (width / 2)), y: (y + (height * 5 / 8))),
  241. radius: (width * 3 / 8),
  242. startAngle: .pi,
  243. endAngle: 0,
  244. clockwise: false)
  245. addCurve(to: topPoint,
  246. controlPoint1: CGPoint(x: x + width * 7 / 8, y: (y + (height * 3 / 8))),
  247. controlPoint2: CGPoint(x: x + width / 2, y: height / 8))
  248. }
  249. /**
  250. Create a Bezier path for a plus sign shape.
  251. - Parameter bounds: The bounds of shape.
  252. */
  253. convenience init(plusSignInRect bounds: CGRect, width signWidth: CGFloat) {
  254. self.init()
  255. let (x, y, width, height) = bounds/*.centeredSquare*/.flatten()
  256. if signWidth > width {
  257. return
  258. }
  259. let midX = x + width / 2
  260. let midY = y + height / 2
  261. let right = x + width
  262. let left = x
  263. let top = y
  264. let bottom = y + height
  265. move(to: CGPoint(x: midX - signWidth / 2, y: top))
  266. addLine(to: CGPoint(x: midX + signWidth / 2, y: top))
  267. addLine(to: CGPoint(x: midX + signWidth / 2, y: midY - signWidth / 2))
  268. addLine(to: CGPoint(x: right, y: midY - signWidth / 2))
  269. addLine(to: CGPoint(x: right, y: midY + signWidth / 2))
  270. addLine(to: CGPoint(x: midX + signWidth / 2, y: midY + signWidth / 2))
  271. addLine(to: CGPoint(x: midX + signWidth / 2, y: bottom))
  272. addLine(to: CGPoint(x: midX - signWidth / 2, y: bottom))
  273. addLine(to: CGPoint(x: midX - signWidth / 2, y: midY + signWidth / 2))
  274. addLine(to: CGPoint(x: left, y: midY + signWidth / 2))
  275. addLine(to: CGPoint(x: left, y: midY - signWidth / 2))
  276. addLine(to: CGPoint(x: midX - signWidth / 2, y: midY - signWidth / 2))
  277. addLine(to: CGPoint(x: midX - signWidth / 2, y: top))
  278. }
  279. /**
  280. Create a Bezier path for a moon shape.
  281. - Parameter bounds: The bounds of shape.
  282. - Parameter angle: The angle.
  283. */
  284. convenience init(moonInRect bounds: CGRect, with angle: CGFloat) {
  285. self.init()
  286. let radius = ceil(min(bounds.width, bounds.height) / 2)
  287. let radian: CGFloat
  288. if angle > 0 && angle < 180 {
  289. radian = -angle * .pi / 180
  290. } else {
  291. radian = -90 * .pi / 180
  292. }
  293. addArc(withCenter: .zero, radius: radius, startAngle: -radian / 2, endAngle: radian / 2, clockwise: true)
  294. if angle > 0 && angle < 180 {
  295. addArc(withCenter: CGPoint(x: radius * cos(radian / 2.0), y: 0.0),
  296. radius: radius * sin(radian / 2.0), startAngle: CGFloat.pi / 2, endAngle: -CGFloat.pi / 2.0, clockwise: false)
  297. } else {
  298. addLine(to: .zero)
  299. }
  300. close()
  301. self.translate(to: bounds.center)
  302. }
  303. }
  304. private extension UIBezierPath {
  305. func point(from angle: CGFloat, radius: CGFloat, offset: CGPoint) -> CGPoint {
  306. return CGPoint(x: radius * cos(angle) + offset.x, y: radius * sin(angle) + offset.y)
  307. }
  308. func translate(tx: CGFloat, ty: CGFloat) {
  309. apply(CGAffineTransform(translationX: tx, y: ty))
  310. }
  311. func translate(to point: CGPoint) {
  312. apply(CGAffineTransform(translationX: point.x, y: point.y))
  313. }
  314. func rotate(with theta: CGFloat, around origine: CGPoint = .zero) {
  315. guard theta != 0 else {
  316. return
  317. }
  318. if origine != .zero {
  319. translate(to: CGPoint(x: -origine.x, y: -origine.y))
  320. }
  321. apply(CGAffineTransform(rotationAngle: theta))
  322. if origine != .zero {
  323. translate(to: origine)
  324. }
  325. }
  326. }
  327. extension CGFloat {
  328. static let 𝑒 = CGFloat(M_E)
  329. func sign() -> CGFloat {
  330. if self < 0 {
  331. return -1
  332. } else if self > 0 {
  333. return 1
  334. } else {
  335. return 0
  336. }
  337. }
  338. }
  339. private extension CGRect {
  340. var center: CGPoint {
  341. return CGPoint(x: self.midX, y: self.midY)
  342. }
  343. var diameter: CGFloat {
  344. return ceil(min(self.width, self.height))
  345. }
  346. // Return the inner and outer radii
  347. func radii(for radius: CGFloat) -> (CGFloat, CGFloat) {
  348. let diameter = self.diameter
  349. let innerRadius = max(1, diameter / 2 - radius)
  350. let outerRadius = diameter / 2
  351. return (innerRadius, outerRadius)
  352. }
  353. var centeredSquare: CGRect {
  354. let width = ceil(min(size.width, size.height))
  355. let height = width
  356. let newOrigin = CGPoint(x: origin.x + (size.width - width) / 2, y: origin.y + (size.height - height) / 2)
  357. let newSize = CGSize(width: width, height: height)
  358. return CGRect(origin: newOrigin, size: newSize)
  359. }
  360. // swiftlint:disable:next large_tuple
  361. func flatten() -> (CGFloat, CGFloat, CGFloat, CGFloat) {
  362. return (origin.x, origin.y, size.width, size.height)
  363. }
  364. }