shapes.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. // Copyright 2018 by the rasterx Authors. All rights reserved.
  2. //_
  3. // created: 2/06/2018 by S.R.Wiley
  4. // Functions that rasterize common shapes easily.
  5. package rasterx
  6. import (
  7. "math"
  8. "golang.org/x/image/math/fixed"
  9. )
  10. // MaxDx is the Maximum radians a cubic splice is allowed to span
  11. // in ellipse parametric when approximating an off-axis ellipse.
  12. const MaxDx float64 = math.Pi / 8
  13. // ToFixedP converts two floats to a fixed point.
  14. func ToFixedP(x, y float64) (p fixed.Point26_6) {
  15. p.X = fixed.Int26_6(x * 64)
  16. p.Y = fixed.Int26_6(y * 64)
  17. return
  18. }
  19. // AddCircle adds a circle to the Adder p
  20. func AddCircle(cx, cy, r float64, p Adder) {
  21. AddEllipse(cx, cy, r, r, 0, p)
  22. }
  23. // AddEllipse adds an elipse with center at cx,cy, with the indicated
  24. // x and y radius, (rx, ry), rotated around the center by rot degrees.
  25. func AddEllipse(cx, cy, rx, ry, rot float64, p Adder) {
  26. rotRads := rot * math.Pi / 180
  27. px, py := Identity.
  28. Translate(cx, cy).Rotate(rotRads).Translate(-cx, -cy).Transform(cx+rx, cy)
  29. points := []float64{rx, ry, rot, 1.0, 0.0, px, py}
  30. p.Start(ToFixedP(px, py))
  31. AddArc(points, cx, cy, px, py, p)
  32. p.Stop(true)
  33. }
  34. // AddRect adds a rectangle of the indicated size, rotated
  35. // around the center by rot degrees.
  36. func AddRect(minX, minY, maxX, maxY, rot float64, p Adder) {
  37. rot *= math.Pi / 180
  38. cx, cy := (minX+maxX)/2, (minY+maxY)/2
  39. m := Identity.Translate(cx, cy).Rotate(rot).Translate(-cx, -cy)
  40. q := &MatrixAdder{M: m, Adder: p}
  41. q.Start(ToFixedP(minX, minY))
  42. q.Line(ToFixedP(maxX, minY))
  43. q.Line(ToFixedP(maxX, maxY))
  44. q.Line(ToFixedP(minX, maxY))
  45. q.Stop(true)
  46. }
  47. // AddRoundRect adds a rectangle of the indicated size, rotated
  48. // around the center by rot degrees with rounded corners of radius
  49. // rx in the x axis and ry in the y axis. gf specifes the shape of the
  50. // filleting function. Valid values are RoundGap, QuadraticGap, CubicGap,
  51. // FlatGap, or nil which defaults to a flat gap.
  52. func AddRoundRect(minX, minY, maxX, maxY, rx, ry, rot float64, gf GapFunc, p Adder) {
  53. if rx <= 0 || ry <= 0 {
  54. AddRect(minX, minY, maxX, maxY, rot, p)
  55. return
  56. }
  57. rot *= math.Pi / 180
  58. if gf == nil {
  59. gf = FlatGap
  60. }
  61. w := maxX - minX
  62. if w < rx*2 {
  63. rx = w / 2
  64. }
  65. h := maxY - minY
  66. if h < ry*2 {
  67. ry = h / 2
  68. }
  69. stretch := rx / ry
  70. midY := minY + h/2
  71. m := Identity.Translate(minX+w/2, midY).Rotate(rot).Scale(1, 1/stretch).Translate(-minX-w/2, -minY-h/2)
  72. maxY = midY + h/2*stretch
  73. minY = midY - h/2*stretch
  74. q := &MatrixAdder{M: m, Adder: p}
  75. q.Start(ToFixedP(minX+rx, minY))
  76. q.Line(ToFixedP(maxX-rx, minY))
  77. gf(q, ToFixedP(maxX-rx, minY+rx), ToFixedP(0, -rx), ToFixedP(rx, 0))
  78. q.Line(ToFixedP(maxX, maxY-rx))
  79. gf(q, ToFixedP(maxX-rx, maxY-rx), ToFixedP(rx, 0), ToFixedP(0, rx))
  80. q.Line(ToFixedP(minX+rx, maxY))
  81. gf(q, ToFixedP(minX+rx, maxY-rx), ToFixedP(0, rx), ToFixedP(-rx, 0))
  82. q.Line(ToFixedP(minX, minY+rx))
  83. gf(q, ToFixedP(minX+rx, minY+rx), ToFixedP(-rx, 0), ToFixedP(0, -rx))
  84. q.Stop(true)
  85. }
  86. //AddArc adds an arc to the adder p
  87. func AddArc(points []float64, cx, cy, px, py float64, p Adder) (lx, ly float64) {
  88. rotX := points[2] * math.Pi / 180 // Convert degress to radians
  89. largeArc := points[3] != 0
  90. sweep := points[4] != 0
  91. startAngle := math.Atan2(py-cy, px-cx) - rotX
  92. endAngle := math.Atan2(points[6]-cy, points[5]-cx) - rotX
  93. deltaTheta := endAngle - startAngle
  94. arcBig := math.Abs(deltaTheta) > math.Pi
  95. // Approximate ellipse using cubic bezeir splines
  96. etaStart := math.Atan2(math.Sin(startAngle)/points[1], math.Cos(startAngle)/points[0])
  97. etaEnd := math.Atan2(math.Sin(endAngle)/points[1], math.Cos(endAngle)/points[0])
  98. deltaEta := etaEnd - etaStart
  99. if (arcBig && !largeArc) || (!arcBig && largeArc) { // Go has no boolean XOR
  100. if deltaEta < 0 {
  101. deltaEta += math.Pi * 2
  102. } else {
  103. deltaEta -= math.Pi * 2
  104. }
  105. }
  106. // This check might be needed if the center point of the elipse is
  107. // at the midpoint of the start and end lines.
  108. if deltaEta < 0 && sweep {
  109. deltaEta += math.Pi * 2
  110. } else if deltaEta >= 0 && !sweep {
  111. deltaEta -= math.Pi * 2
  112. }
  113. // Round up to determine number of cubic splines to approximate bezier curve
  114. segs := int(math.Abs(deltaEta)/MaxDx) + 1
  115. dEta := deltaEta / float64(segs) // span of each segment
  116. // Approximate the ellipse using a set of cubic bezier curves by the method of
  117. // L. Maisonobe, "Drawing an elliptical arc using polylines, quadratic
  118. // or cubic Bezier curves", 2003
  119. // https://www.spaceroots.org/documents/elllipse/elliptical-arc.pdf
  120. tde := math.Tan(dEta / 2)
  121. alpha := math.Sin(dEta) * (math.Sqrt(4+3*tde*tde) - 1) / 3 // Math is fun!
  122. lx, ly = px, py
  123. sinTheta, cosTheta := math.Sin(rotX), math.Cos(rotX)
  124. ldx, ldy := ellipsePrime(points[0], points[1], sinTheta, cosTheta, etaStart, cx, cy)
  125. for i := 1; i <= segs; i++ {
  126. eta := etaStart + dEta*float64(i)
  127. var px, py float64
  128. if i == segs {
  129. px, py = points[5], points[6] // Just makes the end point exact; no roundoff error
  130. } else {
  131. px, py = ellipsePointAt(points[0], points[1], sinTheta, cosTheta, eta, cx, cy)
  132. }
  133. dx, dy := ellipsePrime(points[0], points[1], sinTheta, cosTheta, eta, cx, cy)
  134. p.CubeBezier(ToFixedP(lx+alpha*ldx, ly+alpha*ldy),
  135. ToFixedP(px-alpha*dx, py-alpha*dy), ToFixedP(px, py))
  136. lx, ly, ldx, ldy = px, py, dx, dy
  137. }
  138. return lx, ly
  139. }
  140. // ellipsePrime gives tangent vectors for parameterized elipse; a, b, radii, eta parameter, center cx, cy
  141. func ellipsePrime(a, b, sinTheta, cosTheta, eta, cx, cy float64) (px, py float64) {
  142. bCosEta := b * math.Cos(eta)
  143. aSinEta := a * math.Sin(eta)
  144. px = -aSinEta*cosTheta - bCosEta*sinTheta
  145. py = -aSinEta*sinTheta + bCosEta*cosTheta
  146. return
  147. }
  148. // ellipsePointAt gives points for parameterized elipse; a, b, radii, eta parameter, center cx, cy
  149. func ellipsePointAt(a, b, sinTheta, cosTheta, eta, cx, cy float64) (px, py float64) {
  150. aCosEta := a * math.Cos(eta)
  151. bSinEta := b * math.Sin(eta)
  152. px = cx + aCosEta*cosTheta - bSinEta*sinTheta
  153. py = cy + aCosEta*sinTheta + bSinEta*cosTheta
  154. return
  155. }
  156. // FindEllipseCenter locates the center of the Ellipse if it exists. If it does not exist,
  157. // the radius values will be increased minimally for a solution to be possible
  158. // while preserving the ra to rb ratio. ra and rb arguments are pointers that can be
  159. // checked after the call to see if the values changed. This method uses coordinate transformations
  160. // to reduce the problem to finding the center of a circle that includes the origin
  161. // and an arbitrary point. The center of the circle is then transformed
  162. // back to the original coordinates and returned.
  163. func FindEllipseCenter(ra, rb *float64, rotX, startX, startY, endX, endY float64, sweep, smallArc bool) (cx, cy float64) {
  164. cos, sin := math.Cos(rotX), math.Sin(rotX)
  165. // Move origin to start point
  166. nx, ny := endX-startX, endY-startY
  167. // Rotate ellipse x-axis to coordinate x-axis
  168. nx, ny = nx*cos+ny*sin, -nx*sin+ny*cos
  169. // Scale X dimension so that ra = rb
  170. nx *= *rb / *ra // Now the ellipse is a circle radius rb; therefore foci and center coincide
  171. midX, midY := nx/2, ny/2
  172. midlenSq := midX*midX + midY*midY
  173. var hr float64
  174. if *rb**rb < midlenSq {
  175. // Requested ellipse does not exist; scale ra, rb to fit. Length of
  176. // span is greater than max width of ellipse, must scale *ra, *rb
  177. nrb := math.Sqrt(midlenSq)
  178. if *ra == *rb {
  179. *ra = nrb // prevents roundoff
  180. } else {
  181. *ra = *ra * nrb / *rb
  182. }
  183. *rb = nrb
  184. } else {
  185. hr = math.Sqrt(*rb**rb-midlenSq) / math.Sqrt(midlenSq)
  186. }
  187. // Notice that if hr is zero, both answers are the same.
  188. if (sweep && smallArc) || (!sweep && !smallArc) {
  189. cx = midX + midY*hr
  190. cy = midY - midX*hr
  191. } else {
  192. cx = midX - midY*hr
  193. cy = midY + midX*hr
  194. }
  195. // reverse scale
  196. cx *= *ra / *rb
  197. //Reverse rotate and translate back to original coordinates
  198. return cx*cos - cy*sin + startX, cx*sin + cy*cos + startY
  199. }