dash.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. // Copyright 2017 by the rasterx Authors. All rights reserved.
  2. //_
  3. // created: 2017 by S.R.Wiley
  4. package rasterx
  5. import (
  6. "golang.org/x/image/math/fixed"
  7. )
  8. // Dasher struct extends the Stroker and can draw
  9. // dashed lines with end capping
  10. type Dasher struct {
  11. Stroker
  12. Dashes []fixed.Int26_6
  13. dashPlace int
  14. firstDashIsGap, dashIsGap bool
  15. deltaDash, DashOffset fixed.Int26_6
  16. sgm Rasterx
  17. // sgm allows us to switch between dashing
  18. // and non-dashing rasterizers in the SetStroke function.
  19. }
  20. // joinF overides stroker joinF during dashed stroking, because we need to slightly modify
  21. // the the call as below to handle the case of the join being in a dash gap.
  22. func (r *Dasher) joinF() {
  23. if len(r.Dashes) == 0 || !r.inStroke || !r.dashIsGap {
  24. r.Stroker.joinF()
  25. }
  26. }
  27. // Start starts a dashed line
  28. func (r *Dasher) Start(a fixed.Point26_6) {
  29. // Advance dashPlace to the dashOffset start point and set deltaDash
  30. if len(r.Dashes) > 0 {
  31. r.deltaDash = r.DashOffset
  32. r.dashIsGap = false
  33. r.dashPlace = 0
  34. for r.deltaDash > r.Dashes[r.dashPlace] {
  35. r.deltaDash -= r.Dashes[r.dashPlace]
  36. r.dashIsGap = !r.dashIsGap
  37. r.dashPlace++
  38. if r.dashPlace == len(r.Dashes) {
  39. r.dashPlace = 0
  40. }
  41. }
  42. r.firstDashIsGap = r.dashIsGap
  43. }
  44. r.Stroker.Start(a)
  45. }
  46. // lineF overides stroker lineF to modify the the call as below
  47. // while performing the join in a dashed stroke.
  48. func (r *Dasher) lineF(b fixed.Point26_6) {
  49. var bnorm fixed.Point26_6
  50. a := r.a // Copy local a since r.a is going to change during stroke operation
  51. ba := b.Sub(a)
  52. segLen := Length(ba)
  53. var nlt fixed.Int26_6
  54. if b == r.leadPoint.P { // End of segment
  55. bnorm = r.leadPoint.TNorm // Use more accurate leadPoint tangent
  56. } else {
  57. bnorm = turnPort90(ToLength(b.Sub(a), r.u)) // Intra segment normal
  58. }
  59. for segLen+r.deltaDash > r.Dashes[r.dashPlace] {
  60. nl := r.Dashes[r.dashPlace] - r.deltaDash
  61. nlt += nl
  62. r.dashLineStrokeBit(a.Add(ToLength(ba, nlt)), bnorm, false)
  63. r.dashIsGap = !r.dashIsGap
  64. segLen -= nl
  65. r.deltaDash = 0
  66. r.dashPlace++
  67. if r.dashPlace == len(r.Dashes) {
  68. r.dashPlace = 0
  69. }
  70. }
  71. r.deltaDash += segLen
  72. r.dashLineStrokeBit(b, bnorm, true)
  73. }
  74. // SetStroke set the parameters for stroking a line. width is the width of the line, miterlimit is the miter cutoff
  75. // value for miter, arc, miterclip and arcClip joinModes. CapL and CapT are the capping functions for leading and trailing
  76. // line ends. If one is nil, the other function is used at both ends. gp is the gap function that determines how a
  77. // gap on the convex side of two lines joining is filled. jm is the JoinMode for curve segments. Dashes is the values for
  78. // the dash pattern. Pass in nil or an empty slice for no dashes. dashoffset is the starting offset into the dash array.
  79. func (r *Dasher) SetStroke(width, miterLimit fixed.Int26_6, capL, capT CapFunc, gp GapFunc, jm JoinMode, dashes []float64, dashOffset float64) {
  80. r.Stroker.SetStroke(width, miterLimit, capL, capT, gp, jm)
  81. r.Dashes = r.Dashes[:0] // clear the dash array
  82. if len(dashes) == 0 {
  83. r.sgm = &r.Stroker // This is just plain stroking
  84. return
  85. }
  86. // Dashed Stroke
  87. // Convert the float dash array and offset to fixed point and attach to the Filler
  88. oneIsPos := false // Check to see if at least one dash is > 0
  89. for _, v := range dashes {
  90. fv := fixed.Int26_6(v * 64)
  91. if fv <= 0 { // Negatives are considered 0s.
  92. fv = 0
  93. } else {
  94. oneIsPos = true
  95. }
  96. r.Dashes = append(r.Dashes, fv)
  97. }
  98. if oneIsPos == false {
  99. r.Dashes = r.Dashes[:0]
  100. r.sgm = &r.Stroker // This is just plain stroking
  101. return
  102. }
  103. r.DashOffset = fixed.Int26_6(dashOffset * 64)
  104. r.sgm = r // Use the full dasher
  105. }
  106. //Stop terminates a dashed line
  107. func (r *Dasher) Stop(isClosed bool) {
  108. if len(r.Dashes) == 0 {
  109. r.Stroker.Stop(isClosed)
  110. return
  111. }
  112. if r.inStroke == false {
  113. return
  114. }
  115. if isClosed && r.a != r.firstP.P {
  116. r.LineSeg(r.sgm, r.firstP.P)
  117. }
  118. ra := &r.Filler
  119. if isClosed && !r.firstDashIsGap && !r.dashIsGap { // closed connect w/o caps
  120. a := r.a
  121. r.firstP.TNorm = r.leadPoint.TNorm
  122. r.firstP.RT = r.leadPoint.RT
  123. r.firstP.TTan = r.leadPoint.TTan
  124. ra.Start(r.firstP.P.Sub(r.firstP.TNorm))
  125. ra.Line(a.Sub(r.ln))
  126. ra.Start(a.Add(r.ln))
  127. ra.Line(r.firstP.P.Add(r.firstP.TNorm))
  128. r.Joiner(r.firstP)
  129. r.firstP.blackWidowMark(ra)
  130. } else { // Cap open ends
  131. if !r.dashIsGap {
  132. r.CapL(ra, r.leadPoint.P, r.leadPoint.TNorm)
  133. }
  134. if !r.firstDashIsGap {
  135. r.CapT(ra, r.firstP.P, Invert(r.firstP.LNorm))
  136. }
  137. }
  138. r.inStroke = false
  139. }
  140. // dashLineStrokeBit is a helper function that reduces code redundancey in the
  141. // lineF function.
  142. func (r *Dasher) dashLineStrokeBit(b, bnorm fixed.Point26_6, dontClose bool) {
  143. if !r.dashIsGap { // Moving from dash to gap
  144. a := r.a
  145. ra := &r.Filler
  146. ra.Start(b.Sub(bnorm))
  147. ra.Line(a.Sub(r.ln))
  148. ra.Start(a.Add(r.ln))
  149. ra.Line(b.Add(bnorm))
  150. if dontClose == false {
  151. r.CapL(ra, b, bnorm)
  152. }
  153. } else { // Moving from gap to dash
  154. if dontClose == false {
  155. ra := &r.Filler
  156. r.CapT(ra, b, Invert(bnorm))
  157. }
  158. }
  159. r.a = b
  160. r.ln = bnorm
  161. }
  162. // Line for Dasher is here to pass the dasher sgm to LineP
  163. func (r *Dasher) Line(b fixed.Point26_6) {
  164. r.LineSeg(r.sgm, b)
  165. }
  166. // QuadBezier for dashing
  167. func (r *Dasher) QuadBezier(b, c fixed.Point26_6) {
  168. r.quadBezierf(r.sgm, b, c)
  169. }
  170. // CubeBezier starts a stroked cubic bezier.
  171. // It is a low level function exposed for the purposes of callbacks
  172. // and debugging.
  173. func (r *Dasher) CubeBezier(b, c, d fixed.Point26_6) {
  174. r.cubeBezierf(r.sgm, b, c, d)
  175. }
  176. // NewDasher returns a Dasher ptr with default values.
  177. // A Dasher has all of the capabilities of a Stroker, Filler, and Scanner, plus the ability
  178. // to stroke curves with solid lines. Use SetStroke to configure with non-default
  179. // values.
  180. func NewDasher(width, height int, scanner Scanner) *Dasher {
  181. r := new(Dasher)
  182. r.Scanner = scanner
  183. r.SetBounds(width, height)
  184. r.SetWinding(true)
  185. r.SetStroke(1*64, 4*64, ButtCap, nil, FlatGap, MiterClip, nil, 0)
  186. r.sgm = &r.Stroker
  187. return r
  188. }