1// Copyright 2017 by the rasterx Authors. All rights reserved.
2//
3// created: 2017 by S.R.Wiley
4
5package rasterx
6
7import (
8 "math"
9
10 "golang.org/x/image/math/fixed"
11)
12
13const (
14 cubicsPerHalfCircle = 8 // Number of cubic beziers to approx half a circle
15 epsilonFixed = fixed.Int26_6(16) // 1/4 in fixed point
16 // fixed point t paramaterization shift factor;
17 // (2^this)/64 is the max length of t for fixed.Int26_6
18 tStrokeShift = 14
19)
20
21type (
22 // JoinMode type to specify how segments join.
23 JoinMode uint8
24 // CapFunc defines a function that draws caps on the ends of lines
25 CapFunc func(p Adder, a, eNorm fixed.Point26_6)
26 // GapFunc defines a function to bridge gaps when the miter limit is
27 // exceeded
28 GapFunc func(p Adder, a, tNorm, lNorm fixed.Point26_6)
29
30 // C2Point represents a point that connects two stroke segments
31 // and holds the tangent, normal and radius of curvature
32 // of the trailing and leading segments in fixed point values.
33 C2Point struct {
34 P, TTan, LTan, TNorm, LNorm fixed.Point26_6
35 RT, RL fixed.Int26_6
36 }
37
38 // Stroker does everything a Filler does, but
39 // also allows for stroking and dashed stroking in addition to
40 // filling
41 Stroker struct {
42 Filler
43 CapT, CapL CapFunc // Trailing and leading cap funcs may be set separately
44 JoinGap GapFunc // When gap appears between segments, this function is called
45
46 firstP, trailPoint, leadPoint C2Point // Tracks progress of the stroke
47 ln fixed.Point26_6 // last normal of intra-seg connection.
48 u, mLimit fixed.Int26_6 // u is the half-width of the stroke.
49
50 JoinMode JoinMode
51 inStroke bool
52 }
53)
54
55// JoinMode constants determine how stroke segments bridge the gap at a join
56// ArcClip mode is like MiterClip applied to arcs, and is not part of the SVG2.0
57// standard.
58const (
59 Arc JoinMode = iota
60 ArcClip
61 Miter
62 MiterClip
63 Bevel
64 Round
65)
66
67// NewStroker returns a ptr to a Stroker with default values.
68// A Stroker has all of the capabilities of a Filler and Scanner, plus the ability
69// to stroke curves with solid lines. Use SetStroke to configure with non-default
70// values.
71func NewStroker(width, height int, scanner Scanner) *Stroker {
72 r := new(Stroker)
73 r.Scanner = scanner
74 r.SetBounds(width, height)
75 //Defaults for stroking
76 r.SetWinding(true)
77 r.u = 2 << 6
78 r.mLimit = 4 << 6
79 r.JoinMode = MiterClip
80 r.JoinGap = RoundGap
81 r.CapL = RoundCap
82 r.CapT = RoundCap
83 r.SetStroke(1<<6, 4<<6, ButtCap, nil, FlatGap, MiterClip)
84 return r
85}
86
87// SetStroke set the parameters for stroking a line. width is the width of the line, miterlimit is the miter cutoff
88// value for miter, arc, miterclip and arcClip joinModes. CapL and CapT are the capping functions for leading and trailing
89// line ends. If one is nil, the other function is used at both ends. If both are nil, both ends are ButtCapped.
90// gp is the gap function that determines how a gap on the convex side of two joining lines is filled. jm is the JoinMode
91// for curve segments.
92func (r *Stroker) SetStroke(width, miterLimit fixed.Int26_6, capL, capT CapFunc, gp GapFunc, jm JoinMode) {
93 r.u = width / 2
94 r.CapL = capL
95 r.CapT = capT
96 r.JoinMode = jm
97 r.JoinGap = gp
98 r.mLimit = (r.u * miterLimit) >> 6
99
100 if r.CapT == nil {
101 if r.CapL == nil {
102 r.CapT = ButtCap
103 } else {
104 r.CapT = r.CapL
105 }
106 }
107 if r.CapL == nil {
108 r.CapL = r.CapT
109 }
110 if gp == nil {
111 if r.JoinMode == Round {
112 r.JoinGap = RoundGap
113 } else {
114 r.JoinGap = FlatGap
115 }
116 }
117
118}
119
120// GapToCap is a utility that converts a CapFunc to GapFunc
121func GapToCap(p Adder, a, eNorm fixed.Point26_6, gf GapFunc) {
122 p.Start(a.Add(eNorm))
123 gf(p, a, eNorm, Invert(eNorm))
124 p.Line(a.Sub(eNorm))
125}
126
127var (
128 // ButtCap caps lines with a straight line
129 ButtCap CapFunc = func(p Adder, a, eNorm fixed.Point26_6) {
130 p.Start(a.Add(eNorm))
131 p.Line(a.Sub(eNorm))
132 }
133 // SquareCap caps lines with a square which is slightly longer than ButtCap
134 SquareCap CapFunc = func(p Adder, a, eNorm fixed.Point26_6) {
135 tpt := a.Add(turnStarboard90(eNorm))
136 p.Start(a.Add(eNorm))
137 p.Line(tpt.Add(eNorm))
138 p.Line(tpt.Sub(eNorm))
139 p.Line(a.Sub(eNorm))
140 }
141 // RoundCap caps lines with a half-circle
142 RoundCap CapFunc = func(p Adder, a, eNorm fixed.Point26_6) {
143 GapToCap(p, a, eNorm, RoundGap)
144 }
145 // CubicCap caps lines with a cubic bezier
146 CubicCap CapFunc = func(p Adder, a, eNorm fixed.Point26_6) {
147 GapToCap(p, a, eNorm, CubicGap)
148 }
149 // QuadraticCap caps lines with a quadratic bezier
150 QuadraticCap CapFunc = func(p Adder, a, eNorm fixed.Point26_6) {
151 GapToCap(p, a, eNorm, QuadraticGap)
152 }
153 // Gap functions
154
155 //FlatGap bridges miter-limit gaps with a straight line
156 FlatGap GapFunc = func(p Adder, a, tNorm, lNorm fixed.Point26_6) {
157 p.Line(a.Add(lNorm))
158 }
159 // RoundGap bridges miter-limit gaps with a circular arc
160 RoundGap GapFunc = func(p Adder, a, tNorm, lNorm fixed.Point26_6) {
161 strokeArc(p, a, a.Add(tNorm), a.Add(lNorm), true, 0, 0, p.Line)
162 p.Line(a.Add(lNorm)) // just to be sure line joins cleanly,
163 // last pt in stoke arc may not be precisely s2
164 }
165 // CubicGap bridges miter-limit gaps with a cubic bezier
166 CubicGap GapFunc = func(p Adder, a, tNorm, lNorm fixed.Point26_6) {
167 p.CubeBezier(a.Add(tNorm).Add(turnStarboard90(tNorm)), a.Add(lNorm).Add(turnPort90(lNorm)), a.Add(lNorm))
168 }
169 // QuadraticGap bridges miter-limit gaps with a quadratic bezier
170 QuadraticGap GapFunc = func(p Adder, a, tNorm, lNorm fixed.Point26_6) {
171 c1, c2 := a.Add(tNorm).Add(turnStarboard90(tNorm)), a.Add(lNorm).Add(turnPort90(lNorm))
172 cm := c1.Add(c2).Mul(fixed.Int26_6(1 << 5))
173 p.QuadBezier(cm, a.Add(lNorm))
174 }
175)
176
177// StrokeArc strokes a circular arc by approximation with bezier curves
178func strokeArc(p Adder, a, s1, s2 fixed.Point26_6, clockwise bool, trimStart,
179 trimEnd fixed.Int26_6, firstPoint func(p fixed.Point26_6)) (ps1, ds1, ps2, ds2 fixed.Point26_6) {
180 // Approximate the circular arc using a set of cubic bezier curves by the method of
181 // L. Maisonobe, "Drawing an elliptical arc using polylines, quadratic
182 // or cubic Bezier curves", 2003
183 // https://www.spaceroots.org/documents/elllipse/elliptical-arc.pdf
184 // The method was simplified for circles.
185 theta1 := math.Atan2(float64(s1.Y-a.Y), float64(s1.X-a.X))
186 theta2 := math.Atan2(float64(s2.Y-a.Y), float64(s2.X-a.X))
187 if !clockwise {
188 for theta1 < theta2 {
189 theta1 += math.Pi * 2
190 }
191 } else {
192 for theta2 < theta1 {
193 theta2 += math.Pi * 2
194 }
195 }
196 deltaTheta := theta2 - theta1
197 if trimStart > 0 {
198 ds := (deltaTheta * float64(trimStart)) / float64(1<<tStrokeShift)
199 deltaTheta -= ds
200 theta1 += ds
201 }
202 if trimEnd > 0 {
203 ds := (deltaTheta * float64(trimEnd)) / float64(1<<tStrokeShift)
204 deltaTheta -= ds
205 }
206
207 segs := int(math.Abs(deltaTheta)/(math.Pi/cubicsPerHalfCircle)) + 1
208 dTheta := deltaTheta / float64(segs)
209 tde := math.Tan(dTheta / 2)
210 alpha := fixed.Int26_6(math.Sin(dTheta) * (math.Sqrt(4+3*tde*tde) - 1) * (64.0 / 3.0)) // Math is fun!
211 r := float64(Length(s1.Sub(a))) // Note r is *64
212 ldp := fixed.Point26_6{X: -fixed.Int26_6(r * math.Sin(theta1)), Y: fixed.Int26_6(r * math.Cos(theta1))}
213 ds1 = ldp
214 ps1 = fixed.Point26_6{X: a.X + ldp.Y, Y: a.Y - ldp.X}
215 firstPoint(ps1)
216 s1 = ps1
217 for i := 1; i <= segs; i++ {
218 eta := theta1 + dTheta*float64(i)
219 ds2 = fixed.Point26_6{X: -fixed.Int26_6(r * math.Sin(eta)), Y: fixed.Int26_6(r * math.Cos(eta))}
220 ps2 = fixed.Point26_6{X: a.X + ds2.Y, Y: a.Y - ds2.X} // Using deriviative to calc new pt, because circle
221 p1 := s1.Add(ldp.Mul(alpha))
222 p2 := ps2.Sub(ds2.Mul(alpha))
223 p.CubeBezier(p1, p2, ps2)
224 s1, ldp = ps2, ds2
225 }
226 return
227}
228
229// Joiner is called when two segments of a stroke are joined. it is exposed
230// so that if can be wrapped to generate callbacks for the join points.
231func (r *Stroker) Joiner(p C2Point) {
232 crossProd := p.LNorm.X*p.TNorm.Y - p.TNorm.X*p.LNorm.Y
233 // stroke bottom edge, with the reverse of p
234 r.strokeEdge(C2Point{P: p.P, TNorm: Invert(p.LNorm), LNorm: Invert(p.TNorm),
235 TTan: Invert(p.LTan), LTan: Invert(p.TTan), RT: -p.RL, RL: -p.RT}, -crossProd)
236 // stroke top edge
237 r.strokeEdge(p, crossProd)
238}
239
240// strokeEdge reduces code redundancy in the Joiner function by 2x since it handles
241// the top and bottom edges. This function encodes most of the logic of how to
242// handle joins between the given C2Point point p, and the end of the line.
243func (r *Stroker) strokeEdge(p C2Point, crossProd fixed.Int26_6) {
244 ra := &r.Filler
245 s1, s2 := p.P.Add(p.TNorm), p.P.Add(p.LNorm) // Bevel points for top leading and trailing
246 ra.Start(s1)
247 if crossProd > -epsilonFixed*epsilonFixed { // Almost co-linear or convex
248 ra.Line(s2)
249 return // No need to fill any gaps
250 }
251
252 var ct, cl fixed.Point26_6 // Center of curvature trailing, leading
253 var rt, rl fixed.Int26_6 // Radius of curvature trailing, leading
254
255 // Adjust radiuses for stroke width
256 if r.JoinMode == Arc || r.JoinMode == ArcClip {
257 // Find centers of radius of curvature and adjust the radius to be drawn
258 // by half the stroke width.
259 if p.RT != 0 {
260 if p.RT > 0 {
261 ct = p.P.Add(ToLength(turnPort90(p.TTan), p.RT))
262 rt = p.RT - r.u
263 } else {
264 ct = p.P.Sub(ToLength(turnPort90(p.TTan), -p.RT))
265 rt = -p.RT + r.u
266 }
267 if rt < 0 {
268 rt = 0
269 }
270 }
271 if p.RL != 0 {
272 if p.RL > 0 {
273 cl = p.P.Add(ToLength(turnPort90(p.LTan), p.RL))
274 rl = p.RL - r.u
275 } else {
276 cl = p.P.Sub(ToLength(turnPort90(p.LTan), -p.RL))
277 rl = -p.RL + r.u
278 }
279 if rl < 0 {
280 rl = 0
281 }
282 }
283 }
284
285 if r.JoinMode == MiterClip || r.JoinMode == Miter ||
286 // Arc or ArcClip with 0 tRadCurve and 0 lRadCurve is treated the same as a
287 // Miter or MiterClip join, resp.
288 ((r.JoinMode == Arc || r.JoinMode == ArcClip) && (rt == 0 && rl == 0)) {
289 xt := CalcIntersect(s1.Sub(p.TTan), s1, s2, s2.Sub(p.LTan))
290 xa := xt.Sub(p.P)
291 if Length(xa) < r.mLimit { // within miter limit
292 ra.Line(xt)
293 ra.Line(s2)
294 return
295 }
296 if r.JoinMode == MiterClip || (r.JoinMode == ArcClip) {
297 //Projection of tNorm onto xa
298 tProjP := xa.Mul(fixed.Int26_6((DotProd(xa, p.TNorm) << 6) / DotProd(xa, xa)))
299 projLen := Length(tProjP)
300 if r.mLimit > projLen { // the miter limit line is past the bevel point
301 // t is the fraction shifted by tStrokeShift to scale the vectors from the bevel point
302 // to the line intersection, so that they abbut the miter limit line.
303 tiLength := Length(xa)
304 sx1, sx2 := xt.Sub(s1), xt.Sub(s2)
305 t := (r.mLimit - projLen) << tStrokeShift / (tiLength - projLen)
306 tx := ToLength(sx1, t*Length(sx1)>>tStrokeShift)
307 lx := ToLength(sx2, t*Length(sx2)>>tStrokeShift)
308 vx := ToLength(xa, t*Length(xa)>>tStrokeShift)
309 s1p, _, ap := s1.Add(tx), s2.Add(lx), p.P.Add(vx)
310 gLen := Length(ap.Sub(s1p))
311 ra.Line(s1p)
312 r.JoinGap(ra, ap, ToLength(turnPort90(p.TTan), gLen), ToLength(turnPort90(p.LTan), gLen))
313 ra.Line(s2)
314 return
315 }
316 } // Fallthrough
317 } else if r.JoinMode == Arc || r.JoinMode == ArcClip {
318 // Test for cases of a bezier meeting line, an line meeting a bezier,
319 // or a bezier meeting a bezier. (Line meeting line is handled above.)
320 switch {
321 case rt == 0: // rl != 0, because one must be non-zero as checked above
322 xt, intersect := RayCircleIntersection(s1.Add(p.TTan), s1, cl, rl)
323 if intersect {
324 ray1, ray2 := xt.Sub(cl), s2.Sub(cl)
325 clockwise := (ray1.X*ray2.Y > ray1.Y*ray2.X) // Sign of xprod
326 if Length(p.P.Sub(xt)) < r.mLimit { // within miter limit
327 strokeArc(ra, cl, xt, s2, clockwise, 0, 0, ra.Line)
328 ra.Line(s2)
329 return
330 }
331 // Not within miter limit line
332 if r.JoinMode == ArcClip { // Scale bevel points towards xt, and call gap func
333 xa := xt.Sub(p.P)
334 //Projection of tNorm onto xa
335 tProjP := xa.Mul(fixed.Int26_6((DotProd(xa, p.TNorm) << 6) / DotProd(xa, xa)))
336 projLen := Length(tProjP)
337 if r.mLimit > projLen { // the miter limit line is past the bevel point
338 // t is the fraction shifted by tStrokeShift to scale the line or arc from the bevel point
339 // to the line intersection, so that they abbut the miter limit line.
340 sx1 := xt.Sub(s1) //, xt.Sub(s2)
341 t := fixed.Int26_6(1<<tStrokeShift) - ((r.mLimit - projLen) << tStrokeShift / (Length(xa) - projLen))
342 tx := ToLength(sx1, t*Length(sx1)>>tStrokeShift)
343 s1p := xt.Sub(tx)
344 ra.Line(s1p)
345 sp1, ds1, ps2, _ := strokeArc(ra, cl, xt, s2, clockwise, t, 0, ra.Start)
346 ra.Start(s1p)
347 // calc gap center as pt where -tnorm and line perp to midcoord
348 midP := sp1.Add(s1p).Mul(fixed.Int26_6(1 << 5)) // midpoint
349 midLine := turnPort90(midP.Sub(sp1))
350 if midLine.X*midLine.X+midLine.Y*midLine.Y > epsilonFixed { // if midline is zero, CalcIntersect is invalid
351 ap := CalcIntersect(s1p, s1p.Sub(p.TNorm), midLine.Add(midP), midP)
352 gLen := Length(ap.Sub(s1p))
353 if clockwise {
354 ds1 = Invert(ds1)
355 }
356 r.JoinGap(ra, ap, ToLength(turnPort90(p.TTan), gLen), ToLength(turnStarboard90(ds1), gLen))
357 }
358 ra.Line(sp1)
359 ra.Start(ps2)
360 ra.Line(s2)
361 return
362 }
363 //Bevel points not past miter limit: fallthrough
364 }
365 }
366 case rl == 0: // rt != 0, because one must be non-zero as checked above
367 xt, intersect := RayCircleIntersection(s2.Sub(p.LTan), s2, ct, rt)
368 if intersect {
369 ray1, ray2 := s1.Sub(ct), xt.Sub(ct)
370 clockwise := ray1.X*ray2.Y > ray1.Y*ray2.X
371 if Length(p.P.Sub(xt)) < r.mLimit { // within miter limit
372 strokeArc(ra, ct, s1, xt, clockwise, 0, 0, ra.Line)
373 ra.Line(s2)
374 return
375 }
376 // Not within miter limit line
377 if r.JoinMode == ArcClip { // Scale bevel points towards xt, and call gap func
378 xa := xt.Sub(p.P)
379 //Projection of lNorm onto xa
380 lProjP := xa.Mul(fixed.Int26_6((DotProd(xa, p.LNorm) << 6) / DotProd(xa, xa)))
381 projLen := Length(lProjP)
382 if r.mLimit > projLen { // The miter limit line is past the bevel point,
383 // t is the fraction to scale the line or arc from the bevel point
384 // to the line intersection, so that they abbut the miter limit line.
385 sx2 := xt.Sub(s2)
386 t := fixed.Int26_6(1<<tStrokeShift) - ((r.mLimit - projLen) << tStrokeShift / (Length(xa) - projLen))
387 lx := ToLength(sx2, t*Length(sx2)>>tStrokeShift)
388 s2p := xt.Sub(lx)
389 _, _, ps2, ds2 := strokeArc(ra, ct, s1, xt, clockwise, 0, t, ra.Line)
390 // calc gap center as pt where -lnorm and line perp to midcoord
391 midP := s2p.Add(ps2).Mul(fixed.Int26_6(1 << 5)) // midpoint
392 midLine := turnStarboard90(midP.Sub(ps2))
393 if midLine.X*midLine.X+midLine.Y*midLine.Y > epsilonFixed { // if midline is zero, CalcIntersect is invalid
394 ap := CalcIntersect(midP, midLine.Add(midP), s2p, s2p.Sub(p.LNorm))
395 gLen := Length(ap.Sub(ps2))
396 if clockwise {
397 ds2 = Invert(ds2)
398 }
399 r.JoinGap(ra, ap, ToLength(turnStarboard90(ds2), gLen), ToLength(turnPort90(p.LTan), gLen))
400 }
401 ra.Line(s2)
402 return
403 }
404 //Bevel points not past miter limit: fallthrough
405 }
406 }
407 default: // Both rl != 0 and rt != 0 as checked above
408 xt1, xt2, gIntersect := CircleCircleIntersection(ct, cl, rt, rl)
409 xt, intersect := ClosestPortside(s1, s2, xt1, xt2, gIntersect)
410 if intersect {
411 ray1, ray2 := s1.Sub(ct), xt.Sub(ct)
412 clockwiseT := (ray1.X*ray2.Y > ray1.Y*ray2.X)
413 ray1, ray2 = xt.Sub(cl), s2.Sub(cl)
414 clockwiseL := ray1.X*ray2.Y > ray1.Y*ray2.X
415
416 if Length(p.P.Sub(xt)) < r.mLimit { // within miter limit
417 strokeArc(ra, ct, s1, xt, clockwiseT, 0, 0, ra.Line)
418 strokeArc(ra, cl, xt, s2, clockwiseL, 0, 0, ra.Line)
419 ra.Line(s2)
420 return
421 }
422
423 if r.JoinMode == ArcClip { // Scale bevel points towards xt, and call gap func
424 xa := xt.Sub(p.P)
425 //Projection of lNorm onto xa
426 lProjP := xa.Mul(fixed.Int26_6((DotProd(xa, p.LNorm) << 6) / DotProd(xa, xa)))
427 projLen := Length(lProjP)
428 if r.mLimit > projLen { // The miter limit line is past the bevel point,
429 // t is the fraction to scale the line or arc from the bevel point
430 // to the line intersection, so that they abbut the miter limit line.
431 t := fixed.Int26_6(1<<tStrokeShift) - ((r.mLimit - projLen) << tStrokeShift / (Length(xa) - projLen))
432 _, _, ps1, ds1 := strokeArc(ra, ct, s1, xt, clockwiseT, 0, t, r.Filler.Line)
433 ps2, ds2, fs2, _ := strokeArc(ra, cl, xt, s2, clockwiseL, t, 0, ra.Start)
434 midP := ps1.Add(ps2).Mul(fixed.Int26_6(1 << 5)) // midpoint
435 midLine := turnStarboard90(midP.Sub(ps1))
436 ra.Start(ps1)
437 if midLine.X*midLine.X+midLine.Y*midLine.Y > epsilonFixed { // if midline is zero, CalcIntersect is invalid
438 if clockwiseT {
439 ds1 = Invert(ds1)
440 }
441 if clockwiseL {
442 ds2 = Invert(ds2)
443 }
444 ap := CalcIntersect(midP, midLine.Add(midP), ps2, ps2.Sub(turnStarboard90(ds2)))
445 gLen := Length(ap.Sub(ps2))
446 r.JoinGap(ra, ap, ToLength(turnStarboard90(ds1), gLen), ToLength(turnStarboard90(ds2), gLen))
447 }
448 ra.Line(ps2)
449 ra.Start(fs2)
450 ra.Line(s2)
451 return
452 }
453 }
454 }
455 // fallthrough to final JoinGap
456 }
457 }
458 r.JoinGap(ra, p.P, p.TNorm, p.LNorm)
459 ra.Line(s2)
460 return
461}
462
463// Stop a stroked line. The line will close
464// is isClosed is true. Otherwise end caps will
465// be drawn at both ends.
466func (r *Stroker) Stop(isClosed bool) {
467 if r.inStroke == false {
468 return
469 }
470 rf := &r.Filler
471 if isClosed {
472 if r.firstP.P != rf.a {
473 r.Line(r.firstP.P)
474 }
475 a := rf.a
476 r.firstP.TNorm = r.leadPoint.TNorm
477 r.firstP.RT = r.leadPoint.RT
478 r.firstP.TTan = r.leadPoint.TTan
479
480 rf.Start(r.firstP.P.Sub(r.firstP.TNorm))
481 rf.Line(a.Sub(r.ln))
482 rf.Start(a.Add(r.ln))
483 rf.Line(r.firstP.P.Add(r.firstP.TNorm))
484 r.Joiner(r.firstP)
485 r.firstP.blackWidowMark(rf)
486 } else {
487 a := rf.a
488 rf.Start(r.leadPoint.P.Sub(r.leadPoint.TNorm))
489 rf.Line(a.Sub(r.ln))
490 rf.Start(a.Add(r.ln))
491 rf.Line(r.leadPoint.P.Add(r.leadPoint.TNorm))
492 r.CapL(rf, r.leadPoint.P, r.leadPoint.TNorm)
493 r.CapT(rf, r.firstP.P, Invert(r.firstP.LNorm))
494 }
495 r.inStroke = false
496}
497
498// QuadBezier starts a stroked quadratic bezier.
499func (r *Stroker) QuadBezier(b, c fixed.Point26_6) {
500 r.quadBezierf(r, b, c)
501}
502
503// CubeBezier starts a stroked quadratic bezier.
504func (r *Stroker) CubeBezier(b, c, d fixed.Point26_6) {
505 r.cubeBezierf(r, b, c, d)
506}
507
508// quadBezierf calcs end curvature of beziers
509func (r *Stroker) quadBezierf(s Rasterx, b, c fixed.Point26_6) {
510 r.trailPoint = r.leadPoint
511 r.CalcEndCurvature(r.a, b, c, c, b, r.a, fixed.Int52_12(2<<12), doCalcCurvature(s))
512 r.QuadBezierF(s, b, c)
513 r.a = c
514}
515
516// doCalcCurvature determines if calculation of the end curvature is required
517// depending on the raster type and JoinMode
518func doCalcCurvature(r Rasterx) bool {
519 switch q := r.(type) {
520 case *Filler:
521 return false // never for filler
522 case *Stroker:
523 return (q.JoinMode == Arc || q.JoinMode == ArcClip)
524 case *Dasher:
525 return (q.JoinMode == Arc || q.JoinMode == ArcClip)
526 default:
527 return true // Better safe than sorry if another raster type is used
528 }
529}
530
531func (r *Stroker) cubeBezierf(sgm Rasterx, b, c, d fixed.Point26_6) {
532 if (r.a == b && c == d) || (r.a == b && b == c) || (c == b && d == c) {
533 sgm.Line(d)
534 return
535 }
536 r.trailPoint = r.leadPoint
537 // Only calculate curvature if stroking or and using arc or arc-clip
538 doCalcCurve := doCalcCurvature(sgm)
539 const dm = fixed.Int52_12((3 << 12) / 2)
540 switch {
541 // b != c, and c != d see above
542 case r.a == b:
543 r.CalcEndCurvature(b, c, d, d, c, b, dm, doCalcCurve)
544 // b != a, and b != c, see above
545 case c == d:
546 r.CalcEndCurvature(r.a, b, c, c, b, r.a, dm, doCalcCurve)
547 default:
548 r.CalcEndCurvature(r.a, b, c, d, c, b, dm, doCalcCurve)
549 }
550 r.CubeBezierF(sgm, b, c, d)
551 r.a = d
552}
553
554// Line adds a line segment to the rasterizer
555func (r *Stroker) Line(b fixed.Point26_6) {
556 r.LineSeg(r, b)
557}
558
559//LineSeg is called by both the Stroker and Dasher
560func (r *Stroker) LineSeg(sgm Rasterx, b fixed.Point26_6) {
561 r.trailPoint = r.leadPoint
562 ba := b.Sub(r.a)
563 if ba.X == 0 && ba.Y == 0 { // a == b, line is degenerate
564 if r.trailPoint.TTan.X != 0 || r.trailPoint.TTan.Y != 0 {
565 ba = r.trailPoint.TTan // Use last tangent for seg tangent
566 } else { // Must be on top of last moveto; set ba to X axis unit vector
567 ba = fixed.Point26_6{X: 1 << 6, Y: 0}
568 }
569 }
570 bnorm := turnPort90(ToLength(ba, r.u))
571 r.trailPoint.LTan = ba
572 r.leadPoint.TTan = ba
573 r.trailPoint.LNorm = bnorm
574 r.leadPoint.TNorm = bnorm
575 r.trailPoint.RL = 0.0
576 r.leadPoint.RT = 0.0
577 r.trailPoint.P = r.a
578 r.leadPoint.P = b
579
580 sgm.joinF()
581 sgm.lineF(b)
582 r.a = b
583}
584
585// lineF is for intra-curve lines. It is required for the Rasterizer interface
586// so that if the line is being stroked or dash stroked, different actions can be
587// taken.
588func (r *Stroker) lineF(b fixed.Point26_6) {
589 // b is either an intra-segment value, or
590 // the end of the segment.
591 var bnorm fixed.Point26_6
592 a := r.a // Hold a since r.a is going to change during stroke operation
593 if b == r.leadPoint.P { // End of segment
594 bnorm = r.leadPoint.TNorm // Use more accurate leadPoint tangent
595 } else {
596 bnorm = turnPort90(ToLength(b.Sub(a), r.u)) // Intra segment normal
597 }
598 ra := &r.Filler
599 ra.Start(b.Sub(bnorm))
600 ra.Line(a.Sub(r.ln))
601 ra.Start(a.Add(r.ln))
602 ra.Line(b.Add(bnorm))
603 r.a = b
604 r.ln = bnorm
605}
606
607// Start iniitates a stroked path
608func (r *Stroker) Start(a fixed.Point26_6) {
609 r.inStroke = false
610 r.Filler.Start(a)
611}
612
613// CalcEndCurvature calculates the radius of curvature given the control points
614// of a bezier curve.
615// It is a low level function exposed for the purposes of callbacks
616// and debugging.
617func (r *Stroker) CalcEndCurvature(p0, p1, p2, q0, q1, q2 fixed.Point26_6,
618 dm fixed.Int52_12, calcRadCuve bool) {
619 r.trailPoint.P = p0
620 r.leadPoint.P = q0
621 r.trailPoint.LTan = p1.Sub(p0)
622 r.leadPoint.TTan = q0.Sub(q1)
623 r.trailPoint.LNorm = turnPort90(ToLength(r.trailPoint.LTan, r.u))
624 r.leadPoint.TNorm = turnPort90(ToLength(r.leadPoint.TTan, r.u))
625 if calcRadCuve {
626 r.trailPoint.RL = RadCurvature(p0, p1, p2, dm)
627 r.leadPoint.RT = -RadCurvature(q0, q1, q2, dm)
628 } else {
629 r.trailPoint.RL = 0
630 r.leadPoint.RT = 0
631 }
632}
633
634func (r *Stroker) joinF() {
635 if r.inStroke == false {
636 r.inStroke = true
637 r.firstP = r.trailPoint
638 } else {
639 ra := &r.Filler
640 tl := r.trailPoint.P.Sub(r.trailPoint.TNorm)
641 th := r.trailPoint.P.Add(r.trailPoint.TNorm)
642 if r.a != r.trailPoint.P || r.ln != r.trailPoint.TNorm {
643 a := r.a
644 ra.Start(tl)
645 ra.Line(a.Sub(r.ln))
646 ra.Start(a.Add(r.ln))
647 ra.Line(th)
648 }
649 r.Joiner(r.trailPoint)
650 r.trailPoint.blackWidowMark(ra)
651 }
652 r.ln = r.trailPoint.LNorm
653 r.a = r.trailPoint.P
654}
655
656// blackWidowMark handles a gap in a stroke that can occur when a line end is too close
657// to a segment to segment join point. Although it is only required in those cases,
658// at this point, no code has been written to properly detect when it is needed,
659// so for now it just draws by default.
660func (jp *C2Point) blackWidowMark(ra Adder) {
661 xprod := jp.TNorm.X*jp.LNorm.Y - jp.TNorm.Y*jp.LNorm.X
662 if xprod > epsilonFixed*epsilonFixed {
663 tl := jp.P.Sub(jp.TNorm)
664 ll := jp.P.Sub(jp.LNorm)
665 ra.Start(jp.P)
666 ra.Line(tl)
667 ra.Line(ll)
668 ra.Line(jp.P)
669 } else if xprod < -epsilonFixed*epsilonFixed {
670 th := jp.P.Add(jp.TNorm)
671 lh := jp.P.Add(jp.LNorm)
672 ra.Start(jp.P)
673 ra.Line(lh)
674 ra.Line(th)
675 ra.Line(jp.P)
676 }
677}