UIBezierPathExtension.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  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 = bounds.center
  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: - Rounded polygon
  50. /**
  51. Create a Bezier path for a rounded polygon shape with provided sides and radius.
  52. - Parameter bounds: The bounds of shape.
  53. - Parameter sides: The number of the polygon sides.
  54. - Parameter cornerRadius: The radius of the polygon corner.
  55. */
  56. convenience init(roundedPolygonInRect bounds: CGRect, with sides: Int, cornerRadius: CGFloat) {
  57. self.init()
  58. let center = bounds.center
  59. var angle: CGFloat = -.pi / 2
  60. let angleIncrement = .pi * 2 / CGFloat(sides)
  61. let length = min(bounds.width, bounds.height)
  62. let r = length / 2.0 - cornerRadius * .pi / 180.0
  63. // englobing polygon points
  64. var points: [CGPoint] = [point(from: angle, radius: r, offset: center)]
  65. for _ in 1...sides - 1 {
  66. angle += angleIncrement
  67. points.append(point(from: angle, radius: r, offset: center))
  68. }
  69. // configure
  70. lineCapStyle = .round
  71. usesEvenOddFillRule = true
  72. // check cornerRadius
  73. var cornerRadius = cornerRadius
  74. if cornerRadius < 0 {
  75. cornerRadius = 0
  76. } else {
  77. let maxCornerRadius = points[0].distance(to: points[1]) / 2.0
  78. if cornerRadius > maxCornerRadius {
  79. cornerRadius = maxCornerRadius
  80. }
  81. }
  82. // Add arc and lines
  83. let len = points.count
  84. addArcPoint(previous: points[len - 1], current: points[0 % len], next: points[1 % len], cornerRadius: cornerRadius, isFirst: true)
  85. for i in 0..<len {
  86. addArcPoint(previous: points[i], current: points[(i + 1) % len], next: points[(i + 2) % len], cornerRadius: cornerRadius, isFirst: false)
  87. }
  88. // close path
  89. close()
  90. }
  91. // MARK: - Parallelogram
  92. /**
  93. Create a Bezier path for a parallelogram shape with provided top-left angle.
  94. - Parameter bounds: The bounds of shape.
  95. - Parameter topLeftAngle: The top-left angle of the parallelogram shape.
  96. */
  97. convenience init(parallelogramIn bounds: CGRect, with topLeftAngle: Double) {
  98. self.init()
  99. let topLeftAngleRad = topLeftAngle * .pi / 180
  100. let offset = abs(CGFloat(tan(topLeftAngleRad - .pi / 2)) * bounds.height)
  101. if topLeftAngle <= 90 {
  102. move(to: CGPoint(x: 0, y: 0))
  103. addLine(to: CGPoint(x: bounds.width - offset, y: 0))
  104. addLine(to: CGPoint(x: bounds.width, y: bounds.height))
  105. addLine(to: CGPoint(x: offset, y: bounds.height))
  106. } else {
  107. move(to: CGPoint(x: offset, y: 0))
  108. addLine(to: CGPoint(x: bounds.width, y: 0))
  109. addLine(to: CGPoint(x: bounds.width - offset, y: bounds.height))
  110. addLine(to: CGPoint(x: 0, y: bounds.height))
  111. }
  112. close()
  113. }
  114. // MARK: - Wave
  115. /**
  116. Create a Bezier path for a parallelogram wave with provided prameters.
  117. - Parameter bounds: The bounds of shape.
  118. - Parameter isUp: The flag to indicate whether the wave is up or not.
  119. - Parameter width: The width of the wave shape.
  120. - Parameter offset: The offset of the wave shape.
  121. */
  122. convenience init(waveIn bounds: CGRect, with isUp: Bool, width: CGFloat, offset: CGFloat) {
  123. self.init()
  124. let originY = isUp ? bounds.maxY : bounds.minY
  125. let halfWidth = width / 2.0
  126. let halfHeight = bounds.height / 2.0
  127. let quarterWidth = width / 4.0
  128. var isUp = isUp
  129. var startX = bounds.minX - quarterWidth - (offset.truncatingRemainder(dividingBy: width))
  130. var endX = startX + halfWidth
  131. move(to: CGPoint(x: startX, y: originY))
  132. addLine(to: CGPoint(x: startX, y: bounds.midY))
  133. repeat {
  134. addQuadCurve(
  135. to: CGPoint(x: endX, y: bounds.midY),
  136. controlPoint: CGPoint(
  137. x: startX + quarterWidth,
  138. y: isUp ? bounds.maxY + halfHeight : bounds.minY - halfHeight)
  139. )
  140. startX = endX
  141. endX += halfWidth
  142. isUp = !isUp
  143. } while startX < bounds.maxX
  144. addLine(to: CGPoint(x: currentPoint.x, y: originY))
  145. }
  146. // MARK: - Star
  147. /**
  148. Create a Bezier path for a star shape with provided points.
  149. - Parameter bounds: The bounds of shape.
  150. - Parameter sides: The number of the star points.
  151. */
  152. convenience init(starIn bounds: CGRect, with points: Int, borderWidth: CGFloat = 0) {
  153. self.init()
  154. // Stars must has at least 3 points.
  155. var starPoints = points
  156. if points <= 2 {
  157. starPoints = 5
  158. }
  159. let radius = min(bounds.size.width, bounds.size.height) / 2 - borderWidth
  160. let starExtrusion = radius / 2
  161. let angleIncrement = .pi * 2 / CGFloat(starPoints)
  162. let center = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
  163. var angle: CGFloat = -.pi / 2
  164. for _ in 1...starPoints {
  165. let aPoint = point(from: angle, radius: radius, offset: center)
  166. let nextPoint = point(from: angle + angleIncrement, radius: radius, offset: center)
  167. let midPoint = point(from: angle + angleIncrement / 2.0, radius: starExtrusion, offset: center)
  168. if isEmpty {
  169. move(to: aPoint)
  170. }
  171. addLine(to: midPoint)
  172. addLine(to: nextPoint)
  173. angle += angleIncrement
  174. }
  175. close()
  176. }
  177. /**
  178. Create a Bezier path for a heart shape.
  179. - Parameter bounds: The bounds of shape.
  180. */
  181. convenience init(heartIn bounds: CGRect) {
  182. self.init()
  183. let (x, y, width, height) = bounds.centeredSquare.flatten()
  184. let lowerPoint = CGPoint(x: x + width / 2, y: (y + height ))
  185. move(to: lowerPoint)
  186. addCurve(to: CGPoint(x: x, y: (y + (height / 4))),
  187. controlPoint1: CGPoint(x: (x + (width / 2)), y: (y + (height * 3 / 4))),
  188. controlPoint2: CGPoint(x: x, y: (y + (height / 2))))
  189. addArc(withCenter: CGPoint(x: (x + (width / 4)), y: (y + (height / 4))),
  190. radius: (width / 4),
  191. startAngle: .pi,
  192. endAngle: 0,
  193. clockwise: true)
  194. addArc(withCenter: CGPoint(x: (x + (width * 3 / 4)), y: (y + (height / 4))),
  195. radius: (width / 4),
  196. startAngle: .pi,
  197. endAngle: 0,
  198. clockwise: true)
  199. addCurve(to: lowerPoint,
  200. controlPoint1: CGPoint(x: (x + width), y: (y + (height / 2))),
  201. controlPoint2: CGPoint(x: (x + (width / 2)), y: (y + (height * 3 / 4))))
  202. }
  203. /**
  204. Create a Bezier path for a ring shape.
  205. - Parameter bounds: The bounds of shape.
  206. - Parameter radius: The radius of the shape.
  207. */
  208. convenience init(ringIn bounds: CGRect, radius: CGFloat) {
  209. let center = bounds.center
  210. let (innerRadius, outerRadius) = bounds.radii(for: radius)
  211. self.init()
  212. addArc(withCenter: .zero, radius: innerRadius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
  213. move(to: CGPoint(x: outerRadius, y: 0))
  214. addArc(withCenter: .zero, radius: outerRadius, startAngle: 0, endAngle: .pi * 2, clockwise: true)
  215. self.translate(to: center)
  216. usesEvenOddFillRule = true
  217. }
  218. /**
  219. Create a Bezier path for a gear shape.
  220. - Parameter bounds: The bounds of shape.
  221. - Parameter radius: The radius of the shape.
  222. - Parameter cogs: The number of cogs (min: 2)
  223. */
  224. convenience init(gearIn bounds: CGRect, radius: CGFloat, cogs: Int) {
  225. let center = bounds.center
  226. let (innerRadius, outerRadius) = bounds.radii(for: radius)
  227. self.init()
  228. guard cogs > 2 else {
  229. return
  230. }
  231. let angle: CGFloat = .pi / CGFloat(cogs)
  232. var radius = (outerRadius, innerRadius)
  233. addArc(withCenter: .zero, radius: innerRadius / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true)
  234. move(to: CGPoint(x: radius.0, y: 0))
  235. for _ in 0..<cogs * 2 {
  236. addArc(withCenter: .zero, radius: radius.0, startAngle: 0, endAngle: -angle, clockwise: false)
  237. apply(CGAffineTransform(rotationAngle: angle))
  238. swap(&radius.0, &radius.1)
  239. }
  240. self.translate(to: center)
  241. }
  242. /**
  243. Create a Bezier path for a super ellipse shape.
  244. https://en.wikipedia.org/wiki/Superellipse
  245. - Parameter bounds: The bounds of shape.
  246. - Parameter n: The super ellipse main parameter.
  247. */
  248. convenience init(superEllipseInRect bounds: CGRect, n: CGFloat = CGFloat.𝑒) {
  249. let center = bounds.center
  250. let a = center.x
  251. let b = center.y
  252. let n_2 = 2 / n
  253. let centerLeft = CGPoint(x: bounds.origin.x, y: bounds.midY)
  254. let x = { (t: CGFloat) -> CGFloat in
  255. let cost = cos(t)
  256. return center.x + cost.sign() * a * pow(abs(cost), n_2)
  257. }
  258. let y = { (t: CGFloat) -> CGFloat in
  259. let sint = sin(t)
  260. return center.y + sint.sign() * b * pow(abs(sint), n_2)
  261. }
  262. self.init()
  263. move(to: centerLeft)
  264. let factor = max((a + b) / 10, 32)
  265. for t in stride(from: (-CGFloat.pi), to: CGFloat.pi, by: CGFloat.pi / factor) {
  266. addLine(to: CGPoint(x: x(t), y: y(t)))
  267. }
  268. close()
  269. }
  270. /**
  271. Create a Bezier path for a drop shape.
  272. - Parameter bounds: The bounds of shape.
  273. */
  274. convenience init(dropInRect bounds: CGRect) {
  275. self.init()
  276. let (x, y, width, height) = bounds.centeredSquare.flatten()
  277. let topPoint = CGPoint(x: x + width / 2, y: 0)
  278. move(to: topPoint)
  279. addCurve(to: CGPoint(x: x + width / 8, y: (y + (height * 5 / 8))),
  280. controlPoint1: CGPoint(x: x + width / 2, y: height / 8),
  281. controlPoint2: CGPoint(x: x + width / 8, y: (y + (height * 3 / 8))))
  282. addArc(withCenter: CGPoint(x: (x + (width / 2)), y: (y + (height * 5 / 8))),
  283. radius: (width * 3 / 8),
  284. startAngle: .pi,
  285. endAngle: 0,
  286. clockwise: false)
  287. addCurve(to: topPoint,
  288. controlPoint1: CGPoint(x: x + width * 7 / 8, y: (y + (height * 3 / 8))),
  289. controlPoint2: CGPoint(x: x + width / 2, y: height / 8))
  290. }
  291. /**
  292. Create a Bezier path for a plus sign shape.
  293. - Parameter bounds: The bounds of shape.
  294. */
  295. convenience init(plusSignInRect bounds: CGRect, width signWidth: CGFloat) {
  296. self.init()
  297. let (x, y, width, height) = bounds/*.centeredSquare*/.flatten()
  298. if signWidth > width {
  299. return
  300. }
  301. let midX = x + width / 2
  302. let midY = y + height / 2
  303. let right = x + width
  304. let left = x
  305. let top = y
  306. let bottom = y + height
  307. move(to: CGPoint(x: midX - signWidth / 2, y: top))
  308. addLine(to: CGPoint(x: midX + signWidth / 2, y: top))
  309. addLine(to: CGPoint(x: midX + signWidth / 2, y: midY - signWidth / 2))
  310. addLine(to: CGPoint(x: right, y: midY - signWidth / 2))
  311. addLine(to: CGPoint(x: right, y: midY + signWidth / 2))
  312. addLine(to: CGPoint(x: midX + signWidth / 2, y: midY + signWidth / 2))
  313. addLine(to: CGPoint(x: midX + signWidth / 2, y: bottom))
  314. addLine(to: CGPoint(x: midX - signWidth / 2, y: bottom))
  315. addLine(to: CGPoint(x: midX - signWidth / 2, y: midY + signWidth / 2))
  316. addLine(to: CGPoint(x: left, y: midY + signWidth / 2))
  317. addLine(to: CGPoint(x: left, y: midY - signWidth / 2))
  318. addLine(to: CGPoint(x: midX - signWidth / 2, y: midY - signWidth / 2))
  319. addLine(to: CGPoint(x: midX - signWidth / 2, y: top))
  320. }
  321. /**
  322. Create a Bezier path for a moon shape.
  323. - Parameter bounds: The bounds of shape.
  324. - Parameter angle: The angle.
  325. */
  326. convenience init(moonInRect bounds: CGRect, with angle: CGFloat) {
  327. self.init()
  328. let radius = ceil(min(bounds.width, bounds.height) / 2)
  329. let radian: CGFloat
  330. if angle > 0 && angle < 180 {
  331. radian = -angle * .pi / 180
  332. } else {
  333. radian = -90 * .pi / 180
  334. }
  335. addArc(withCenter: .zero, radius: radius, startAngle: -radian / 2, endAngle: radian / 2, clockwise: true)
  336. if angle > 0 && angle < 180 {
  337. addArc(withCenter: CGPoint(x: radius * cos(radian / 2.0), y: 0.0),
  338. radius: radius * sin(radian / 2.0), startAngle: CGFloat.pi / 2, endAngle: -CGFloat.pi / 2.0, clockwise: false)
  339. } else {
  340. addLine(to: .zero)
  341. }
  342. close()
  343. self.translate(to: bounds.center)
  344. }
  345. /**
  346. Create a Bezier path for a kite shape.
  347. - Parameter bounds: The bounds of shape.
  348. - Parameter angle: The angle.
  349. */
  350. convenience init(kiteInRect bounds: CGRect, with angle: CGFloat) {
  351. self.init()
  352. let topAngleRad = angle * .pi / 180
  353. let offset = abs(CGFloat(sin(topAngleRad - .pi / 2))) * bounds.height
  354. if angle <= 90 {
  355. move(to: CGPoint(x: bounds.width / 2, y: 0))
  356. addLine(to: CGPoint(x: bounds.width, y: offset))
  357. addLine(to: CGPoint(x: bounds.width / 2, y: bounds.height))
  358. addLine(to: CGPoint(x: 0, y: offset))
  359. } else { // dart shape
  360. move(to: CGPoint(x: bounds.width / 2, y: offset))
  361. addLine(to: CGPoint(x: bounds.width, y: 0))
  362. addLine(to: CGPoint(x: bounds.width / 2, y: bounds.height))
  363. addLine(to: CGPoint(x: 0, y: 0))
  364. }
  365. close()
  366. }
  367. }
  368. private extension UIBezierPath {
  369. func point(from angle: CGFloat, radius: CGFloat, offset: CGPoint) -> CGPoint {
  370. return CGPoint(x: radius * cos(angle) + offset.x, y: radius * sin(angle) + offset.y)
  371. }
  372. func translate(tx: CGFloat, ty: CGFloat) {
  373. apply(CGAffineTransform(translationX: tx, y: ty))
  374. }
  375. func translate(to point: CGPoint) {
  376. apply(CGAffineTransform(translationX: point.x, y: point.y))
  377. }
  378. func rotate(with theta: CGFloat, around origine: CGPoint = .zero) {
  379. guard theta != 0 else {
  380. return
  381. }
  382. if origine != .zero {
  383. translate(to: CGPoint(x: -origine.x, y: -origine.y))
  384. }
  385. apply(CGAffineTransform(rotationAngle: theta))
  386. if origine != .zero {
  387. translate(to: origine)
  388. }
  389. }
  390. private func addArcPoint(previous: CGPoint, current: CGPoint, next: CGPoint, cornerRadius: CGFloat, isFirst: Bool) {
  391. var c2p = CGPoint(x: previous.x - current.x, y: previous.y - current.y) // current & previous
  392. var c2n = CGPoint(x: next.x - current.x, y: next.y - current.y) // current & next
  393. let distanceP = c2p.distance(to: .zero)
  394. let distanceN = c2p.distance(to: .zero)
  395. c2p.x /= distanceP
  396. c2p.y /= distanceP
  397. c2n.x /= distanceN
  398. c2n.y /= distanceN
  399. let ω = acos(c2n.x * c2p.x + c2n.y * c2p.y)
  400. let θ = (.pi / 2) - (ω / 2)
  401. let radius = cornerRadius / θ * (.pi / 4)
  402. let rTanθ = radius * tan(θ)
  403. if isFirst {
  404. let end = CGPoint(x: current.x + rTanθ * c2n.x, y: current.y + rTanθ * c2n.y)
  405. move(to: end)
  406. } else {
  407. let start = CGPoint(x: current.x + rTanθ * c2p.x, y: current.y + rTanθ * c2p.y)
  408. addLine(to: start)
  409. let center = CGPoint(x: start.x + c2p.y * radius, y: start.y - c2p.x * radius)
  410. let startAngle = atan2(c2p.x, -c2p.y)
  411. let endAngle = startAngle + (2 * θ)
  412. addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
  413. }
  414. }
  415. }
  416. extension CGFloat {
  417. static let 𝑒 = CGFloat(M_E)
  418. func sign() -> CGFloat {
  419. if self < 0 {
  420. return -1
  421. } else if self > 0 {
  422. return 1
  423. } else {
  424. return 0
  425. }
  426. }
  427. }
  428. private extension CGRect {
  429. var center: CGPoint {
  430. return CGPoint(x: self.midX, y: self.midY)
  431. }
  432. var diameter: CGFloat {
  433. return ceil(min(self.width, self.height))
  434. }
  435. // Return the inner and outer radii
  436. func radii(for radius: CGFloat) -> (CGFloat, CGFloat) {
  437. let diameter = self.diameter
  438. let innerRadius = max(1, diameter / 2 - radius)
  439. let outerRadius = diameter / 2
  440. return (innerRadius, outerRadius)
  441. }
  442. var centeredSquare: CGRect {
  443. let width = ceil(min(size.width, size.height))
  444. let height = width
  445. let newOrigin = CGPoint(x: origin.x + (size.width - width) / 2, y: origin.y + (size.height - height) / 2)
  446. let newSize = CGSize(width: width, height: height)
  447. return CGRect(origin: newOrigin, size: newSize)
  448. }
  449. // swiftlint:disable:next large_tuple
  450. func flatten() -> (CGFloat, CGFloat, CGFloat, CGFloat) {
  451. return (origin.x, origin.y, size.width, size.height)
  452. }
  453. }
  454. fileprivate extension CGPoint {
  455. func distance(to point: CGPoint) -> CGFloat {
  456. return hypot(self.x - point.x, self.y - point.y)
  457. }
  458. }
  459. // swiftlint:disable:this file_length