stroke.go

  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}