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