github.com/cybriq/giocore@v0.0.7-0.20210703034601-cfb9cb5f3900/internal/stroke/stroke.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  // Most of the algorithms to compute strokes and their offsets have been
     4  // extracted, adapted from (and used as a reference implementation):
     5  //  - github.com/tdewolff/canvas (Licensed under MIT)
     6  //
     7  // These algorithms have been implemented from:
     8  //  Fast, precise flattening of cubic Bézier path and offset curves
     9  //   Thomas F. Hain, et al.
    10  //
    11  // An electronic version is available at:
    12  //  https://seant23.files.wordpress.com/2010/11/fastpreciseflatteningofbeziercurve.pdf
    13  //
    14  // Possible improvements (in term of speed and/or accuracy) on these
    15  // algorithms are:
    16  //
    17  //  - Polar Stroking: New Theory and Methods for Stroking Paths,
    18  //    M. Kilgard
    19  //    https://arxiv.org/pdf/2007.00308.pdf
    20  //
    21  //  - https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html
    22  //    R. Levien
    23  
    24  // Package stroke implements conversion of strokes to filled outlines. It is used as a
    25  // fallback for stroke configurations not natively supported by the renderer.
    26  package stroke
    27  
    28  import (
    29  	"encoding/binary"
    30  	"math"
    31  
    32  	"github.com/cybriq/giocore/f32"
    33  	"github.com/cybriq/giocore/internal/ops"
    34  	"github.com/cybriq/giocore/internal/scene"
    35  )
    36  
    37  // The following are copies of types from op/clip to avoid a circular import of
    38  // that package.
    39  // TODO: when the old renderer is gone, this package can be merged with
    40  // op/clip, eliminating the duplicate types.
    41  type StrokeStyle struct {
    42  	Width float32
    43  	Miter float32
    44  	Cap   StrokeCap
    45  	Join  StrokeJoin
    46  }
    47  
    48  type StrokeCap uint8
    49  
    50  const (
    51  	RoundCap StrokeCap = iota
    52  	FlatCap
    53  	SquareCap
    54  )
    55  
    56  type StrokeJoin uint8
    57  
    58  const (
    59  	RoundJoin StrokeJoin = iota
    60  	BevelJoin
    61  )
    62  
    63  // strokeTolerance is used to reconcile rounding errors arising
    64  // when splitting quads into smaller and smaller segments to approximate
    65  // them into straight lines, and when joining back segments.
    66  //
    67  // The magic value of 0.01 was found by striking a compromise between
    68  // aesthetic looking (curves did look like curves, even after linearization)
    69  // and speed.
    70  const strokeTolerance = 0.01
    71  
    72  type QuadSegment struct {
    73  	From, Ctrl, To f32.Point
    74  }
    75  
    76  type StrokeQuad struct {
    77  	Contour uint32
    78  	Quad    QuadSegment
    79  }
    80  
    81  type strokeState struct {
    82  	p0, p1 f32.Point // p0 is the start point, p1 the end point.
    83  	n0, n1 f32.Point // n0 is the normal vector at the start point, n1 at the end point.
    84  	r0, r1 float32   // r0 is the curvature at the start point, r1 at the end point.
    85  	ctl    f32.Point // ctl is the control point of the quadratic Bézier segment.
    86  }
    87  
    88  type StrokeQuads []StrokeQuad
    89  
    90  func (qs *StrokeQuads) setContour(n uint32) {
    91  	for i := range *qs {
    92  		(*qs)[i].Contour = n
    93  	}
    94  }
    95  
    96  func (qs *StrokeQuads) pen() f32.Point {
    97  	return (*qs)[len(*qs)-1].Quad.To
    98  }
    99  
   100  func (qs *StrokeQuads) closed() bool {
   101  	beg := (*qs)[0].Quad.From
   102  	end := (*qs)[len(*qs)-1].Quad.To
   103  	return f32Eq(beg.X, end.X) && f32Eq(beg.Y, end.Y)
   104  }
   105  
   106  func (qs *StrokeQuads) lineTo(pt f32.Point) {
   107  	end := qs.pen()
   108  	*qs = append(*qs, StrokeQuad{
   109  		Quad: QuadSegment{
   110  			From: end,
   111  			Ctrl: end.Add(pt).Mul(0.5),
   112  			To:   pt,
   113  		},
   114  	})
   115  }
   116  
   117  func (qs *StrokeQuads) arc(f1, f2 f32.Point, angle float32) {
   118  	const segments = 16
   119  	pen := qs.pen()
   120  	m := ArcTransform(pen, f1.Add(pen), f2.Add(pen), angle, segments)
   121  	for i := 0; i < segments; i++ {
   122  		p0 := qs.pen()
   123  		p1 := m.Transform(p0)
   124  		p2 := m.Transform(p1)
   125  		ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5))
   126  		*qs = append(*qs, StrokeQuad{
   127  			Quad: QuadSegment{
   128  				From: p0, Ctrl: ctl, To: p2,
   129  			},
   130  		})
   131  	}
   132  }
   133  
   134  // split splits a slice of quads into slices of quads grouped
   135  // by contours (ie: splitted at move-to boundaries).
   136  func (qs StrokeQuads) split() []StrokeQuads {
   137  	if len(qs) == 0 {
   138  		return nil
   139  	}
   140  
   141  	var (
   142  		c uint32
   143  		o []StrokeQuads
   144  		i = len(o)
   145  	)
   146  	for _, q := range qs {
   147  		if q.Contour != c {
   148  			c = q.Contour
   149  			i = len(o)
   150  			o = append(o, StrokeQuads{})
   151  		}
   152  		o[i] = append(o[i], q)
   153  	}
   154  
   155  	return o
   156  }
   157  
   158  func (qs StrokeQuads) stroke(stroke StrokeStyle, dashes DashOp) StrokeQuads {
   159  	if !IsSolidLine(dashes) {
   160  		qs = qs.dash(dashes)
   161  	}
   162  
   163  	var (
   164  		o  StrokeQuads
   165  		hw = 0.5 * stroke.Width
   166  	)
   167  
   168  	for _, ps := range qs.split() {
   169  		rhs, lhs := ps.offset(hw, stroke)
   170  		switch lhs {
   171  		case nil:
   172  			o = o.append(rhs)
   173  		default:
   174  			// Closed path.
   175  			// Inner path should go opposite direction to cancel outer path.
   176  			switch {
   177  			case ps.ccw():
   178  				lhs = lhs.reverse()
   179  				o = o.append(rhs)
   180  				o = o.append(lhs)
   181  			default:
   182  				rhs = rhs.reverse()
   183  				o = o.append(lhs)
   184  				o = o.append(rhs)
   185  			}
   186  		}
   187  	}
   188  
   189  	return o
   190  }
   191  
   192  // offset returns the right-hand and left-hand sides of the path, offset by
   193  // the half-width hw.
   194  // The stroke handles how segments are joined and ends are capped.
   195  func (qs StrokeQuads) offset(hw float32, stroke StrokeStyle) (rhs, lhs StrokeQuads) {
   196  	var (
   197  		states []strokeState
   198  		beg    = qs[0].Quad.From
   199  		end    = qs[len(qs)-1].Quad.To
   200  		closed = beg == end
   201  	)
   202  	for i := range qs {
   203  		q := qs[i].Quad
   204  
   205  		var (
   206  			n0 = strokePathNorm(q.From, q.Ctrl, q.To, 0, hw)
   207  			n1 = strokePathNorm(q.From, q.Ctrl, q.To, 1, hw)
   208  			r0 = strokePathCurv(q.From, q.Ctrl, q.To, 0)
   209  			r1 = strokePathCurv(q.From, q.Ctrl, q.To, 1)
   210  		)
   211  		states = append(states, strokeState{
   212  			p0:  q.From,
   213  			p1:  q.To,
   214  			n0:  n0,
   215  			n1:  n1,
   216  			r0:  r0,
   217  			r1:  r1,
   218  			ctl: q.Ctrl,
   219  		})
   220  	}
   221  
   222  	for i, state := range states {
   223  		rhs = rhs.append(strokeQuadBezier(state, +hw, strokeTolerance))
   224  		lhs = lhs.append(strokeQuadBezier(state, -hw, strokeTolerance))
   225  
   226  		// join the current and next segments
   227  		if hasNext := i+1 < len(states); hasNext || closed {
   228  			var next strokeState
   229  			switch {
   230  			case hasNext:
   231  				next = states[i+1]
   232  			case closed:
   233  				next = states[0]
   234  			}
   235  			if state.n1 != next.n0 {
   236  				strokePathJoin(stroke, &rhs, &lhs, hw, state.p1, state.n1, next.n0, state.r1, next.r0)
   237  			}
   238  		}
   239  	}
   240  
   241  	if closed {
   242  		rhs.close()
   243  		lhs.close()
   244  		return rhs, lhs
   245  	}
   246  
   247  	qbeg := &states[0]
   248  	qend := &states[len(states)-1]
   249  
   250  	// Default to counter-clockwise direction.
   251  	lhs = lhs.reverse()
   252  	strokePathCap(stroke, &rhs, hw, qend.p1, qend.n1)
   253  
   254  	rhs = rhs.append(lhs)
   255  	strokePathCap(stroke, &rhs, hw, qbeg.p0, qbeg.n0.Mul(-1))
   256  
   257  	rhs.close()
   258  
   259  	return rhs, nil
   260  }
   261  
   262  func (qs *StrokeQuads) close() {
   263  	p0 := (*qs)[len(*qs)-1].Quad.To
   264  	p1 := (*qs)[0].Quad.From
   265  
   266  	if p1 == p0 {
   267  		return
   268  	}
   269  
   270  	*qs = append(*qs, StrokeQuad{
   271  		Quad: QuadSegment{
   272  			From: p0,
   273  			Ctrl: p0.Add(p1).Mul(0.5),
   274  			To:   p1,
   275  		},
   276  	})
   277  }
   278  
   279  // ccw returns whether the path is counter-clockwise.
   280  func (qs StrokeQuads) ccw() bool {
   281  	// Use the Shoelace formula:
   282  	//  https://en.wikipedia.org/wiki/Shoelace_formula
   283  	var area float32
   284  	for _, ps := range qs.split() {
   285  		for i := 1; i < len(ps); i++ {
   286  			pi := ps[i].Quad.To
   287  			pj := ps[i-1].Quad.To
   288  			area += (pi.X - pj.X) * (pi.Y + pj.Y)
   289  		}
   290  	}
   291  	return area <= 0.0
   292  }
   293  
   294  func (qs StrokeQuads) reverse() StrokeQuads {
   295  	if len(qs) == 0 {
   296  		return nil
   297  	}
   298  
   299  	ps := make(StrokeQuads, 0, len(qs))
   300  	for i := range qs {
   301  		q := qs[len(qs)-1-i]
   302  		q.Quad.To, q.Quad.From = q.Quad.From, q.Quad.To
   303  		ps = append(ps, q)
   304  	}
   305  
   306  	return ps
   307  }
   308  
   309  func (qs StrokeQuads) append(ps StrokeQuads) StrokeQuads {
   310  	switch {
   311  	case len(ps) == 0:
   312  		return qs
   313  	case len(qs) == 0:
   314  		return ps
   315  	}
   316  
   317  	// Consolidate quads and smooth out rounding errors.
   318  	// We need to also check for the strokeTolerance to correctly handle
   319  	// join/cap points or on-purpose disjoint quads.
   320  	p0 := qs[len(qs)-1].Quad.To
   321  	p1 := ps[0].Quad.From
   322  	if p0 != p1 && lenPt(p0.Sub(p1)) < strokeTolerance {
   323  		qs = append(qs, StrokeQuad{
   324  			Quad: QuadSegment{
   325  				From: p0,
   326  				Ctrl: p0.Add(p1).Mul(0.5),
   327  				To:   p1,
   328  			},
   329  		})
   330  	}
   331  	return append(qs, ps...)
   332  }
   333  
   334  func (q QuadSegment) Transform(t f32.Affine2D) QuadSegment {
   335  	q.From = t.Transform(q.From)
   336  	q.Ctrl = t.Transform(q.Ctrl)
   337  	q.To = t.Transform(q.To)
   338  	return q
   339  }
   340  
   341  // strokePathNorm returns the normal vector at t.
   342  func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point {
   343  	switch t {
   344  	case 0:
   345  		n := p1.Sub(p0)
   346  		if n.X == 0 && n.Y == 0 {
   347  			return f32.Point{}
   348  		}
   349  		n = rot90CW(n)
   350  		return normPt(n, d)
   351  	case 1:
   352  		n := p2.Sub(p1)
   353  		if n.X == 0 && n.Y == 0 {
   354  			return f32.Point{}
   355  		}
   356  		n = rot90CW(n)
   357  		return normPt(n, d)
   358  	}
   359  	panic("impossible")
   360  }
   361  
   362  func rot90CW(p f32.Point) f32.Point  { return f32.Pt(+p.Y, -p.X) }
   363  func rot90CCW(p f32.Point) f32.Point { return f32.Pt(-p.Y, +p.X) }
   364  
   365  // cosPt returns the cosine of the opening angle between p and q.
   366  func cosPt(p, q f32.Point) float32 {
   367  	np := math.Hypot(float64(p.X), float64(p.Y))
   368  	nq := math.Hypot(float64(q.X), float64(q.Y))
   369  	return dotPt(p, q) / float32(np*nq)
   370  }
   371  
   372  func normPt(p f32.Point, l float32) f32.Point {
   373  	d := math.Hypot(float64(p.X), float64(p.Y))
   374  	l64 := float64(l)
   375  	if math.Abs(d-l64) < 1e-10 {
   376  		return f32.Point{}
   377  	}
   378  	n := float32(l64 / d)
   379  	return f32.Point{X: p.X * n, Y: p.Y * n}
   380  }
   381  
   382  func lenPt(p f32.Point) float32 {
   383  	return float32(math.Hypot(float64(p.X), float64(p.Y)))
   384  }
   385  
   386  func dotPt(p, q f32.Point) float32 {
   387  	return p.X*q.X + p.Y*q.Y
   388  }
   389  
   390  func perpDot(p, q f32.Point) float32 {
   391  	return p.X*q.Y - p.Y*q.X
   392  }
   393  
   394  // strokePathCurv returns the curvature at t, along the quadratic Bézier
   395  // curve defined by the triplet (beg, ctl, end).
   396  func strokePathCurv(beg, ctl, end f32.Point, t float32) float32 {
   397  	var (
   398  		d1p = quadBezierD1(beg, ctl, end, t)
   399  		d2p = quadBezierD2(beg, ctl, end, t)
   400  
   401  		// Negative when bending right, ie: the curve is CW at this point.
   402  		a = float64(perpDot(d1p, d2p))
   403  	)
   404  
   405  	// We check early that the segment isn't too line-like and
   406  	// save a costly call to math.Pow that will be discarded by dividing
   407  	// with a too small 'a'.
   408  	if math.Abs(a) < 1e-10 {
   409  		return float32(math.NaN())
   410  	}
   411  	return float32(math.Pow(float64(d1p.X*d1p.X+d1p.Y*d1p.Y), 1.5) / a)
   412  }
   413  
   414  // quadBezierSample returns the point on the Bézier curve at t.
   415  //  B(t) = (1-t)^2 P0 + 2(1-t)t P1 + t^2 P2
   416  func quadBezierSample(p0, p1, p2 f32.Point, t float32) f32.Point {
   417  	t1 := 1 - t
   418  	c0 := t1 * t1
   419  	c1 := 2 * t1 * t
   420  	c2 := t * t
   421  
   422  	o := p0.Mul(c0)
   423  	o = o.Add(p1.Mul(c1))
   424  	o = o.Add(p2.Mul(c2))
   425  	return o
   426  }
   427  
   428  // quadBezierD1 returns the first derivative of the Bézier curve with respect to t.
   429  //  B'(t) = 2(1-t)(P1 - P0) + 2t(P2 - P1)
   430  func quadBezierD1(p0, p1, p2 f32.Point, t float32) f32.Point {
   431  	p10 := p1.Sub(p0).Mul(2 * (1 - t))
   432  	p21 := p2.Sub(p1).Mul(2 * t)
   433  
   434  	return p10.Add(p21)
   435  }
   436  
   437  // quadBezierD2 returns the second derivative of the Bézier curve with respect to t:
   438  //  B''(t) = 2(P2 - 2P1 + P0)
   439  func quadBezierD2(p0, p1, p2 f32.Point, t float32) f32.Point {
   440  	p := p2.Sub(p1.Mul(2)).Add(p0)
   441  	return p.Mul(2)
   442  }
   443  
   444  // quadBezierLen returns the length of the Bézier curve.
   445  // See:
   446  //  https://malczak.linuxpl.com/blog/quadratic-bezier-curve-length/
   447  func quadBezierLen(p0, p1, p2 f32.Point) float32 {
   448  	a := p0.Sub(p1.Mul(2)).Add(p2)
   449  	b := p1.Mul(2).Sub(p0.Mul(2))
   450  	A := float64(4 * dotPt(a, a))
   451  	B := float64(4 * dotPt(a, b))
   452  	C := float64(dotPt(b, b))
   453  	if f64Eq(A, 0.0) {
   454  		// p1 is in the middle between p0 and p2,
   455  		// so it is a straight line from p0 to p2.
   456  		return lenPt(p2.Sub(p0))
   457  	}
   458  
   459  	Sabc := 2 * math.Sqrt(A+B+C)
   460  	A2 := math.Sqrt(A)
   461  	A32 := 2 * A * A2
   462  	C2 := 2 * math.Sqrt(C)
   463  	BA := B / A2
   464  	return float32((A32*Sabc + A2*B*(Sabc-C2) + (4*C*A-B*B)*math.Log((2*A2+BA+Sabc)/(BA+C2))) / (4 * A32))
   465  }
   466  
   467  func strokeQuadBezier(state strokeState, d, flatness float32) StrokeQuads {
   468  	// Gio strokes are only quadratic Bézier curves, w/o any inflection point.
   469  	// So we just have to flatten them.
   470  	var qs StrokeQuads
   471  	return flattenQuadBezier(qs, state.p0, state.ctl, state.p1, d, flatness)
   472  }
   473  
   474  // flattenQuadBezier splits a Bézier quadratic curve into linear sub-segments,
   475  // themselves also encoded as Bézier (degenerate, flat) quadratic curves.
   476  func flattenQuadBezier(qs StrokeQuads, p0, p1, p2 f32.Point, d, flatness float32) StrokeQuads {
   477  	var (
   478  		t      float32
   479  		flat64 = float64(flatness)
   480  	)
   481  	for t < 1 {
   482  		s2 := float64((p2.X-p0.X)*(p1.Y-p0.Y) - (p2.Y-p0.Y)*(p1.X-p0.X))
   483  		den := math.Hypot(float64(p1.X-p0.X), float64(p1.Y-p0.Y))
   484  		if s2*den == 0.0 {
   485  			break
   486  		}
   487  
   488  		s2 /= den
   489  		t = 2.0 * float32(math.Sqrt(flat64/3.0/math.Abs(s2)))
   490  		if t >= 1.0 {
   491  			break
   492  		}
   493  		var q0, q1, q2 f32.Point
   494  		q0, q1, q2, p0, p1, p2 = quadBezierSplit(p0, p1, p2, t)
   495  		qs.addLine(q0, q1, q2, 0, d)
   496  	}
   497  	qs.addLine(p0, p1, p2, 1, d)
   498  	return qs
   499  }
   500  
   501  func (qs *StrokeQuads) addLine(p0, ctrl, p1 f32.Point, t, d float32) {
   502  
   503  	switch i := len(*qs); i {
   504  	case 0:
   505  		p0 = p0.Add(strokePathNorm(p0, ctrl, p1, 0, d))
   506  	default:
   507  		// Address possible rounding errors and use previous point.
   508  		p0 = (*qs)[i-1].Quad.To
   509  	}
   510  
   511  	p1 = p1.Add(strokePathNorm(p0, ctrl, p1, 1, d))
   512  
   513  	*qs = append(*qs,
   514  		StrokeQuad{
   515  			Quad: QuadSegment{
   516  				From: p0,
   517  				Ctrl: p0.Add(p1).Mul(0.5),
   518  				To:   p1,
   519  			},
   520  		},
   521  	)
   522  }
   523  
   524  // quadInterp returns the interpolated point at t.
   525  func quadInterp(p, q f32.Point, t float32) f32.Point {
   526  	return f32.Pt(
   527  		(1-t)*p.X+t*q.X,
   528  		(1-t)*p.Y+t*q.Y,
   529  	)
   530  }
   531  
   532  // quadBezierSplit returns the pair of triplets (from,ctrl,to) Bézier curve,
   533  // split before (resp. after) the provided parametric t value.
   534  func quadBezierSplit(p0, p1, p2 f32.Point, t float32) (f32.Point, f32.Point, f32.Point, f32.Point, f32.Point, f32.Point) {
   535  
   536  	var (
   537  		b0 = p0
   538  		b1 = quadInterp(p0, p1, t)
   539  		b2 = quadBezierSample(p0, p1, p2, t)
   540  
   541  		a0 = b2
   542  		a1 = quadInterp(p1, p2, t)
   543  		a2 = p2
   544  	)
   545  
   546  	return b0, b1, b2, a0, a1, a2
   547  }
   548  
   549  // strokePathJoin joins the two paths rhs and lhs, according to the provided
   550  // stroke operation.
   551  func strokePathJoin(stroke StrokeStyle, rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
   552  	if stroke.Miter > 0 {
   553  		strokePathMiterJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1)
   554  		return
   555  	}
   556  	switch stroke.Join {
   557  	case BevelJoin:
   558  		strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1)
   559  	case RoundJoin:
   560  		strokePathRoundJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1)
   561  	default:
   562  		panic("impossible")
   563  	}
   564  }
   565  
   566  func strokePathBevelJoin(rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
   567  
   568  	rp := pivot.Add(n1)
   569  	lp := pivot.Sub(n1)
   570  
   571  	rhs.lineTo(rp)
   572  	lhs.lineTo(lp)
   573  }
   574  
   575  func strokePathRoundJoin(rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
   576  	rp := pivot.Add(n1)
   577  	lp := pivot.Sub(n1)
   578  	cw := dotPt(rot90CW(n0), n1) >= 0.0
   579  	switch {
   580  	case cw:
   581  		// Path bends to the right, ie. CW (or 180 degree turn).
   582  		c := pivot.Sub(lhs.pen())
   583  		angle := -math.Acos(float64(cosPt(n0, n1)))
   584  		lhs.arc(c, c, float32(angle))
   585  		lhs.lineTo(lp) // Add a line to accommodate for rounding errors.
   586  		rhs.lineTo(rp)
   587  	default:
   588  		// Path bends to the left, ie. CCW.
   589  		angle := math.Acos(float64(cosPt(n0, n1)))
   590  		c := pivot.Sub(rhs.pen())
   591  		rhs.arc(c, c, float32(angle))
   592  		rhs.lineTo(rp) // Add a line to accommodate for rounding errors.
   593  		lhs.lineTo(lp)
   594  	}
   595  }
   596  
   597  func strokePathMiterJoin(stroke StrokeStyle, rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
   598  	if n0 == n1.Mul(-1) {
   599  		strokePathBevelJoin(rhs, lhs, hw, pivot, n0, n1, r0, r1)
   600  		return
   601  	}
   602  
   603  	// This is to handle nearly linear joints that would be clipped otherwise.
   604  	limit := math.Max(float64(stroke.Miter), 1.001)
   605  
   606  	cw := dotPt(rot90CW(n0), n1) >= 0.0
   607  	if cw {
   608  		// hw is used to calculate |R|.
   609  		// When running CW, n0 and n1 point the other way,
   610  		// so the sign of r0 and r1 is negated.
   611  		hw = -hw
   612  	}
   613  	hw64 := float64(hw)
   614  
   615  	cos := math.Sqrt(0.5 * (1 + float64(cosPt(n0, n1))))
   616  	d := hw64 / cos
   617  	if math.Abs(limit*hw64) < math.Abs(d) {
   618  		stroke.Miter = 0 // Set miter to zero to disable the miter joint.
   619  		strokePathJoin(stroke, rhs, lhs, hw, pivot, n0, n1, r0, r1)
   620  		return
   621  	}
   622  	mid := pivot.Add(normPt(n0.Add(n1), float32(d)))
   623  
   624  	rp := pivot.Add(n1)
   625  	lp := pivot.Sub(n1)
   626  	switch {
   627  	case cw:
   628  		// Path bends to the right, ie. CW.
   629  		lhs.lineTo(mid)
   630  	default:
   631  		// Path bends to the left, ie. CCW.
   632  		rhs.lineTo(mid)
   633  	}
   634  	rhs.lineTo(rp)
   635  	lhs.lineTo(lp)
   636  }
   637  
   638  // strokePathCap caps the provided path qs, according to the provided stroke operation.
   639  func strokePathCap(stroke StrokeStyle, qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
   640  	switch stroke.Cap {
   641  	case FlatCap:
   642  		strokePathFlatCap(qs, hw, pivot, n0)
   643  	case SquareCap:
   644  		strokePathSquareCap(qs, hw, pivot, n0)
   645  	case RoundCap:
   646  		strokePathRoundCap(qs, hw, pivot, n0)
   647  	default:
   648  		panic("impossible")
   649  	}
   650  }
   651  
   652  // strokePathFlatCap caps the start or end of a path with a flat cap.
   653  func strokePathFlatCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
   654  	end := pivot.Sub(n0)
   655  	qs.lineTo(end)
   656  }
   657  
   658  // strokePathSquareCap caps the start or end of a path with a square cap.
   659  func strokePathSquareCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
   660  	var (
   661  		e       = pivot.Add(rot90CCW(n0))
   662  		corner1 = e.Add(n0)
   663  		corner2 = e.Sub(n0)
   664  		end     = pivot.Sub(n0)
   665  	)
   666  
   667  	qs.lineTo(corner1)
   668  	qs.lineTo(corner2)
   669  	qs.lineTo(end)
   670  }
   671  
   672  // strokePathRoundCap caps the start or end of a path with a round cap.
   673  func strokePathRoundCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
   674  	c := pivot.Sub(qs.pen())
   675  	qs.arc(c, c, math.Pi)
   676  }
   677  
   678  // ArcTransform computes a transformation that can be used for generating quadratic bézier
   679  // curve approximations for an arc.
   680  //
   681  // The math is extracted from the following paper:
   682  //  "Drawing an elliptical arc using polylines, quadratic or
   683  //   cubic Bezier curves", L. Maisonobe
   684  // An electronic version may be found at:
   685  //  http://spaceroots.org/documents/ellipse/elliptical-arc.pdf
   686  func ArcTransform(p, f1, f2 f32.Point, angle float32, segments int) f32.Affine2D {
   687  	c := f32.Point{
   688  		X: 0.5 * (f1.X + f2.X),
   689  		Y: 0.5 * (f1.Y + f2.Y),
   690  	}
   691  
   692  	// semi-major axis: 2a = |PF1| + |PF2|
   693  	a := 0.5 * (dist(f1, p) + dist(f2, p))
   694  
   695  	// semi-minor axis: c^2 = a^2+b^2 (c: focal distance)
   696  	f := dist(f1, c)
   697  	b := math.Sqrt(a*a - f*f)
   698  
   699  	var rx, ry, alpha float64
   700  	switch {
   701  	case a > b:
   702  		rx = a
   703  		ry = b
   704  	default:
   705  		rx = b
   706  		ry = a
   707  	}
   708  
   709  	var x float64
   710  	switch {
   711  	case f1 == c || f2 == c:
   712  		// degenerate case of a circle.
   713  		alpha = 0
   714  	default:
   715  		switch {
   716  		case f1.X > c.X:
   717  			x = float64(f1.X - c.X)
   718  			alpha = math.Acos(x / f)
   719  		case f1.X < c.X:
   720  			x = float64(f2.X - c.X)
   721  			alpha = math.Acos(x / f)
   722  		case f1.X == c.X:
   723  			// special case of a "vertical" ellipse.
   724  			alpha = math.Pi / 2
   725  			if f1.Y < c.Y {
   726  				alpha = -alpha
   727  			}
   728  		}
   729  	}
   730  
   731  	var (
   732  		θ   = angle / float32(segments)
   733  		ref f32.Affine2D // transform from absolute frame to ellipse-based one
   734  		rot f32.Affine2D // rotation matrix for each segment
   735  		inv f32.Affine2D // transform from ellipse-based frame to absolute one
   736  	)
   737  	ref = ref.Offset(f32.Point{}.Sub(c))
   738  	ref = ref.Rotate(f32.Point{}, float32(-alpha))
   739  	ref = ref.Scale(f32.Point{}, f32.Point{
   740  		X: float32(1 / rx),
   741  		Y: float32(1 / ry),
   742  	})
   743  	inv = ref.Invert()
   744  	rot = rot.Rotate(f32.Point{}, float32(0.5*θ))
   745  
   746  	// Instead of invoking math.Sincos for every segment, compute a rotation
   747  	// matrix once and apply for each segment.
   748  	// Before applying the rotation matrix rot, transform the coordinates
   749  	// to a frame centered to the ellipse (and warped into a unit circle), then rotate.
   750  	// Finally, transform back into the original frame.
   751  	return inv.Mul(rot).Mul(ref)
   752  }
   753  
   754  func dist(p1, p2 f32.Point) float64 {
   755  	var (
   756  		x1 = float64(p1.X)
   757  		y1 = float64(p1.Y)
   758  		x2 = float64(p2.X)
   759  		y2 = float64(p2.Y)
   760  		dx = x2 - x1
   761  		dy = y2 - y1
   762  	)
   763  	return math.Hypot(dx, dy)
   764  }
   765  
   766  func StrokePathCommands(style StrokeStyle, dashes DashOp, scene []byte) StrokeQuads {
   767  	quads := decodeToStrokeQuads(scene)
   768  	return quads.stroke(style, dashes)
   769  }
   770  
   771  // decodeToStrokeQuads decodes scene commands to quads ready to stroke.
   772  func decodeToStrokeQuads(pathData []byte) StrokeQuads {
   773  	quads := make(StrokeQuads, 0, 2*len(pathData)/(scene.CommandSize+4))
   774  	for len(pathData) >= scene.CommandSize+4 {
   775  		contour := binary.LittleEndian.Uint32(pathData)
   776  		cmd := ops.DecodeCommand(pathData[4:])
   777  		switch cmd.Op() {
   778  		case scene.OpLine:
   779  			var q QuadSegment
   780  			q.From, q.To = scene.DecodeLine(cmd)
   781  			q.Ctrl = q.From.Add(q.To).Mul(.5)
   782  			quad := StrokeQuad{
   783  				Contour: contour,
   784  				Quad:    q,
   785  			}
   786  			quads = append(quads, quad)
   787  		case scene.OpQuad:
   788  			var q QuadSegment
   789  			q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
   790  			quad := StrokeQuad{
   791  				Contour: contour,
   792  				Quad:    q,
   793  			}
   794  			quads = append(quads, quad)
   795  		case scene.OpCubic:
   796  			for _, q := range SplitCubic(scene.DecodeCubic(cmd)) {
   797  				quad := StrokeQuad{
   798  					Contour: contour,
   799  					Quad:    q,
   800  				}
   801  				quads = append(quads, quad)
   802  			}
   803  		default:
   804  			panic("unsupported scene command")
   805  		}
   806  		pathData = pathData[scene.CommandSize+4:]
   807  	}
   808  	return quads
   809  }
   810  
   811  func SplitCubic(from, ctrl0, ctrl1, to f32.Point) []QuadSegment {
   812  	quads := make([]QuadSegment, 0, 10)
   813  	// Set the maximum distance proportionally to the longest side
   814  	// of the bounding rectangle.
   815  	hull := f32.Rectangle{
   816  		Min: from,
   817  		Max: ctrl0,
   818  	}.Canon().Add(ctrl1).Add(to)
   819  	l := hull.Dx()
   820  	if h := hull.Dy(); h > l {
   821  		l = h
   822  	}
   823  	approxCubeTo(&quads, 0, l*0.001, from, ctrl0, ctrl1, to)
   824  	return quads
   825  }
   826  
   827  // approxCubeTo approximates a cubic Bézier by a series of quadratic
   828  // curves.
   829  func approxCubeTo(quads *[]QuadSegment, splits int, maxDist float32, from, ctrl0, ctrl1, to f32.Point) int {
   830  	// The idea is from
   831  	// https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html
   832  	// where a quadratic approximates a cubic by eliminating its t³ term
   833  	// from its polynomial expression anchored at the starting point:
   834  	//
   835  	// P(t) = pen + 3t(ctrl0 - pen) + 3t²(ctrl1 - 2ctrl0 + pen) + t³(to - 3ctrl1 + 3ctrl0 - pen)
   836  	//
   837  	// The control point for the new quadratic Q1 that shares starting point, pen, with P is
   838  	//
   839  	// C1 = (3ctrl0 - pen)/2
   840  	//
   841  	// The reverse cubic anchored at the end point has the polynomial
   842  	//
   843  	// P'(t) = to + 3t(ctrl1 - to) + 3t²(ctrl0 - 2ctrl1 + to) + t³(pen - 3ctrl0 + 3ctrl1 - to)
   844  	//
   845  	// The corresponding quadratic Q2 that shares the end point, to, with P has control
   846  	// point
   847  	//
   848  	// C2 = (3ctrl1 - to)/2
   849  	//
   850  	// The combined quadratic Bézier, Q, shares both start and end points with its cubic
   851  	// and use the midpoint between the two curves Q1 and Q2 as control point:
   852  	//
   853  	// C = (3ctrl0 - pen + 3ctrl1 - to)/4
   854  	c := ctrl0.Mul(3).Sub(from).Add(ctrl1.Mul(3)).Sub(to).Mul(1.0 / 4.0)
   855  	const maxSplits = 32
   856  	if splits >= maxSplits {
   857  		*quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to})
   858  		return splits
   859  	}
   860  	// The maximum distance between the cubic P and its approximation Q given t
   861  	// can be shown to be
   862  	//
   863  	// d = sqrt(3)/36*|to - 3ctrl1 + 3ctrl0 - pen|
   864  	//
   865  	// To save a square root, compare d² with the squared tolerance.
   866  	v := to.Sub(ctrl1.Mul(3)).Add(ctrl0.Mul(3)).Sub(from)
   867  	d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36)
   868  	if d2 <= maxDist*maxDist {
   869  		*quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to})
   870  		return splits
   871  	}
   872  	// De Casteljau split the curve and approximate the halves.
   873  	t := float32(0.5)
   874  	c0 := from.Add(ctrl0.Sub(from).Mul(t))
   875  	c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t))
   876  	c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t))
   877  	c01 := c0.Add(c1.Sub(c0).Mul(t))
   878  	c12 := c1.Add(c2.Sub(c1).Mul(t))
   879  	c0112 := c01.Add(c12.Sub(c01).Mul(t))
   880  	splits++
   881  	splits = approxCubeTo(quads, splits, maxDist, from, c0, c01, c0112)
   882  	splits = approxCubeTo(quads, splits, maxDist, c0112, c12, c2, to)
   883  	return splits
   884  }