M13CheckboxCheckPathGenerator.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. //
  2. // M13CheckboxCheckPathGenerator.swift
  3. // M13Checkbox
  4. //
  5. // Created by McQuilkin, Brandon on 10/6/16.
  6. // Copyright © 2016 Brandon McQuilkin. All rights reserved.
  7. //
  8. // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
  9. //
  10. // The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
  11. //
  12. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  13. import UIKit
  14. internal class M13CheckboxCheckPathGenerator: M13CheckboxPathGenerator {
  15. //----------------------------
  16. // MARK: - Structures
  17. //----------------------------
  18. /// Contains the geometry information needed to generate the checkmark, as well as generates the locations of the feature points.
  19. struct CheckmarkProperties {
  20. /// The angle between the x-axis, and the line created between the origin, and the location where the extended long arm of the checkmark meets the box. (Diagram: Θ)
  21. var longArmBoxIntersectionAngle: CGFloat = 45.0 * CGFloat(Double.pi / 180.0)
  22. /// The distance from the center the long arm of the checkmark draws to, as a percentage of size. (Diagram: S)
  23. var longArmRadius: (circle: CGFloat, box: CGFloat) = (circle: 0.22, box: 0.33)
  24. /// The distance from the center of the middle/bottom point of the checkbox, as a percentage of size. (Diagram: T)
  25. var middlePointRadius: (circle: CGFloat, box: CGFloat) = (circle: 0.133, box: 0.1995)
  26. /// The distance between the horizontal center and the middle point of the checkbox.
  27. var middlePointOffset: (circle: CGFloat, box: CGFloat) = (circle: -0.04, box: -0.06)
  28. /// The distance from the center of the left most point of the checkmark, as a percentage of size.
  29. var shortArmRadius: (circle: CGFloat, box: CGFloat) = (circle: 0.17, box: 0.255)
  30. /// The distance between the vertical center and the left most point of the checkmark, as a percentage of size.
  31. var shortArmOffset: (circle: CGFloat, box: CGFloat) = (circle: 0.02, box: 0.03)
  32. }
  33. //----------------------------
  34. // MARK: - Properties
  35. //----------------------------
  36. /// The parameters that define the checkmark.
  37. var checkmarkProperties: CheckmarkProperties = CheckmarkProperties()
  38. //----------------------------
  39. // MARK: - Points of Intrest
  40. //----------------------------
  41. /// The intersection point between the extended long checkmark arm, and the box.
  42. var checkmarkLongArmBoxIntersectionPoint: CGPoint {
  43. let cornerRadius: CGFloat = self.cornerRadius
  44. let boxLineWidth: CGFloat = self.boxLineWidth
  45. let size: CGFloat = self.size
  46. let radius: CGFloat = (size - boxLineWidth) / 2.0
  47. let theta:CGFloat = checkmarkProperties.longArmBoxIntersectionAngle
  48. if boxType == .circle {
  49. // Basic trig to get the location of the point on the circle.
  50. let x: CGFloat = (size / 2.0) + (radius * cos(theta))
  51. let y: CGFloat = (size / 2.0) - (radius * sin(theta))
  52. return CGPoint(x: x, y: y)
  53. } else {
  54. // We need to differentiate between the box edges and the rounded corner.
  55. let lineOffset: CGFloat = boxLineWidth / 2.0
  56. let circleX: CGFloat = size - lineOffset - cornerRadius
  57. let circleY: CGFloat = 0.0 + lineOffset + cornerRadius
  58. let edgeX: CGFloat = (size / 2.0) + (0.5 * (size - boxLineWidth) * (1.0 / tan(theta)))
  59. let edgeY: CGFloat = (size / 2.0) - (0.5 * (size - boxLineWidth) * tan(theta));
  60. if edgeX <= circleX {
  61. // On the top edge.
  62. return CGPoint(x: edgeX, y: lineOffset)
  63. } else if edgeY >= circleY {
  64. // On the right edge.
  65. let x: CGFloat = size - lineOffset
  66. return CGPoint(x: x, y: edgeY)
  67. } else {
  68. // On the corner
  69. let cos2Theta: CGFloat = cos(2.0 * theta)
  70. let sin2Theta: CGFloat = sin(2.0 * theta)
  71. let powC: CGFloat = pow((-2.0 * cornerRadius) + size, 2.0)
  72. let a: CGFloat = size * (3.0 + cos2Theta + sin2Theta)
  73. let b: CGFloat = -2.0 * cornerRadius * (cos(theta) + sin(theta))
  74. let c: CGFloat = (((4.0 * cornerRadius) - size) * size) + (powC * sin2Theta)
  75. let d: CGFloat = size * cos(theta) * (cos(theta) - sin(theta))
  76. let e: CGFloat = 2.0 * cornerRadius * sin(theta) * (cos(theta) + sin(theta))
  77. let x: CGFloat = 0.25 * (a + (2.0 * (b + sqrt(c)) * cos(theta))) - boxLineWidth
  78. let y: CGFloat = 0.50 * (d + e - (sqrt(c) * sin(theta))) + boxLineWidth
  79. return CGPoint(x: x, y: y)
  80. }
  81. }
  82. }
  83. var checkmarkLongArmEndPoint: CGPoint {
  84. let size: CGFloat = self.size
  85. let boxLineWidth: CGFloat = self.boxLineWidth
  86. // Known variables
  87. let boxEndPoint: CGPoint = checkmarkLongArmBoxIntersectionPoint
  88. let x2: CGFloat = boxEndPoint.x
  89. let y2: CGFloat = boxEndPoint.y
  90. let midPoint: CGPoint = checkmarkMiddlePoint
  91. let x1: CGFloat = midPoint.x
  92. let y1: CGFloat = midPoint.y
  93. let r: CGFloat = boxType == .circle ? size * checkmarkProperties.longArmRadius.circle : size * checkmarkProperties.longArmRadius.box
  94. let a1: CGFloat = (size * pow(x1, 2.0)) - (2.0 * size * x1 * x2) + (size * pow(x2, 2.0)) + (size * x1 * y1) - (size * x2 * y1)
  95. let a2: CGFloat = (2.0 * x2 * pow(y1, 2.0)) - (size * x1 * y2) + (size * x2 * y2) - (2.0 * x1 * y1 * y2) - (2.0 * x2 * y1 * y2) + (2.0 * x1 * pow(y2, 2.0))
  96. let b: CGFloat = -16.0 * (pow(x1, 2.0) - (2.0 * x1 * x2) + pow(x2, 2.0) + pow(y1, 2.0) - (2.0 * y1 * y2) + pow(y2, 2.0))
  97. let c1: CGFloat = pow(r, 2.0) * ((-pow(x1, 2.0)) + (2.0 * x1 * x2) - pow(x2, 2.0))
  98. let c2: CGFloat = pow(size, 2.0) * ((0.5 * pow(x1, 2.0)) - (x1 * x2) + (0.5 * pow(x2, 2.0)))
  99. let d1: CGFloat = (pow(x2, 2.0) * pow(y1, 2.0)) - (2.0 * x1 * x2 * y1 * y2) + (pow(x1, 2.0) * pow(y2, 2.0))
  100. let d2: CGFloat = size * ((x1 * x2 * y1) - (pow(x2, 2.0) * y1) - (pow(x1, 2.0) * y2) + (x1 * x2 * y2))
  101. let cd: CGFloat = c1 + c2 + d1 + d2
  102. let e1: CGFloat = (x1 * ((4.0 * y1) - (4.0 * y2)) * y2) + (x2 * y1 * ((-4.0 * y1) + (4.0 * y2)))
  103. let e2: CGFloat = size * ((-2.0 * pow(x1, 2.0)) + (x2 * ((-2.0 * x2) + (2.0 * y1) - (2.0 * y2))) + (x1 * (4.0 * x2 - (2.0 * y1) + (2.0 * y2))))
  104. let f: CGFloat = pow(x1, 2.0) - (2.0 * x1 * x2) + pow(x2, 2.0) + pow(y1, 2.0) - (2.0 * y1 * y2) + pow(y2, 2)
  105. let g1: CGFloat = (0.5 * size * x1 * y1) - (0.5 * size * x2 * y1) - (x1 * x2 * y1) + (pow(x2, 2.0) * y1) + (0.5 * size * pow(y1, 2.0))
  106. let g2: CGFloat = (-0.5 * size * x1 * y2) + (pow(x1, 2.0) * y2) + (0.5 * size * x2 * y2) - (x1 * x2 * y2) - (size * y1 * y2) + (0.5 * size * pow(y2, 2.0))
  107. let h1: CGFloat = (-4.0 * pow(x2, 2.0) * y1) - (4.0 * pow(x1, 2.0) * y2) + (x1 * x2 * ((4.0 * y1) + (4.0 * y2)))
  108. let h2: CGFloat = size * ((-2.0 * x1 * y1) + (2.0 * x2 * y1) - (2.0 * pow(y1, 2.0)) + (2.0 * x1 * y2) - (2.0 * x2 * y2) + (4.0 * y1 * y2) - (2.0 * pow(y2, 2.0)))
  109. let i: CGFloat = (pow(r, 2.0) * (-pow(y1, 2.0) + (2.0 * y1 * y2) - pow(y2, 2.0))) + (pow(size, 2.0) * ((0.5 * pow(y1, 2.0)) - (y1 * y2) + (0.5 * pow(y2, 2.0))))
  110. let j: CGFloat = size * ((x1 * (y1 - y2) * y2) + (x2 * y1 * (-y1 + y2)))
  111. let powE1E2: CGFloat = pow(e1 + e2, 2.0)
  112. let subX1: CGFloat = (b * cd) + powE1E2
  113. let subX2: CGFloat = (a1 + a2 + (0.5 * sqrt(subX1)))
  114. let powH1H2: CGFloat = pow(h1 + h2, 2.0)
  115. let subY1: CGFloat = powH1H2 + (b * (d1 + i + j))
  116. let subY2: CGFloat = (0.25 * sqrt(subY1))
  117. let x: CGFloat = (0.5 * subX2 + (boxLineWidth / 2.0)) / f
  118. let y: CGFloat = (g1 + g2 - subY2 + (boxLineWidth / 2.0)) / f
  119. return CGPoint(x: x, y: y)
  120. }
  121. var checkmarkMiddlePoint: CGPoint {
  122. let r: CGFloat = boxType == .circle ? checkmarkProperties.middlePointRadius.circle : checkmarkProperties.middlePointRadius.box
  123. let o: CGFloat = boxType == .circle ? checkmarkProperties.middlePointOffset.circle : checkmarkProperties.middlePointOffset.box
  124. let x: CGFloat = (size / 2.0) + (size * o)
  125. let y: CGFloat = (size / 2.0 ) + (size * r)
  126. return CGPoint(x: x, y: y)
  127. }
  128. var checkmarkShortArmEndPoint: CGPoint {
  129. let r: CGFloat = boxType == .circle ? checkmarkProperties.shortArmRadius.circle : checkmarkProperties.shortArmRadius.box
  130. let o: CGFloat = boxType == .circle ? checkmarkProperties.shortArmOffset.circle : checkmarkProperties.shortArmOffset.box
  131. let x: CGFloat = (size / 2.0) - (size * r)
  132. let y: CGFloat = (size / 2.0) + (size * o)
  133. return CGPoint(x: x, y: y)
  134. }
  135. //----------------------------
  136. // MARK: - Box Paths
  137. //----------------------------
  138. override func pathForCircle() -> UIBezierPath? {
  139. let radius = (size - boxLineWidth) / 2.0
  140. // Create a circle that starts in the top right hand corner.
  141. return UIBezierPath(arcCenter: CGPoint(x: size / 2.0, y: size / 2.0),
  142. radius: radius,
  143. startAngle: -checkmarkProperties.longArmBoxIntersectionAngle,
  144. endAngle: CGFloat(2 * Double.pi) - checkmarkProperties.longArmBoxIntersectionAngle,
  145. clockwise: true)
  146. }
  147. override func pathForRoundedRect() -> UIBezierPath? {
  148. let path = UIBezierPath()
  149. let lineOffset: CGFloat = boxLineWidth / 2.0
  150. let trX: CGFloat = size - lineOffset - cornerRadius
  151. let trY: CGFloat = 0.0 + lineOffset + cornerRadius
  152. let tr = CGPoint(x: trX, y: trY)
  153. let brX: CGFloat = size - lineOffset - cornerRadius
  154. let brY: CGFloat = size - lineOffset - cornerRadius
  155. let br = CGPoint(x: brX, y: brY)
  156. let blX: CGFloat = 0.0 + lineOffset + cornerRadius
  157. let blY: CGFloat = size - lineOffset - cornerRadius
  158. let bl = CGPoint(x: blX, y: blY)
  159. let tlX: CGFloat = 0.0 + lineOffset + cornerRadius
  160. let tlY: CGFloat = 0.0 + lineOffset + cornerRadius
  161. let tl = CGPoint(x: tlX, y: tlY)
  162. // Start in the top right corner.
  163. let offset: CGFloat = ((cornerRadius * sqrt(2)) / 2.0)
  164. let trXOffset: CGFloat = tr.x + offset
  165. let trYOffset: CGFloat = tr.y - offset
  166. path.move(to: CGPoint(x: trXOffset, y: trYOffset))
  167. // Bottom of top right arc.12124
  168. if cornerRadius != 0 {
  169. path.addArc(withCenter: tr,
  170. radius: cornerRadius,
  171. startAngle: -(CGFloat.pi / 4),
  172. endAngle: 0.0,
  173. clockwise: true)
  174. }
  175. // Right side.
  176. let brXCr: CGFloat = br.x + cornerRadius
  177. path.addLine(to: CGPoint(x: brXCr, y: br.y))
  178. // Bottom right arc.
  179. if cornerRadius != 0 {
  180. path.addArc(withCenter: br,
  181. radius: cornerRadius,
  182. startAngle: 0.0,
  183. endAngle: CGFloat.pi / 2,
  184. clockwise: true)
  185. }
  186. // Bottom side.
  187. let blYCr: CGFloat = bl.y + cornerRadius
  188. path.addLine(to: CGPoint(x: bl.x , y: blYCr))
  189. // Bottom left arc.
  190. if cornerRadius != 0 {
  191. path.addArc(withCenter: bl,
  192. radius: cornerRadius,
  193. startAngle: CGFloat.pi / 2,
  194. endAngle: CGFloat.pi,
  195. clockwise: true)
  196. }
  197. // Left side.
  198. let tlXCr: CGFloat = tl.x - cornerRadius
  199. path.addLine(to: CGPoint(x: tlXCr, y: tl.y))
  200. // Top left arc.
  201. if cornerRadius != 0 {
  202. path.addArc(withCenter: tl,
  203. radius: cornerRadius,
  204. startAngle: CGFloat.pi,
  205. endAngle: CGFloat(CGFloat.pi + (CGFloat.pi / 2)),
  206. clockwise: true)
  207. }
  208. // Top side.
  209. let trYCr: CGFloat = tr.y - cornerRadius
  210. path.addLine(to: CGPoint(x: tr.x, y: trYCr))
  211. // Top of top right arc
  212. if cornerRadius != 0 {
  213. path.addArc(withCenter: tr,
  214. radius: cornerRadius,
  215. startAngle: CGFloat(CGFloat.pi + (CGFloat.pi / 2)),
  216. endAngle: CGFloat(CGFloat.pi + (CGFloat.pi / 2) + (CGFloat.pi / 4)),
  217. clockwise: true)
  218. }
  219. path.close()
  220. return path
  221. }
  222. //----------------------------
  223. // MARK: - Mark Generation
  224. //----------------------------
  225. override func pathForMark() -> UIBezierPath? {
  226. let path = UIBezierPath()
  227. path.move(to: checkmarkShortArmEndPoint)
  228. path.addLine(to: checkmarkMiddlePoint)
  229. path.addLine(to: checkmarkLongArmEndPoint)
  230. return path
  231. }
  232. override func pathForLongMark() -> UIBezierPath? {
  233. let path = UIBezierPath()
  234. path.move(to: checkmarkShortArmEndPoint)
  235. path.addLine(to: checkmarkMiddlePoint)
  236. path.addLine(to: checkmarkLongArmBoxIntersectionPoint)
  237. return path
  238. }
  239. override func pathForMixedMark() -> UIBezierPath? {
  240. let path = UIBezierPath()
  241. path.move(to: CGPoint(x: size * 0.25, y: size * 0.5))
  242. path.addLine(to: CGPoint(x: size * 0.5, y: size * 0.5))
  243. path.addLine(to: CGPoint(x: size * 0.75, y: size * 0.5))
  244. return path
  245. }
  246. override func pathForLongMixedMark() -> UIBezierPath? {
  247. let path = UIBezierPath()
  248. path.move(to: CGPoint(x: size * 0.25, y: size * 0.5))
  249. path.addLine(to: CGPoint(x: size * 0.5, y: size * 0.5))
  250. path.addLine(to: CGPoint(x: size - boxLineWidth, y: size * 0.5))
  251. return path
  252. }
  253. override func pathForUnselectedMark() -> UIBezierPath? {
  254. return nil
  255. }
  256. override func pathForLongUnselectedMark() -> UIBezierPath? {
  257. return nil
  258. }
  259. }