github.com/utopiagio/gio@v0.0.8/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/utopiagio/gio/internal/f32"
    33  	"github.com/utopiagio/gio/internal/ops"
    34  	"github.com/utopiagio/gio/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  }
    44  
    45  // strokeTolerance is used to reconcile rounding errors arising
    46  // when splitting quads into smaller and smaller segments to approximate
    47  // them into straight lines, and when joining back segments.
    48  //
    49  // The magic value of 0.01 was found by striking a compromise between
    50  // aesthetic looking (curves did look like curves, even after linearization)
    51  // and speed.
    52  const strokeTolerance = 0.01
    53  
    54  type QuadSegment struct {
    55  	From, Ctrl, To f32.Point
    56  }
    57  
    58  type StrokeQuad struct {
    59  	Contour uint32
    60  	Quad    QuadSegment
    61  }
    62  
    63  type strokeState struct {
    64  	p0, p1 f32.Point // p0 is the start point, p1 the end point.
    65  	n0, n1 f32.Point // n0 is the normal vector at the start point, n1 at the end point.
    66  	r0, r1 float32   // r0 is the curvature at the start point, r1 at the end point.
    67  	ctl    f32.Point // ctl is the control point of the quadratic Bézier segment.
    68  }
    69  
    70  type StrokeQuads []StrokeQuad
    71  
    72  func (qs *StrokeQuads) pen() f32.Point {
    73  	return (*qs)[len(*qs)-1].Quad.To
    74  }
    75  
    76  func (qs *StrokeQuads) lineTo(pt f32.Point) {
    77  	end := qs.pen()
    78  	*qs = append(*qs, StrokeQuad{
    79  		Quad: QuadSegment{
    80  			From: end,
    81  			Ctrl: end.Add(pt).Mul(0.5),
    82  			To:   pt,
    83  		},
    84  	})
    85  }
    86  
    87  func (qs *StrokeQuads) arc(f1, f2 f32.Point, angle float32) {
    88  	pen := qs.pen()
    89  	m, segments := ArcTransform(pen, f1.Add(pen), f2.Add(pen), angle)
    90  	for i := 0; i < segments; i++ {
    91  		p0 := qs.pen()
    92  		p1 := m.Transform(p0)
    93  		p2 := m.Transform(p1)
    94  		ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5))
    95  		*qs = append(*qs, StrokeQuad{
    96  			Quad: QuadSegment{
    97  				From: p0, Ctrl: ctl, To: p2,
    98  			},
    99  		})
   100  	}
   101  }
   102  
   103  // split splits a slice of quads into slices of quads grouped
   104  // by contours (ie: splitted at move-to boundaries).
   105  func (qs StrokeQuads) split() []StrokeQuads {
   106  	if len(qs) == 0 {
   107  		return nil
   108  	}
   109  
   110  	var (
   111  		c uint32
   112  		o []StrokeQuads
   113  		i = len(o)
   114  	)
   115  	for _, q := range qs {
   116  		if q.Contour != c {
   117  			c = q.Contour
   118  			i = len(o)
   119  			o = append(o, StrokeQuads{})
   120  		}
   121  		o[i] = append(o[i], q)
   122  	}
   123  
   124  	return o
   125  }
   126  
   127  func (qs StrokeQuads) stroke(stroke StrokeStyle) StrokeQuads {
   128  	var (
   129  		o  StrokeQuads
   130  		hw = 0.5 * stroke.Width
   131  	)
   132  
   133  	for _, ps := range qs.split() {
   134  		rhs, lhs := ps.offset(hw, stroke)
   135  		switch lhs {
   136  		case nil:
   137  			o = o.append(rhs)
   138  		default:
   139  			// Closed path.
   140  			// Inner path should go opposite direction to cancel outer path.
   141  			switch {
   142  			case ps.ccw():
   143  				lhs = lhs.reverse()
   144  				o = o.append(rhs)
   145  				o = o.append(lhs)
   146  			default:
   147  				rhs = rhs.reverse()
   148  				o = o.append(lhs)
   149  				o = o.append(rhs)
   150  			}
   151  		}
   152  	}
   153  
   154  	return o
   155  }
   156  
   157  // offset returns the right-hand and left-hand sides of the path, offset by
   158  // the half-width hw.
   159  // The stroke handles how segments are joined and ends are capped.
   160  func (qs StrokeQuads) offset(hw float32, stroke StrokeStyle) (rhs, lhs StrokeQuads) {
   161  	var (
   162  		states []strokeState
   163  		beg    = qs[0].Quad.From
   164  		end    = qs[len(qs)-1].Quad.To
   165  		closed = beg == end
   166  	)
   167  	for i := range qs {
   168  		q := qs[i].Quad
   169  
   170  		var (
   171  			n0 = strokePathNorm(q.From, q.Ctrl, q.To, 0, hw)
   172  			n1 = strokePathNorm(q.From, q.Ctrl, q.To, 1, hw)
   173  			r0 = strokePathCurv(q.From, q.Ctrl, q.To, 0)
   174  			r1 = strokePathCurv(q.From, q.Ctrl, q.To, 1)
   175  		)
   176  		states = append(states, strokeState{
   177  			p0:  q.From,
   178  			p1:  q.To,
   179  			n0:  n0,
   180  			n1:  n1,
   181  			r0:  r0,
   182  			r1:  r1,
   183  			ctl: q.Ctrl,
   184  		})
   185  	}
   186  
   187  	for i, state := range states {
   188  		rhs = rhs.append(strokeQuadBezier(state, +hw, strokeTolerance))
   189  		lhs = lhs.append(strokeQuadBezier(state, -hw, strokeTolerance))
   190  
   191  		// join the current and next segments
   192  		if hasNext := i+1 < len(states); hasNext || closed {
   193  			var next strokeState
   194  			switch {
   195  			case hasNext:
   196  				next = states[i+1]
   197  			case closed:
   198  				next = states[0]
   199  			}
   200  			if state.n1 != next.n0 {
   201  				strokePathRoundJoin(&rhs, &lhs, hw, state.p1, state.n1, next.n0, state.r1, next.r0)
   202  			}
   203  		}
   204  	}
   205  
   206  	if closed {
   207  		rhs.close()
   208  		lhs.close()
   209  		return rhs, lhs
   210  	}
   211  
   212  	qbeg := &states[0]
   213  	qend := &states[len(states)-1]
   214  
   215  	// Default to counter-clockwise direction.
   216  	lhs = lhs.reverse()
   217  	strokePathCap(stroke, &rhs, hw, qend.p1, qend.n1)
   218  
   219  	rhs = rhs.append(lhs)
   220  	strokePathCap(stroke, &rhs, hw, qbeg.p0, qbeg.n0.Mul(-1))
   221  
   222  	rhs.close()
   223  
   224  	return rhs, nil
   225  }
   226  
   227  func (qs *StrokeQuads) close() {
   228  	p0 := (*qs)[len(*qs)-1].Quad.To
   229  	p1 := (*qs)[0].Quad.From
   230  
   231  	if p1 == p0 {
   232  		return
   233  	}
   234  
   235  	*qs = append(*qs, StrokeQuad{
   236  		Quad: QuadSegment{
   237  			From: p0,
   238  			Ctrl: p0.Add(p1).Mul(0.5),
   239  			To:   p1,
   240  		},
   241  	})
   242  }
   243  
   244  // ccw returns whether the path is counter-clockwise.
   245  func (qs StrokeQuads) ccw() bool {
   246  	// Use the Shoelace formula:
   247  	//  https://en.wikipedia.org/wiki/Shoelace_formula
   248  	var area float32
   249  	for _, ps := range qs.split() {
   250  		for i := 1; i < len(ps); i++ {
   251  			pi := ps[i].Quad.To
   252  			pj := ps[i-1].Quad.To
   253  			area += (pi.X - pj.X) * (pi.Y + pj.Y)
   254  		}
   255  	}
   256  	return area <= 0.0
   257  }
   258  
   259  func (qs StrokeQuads) reverse() StrokeQuads {
   260  	if len(qs) == 0 {
   261  		return nil
   262  	}
   263  
   264  	ps := make(StrokeQuads, 0, len(qs))
   265  	for i := range qs {
   266  		q := qs[len(qs)-1-i]
   267  		q.Quad.To, q.Quad.From = q.Quad.From, q.Quad.To
   268  		ps = append(ps, q)
   269  	}
   270  
   271  	return ps
   272  }
   273  
   274  func (qs StrokeQuads) append(ps StrokeQuads) StrokeQuads {
   275  	switch {
   276  	case len(ps) == 0:
   277  		return qs
   278  	case len(qs) == 0:
   279  		return ps
   280  	}
   281  
   282  	// Consolidate quads and smooth out rounding errors.
   283  	// We need to also check for the strokeTolerance to correctly handle
   284  	// join/cap points or on-purpose disjoint quads.
   285  	p0 := qs[len(qs)-1].Quad.To
   286  	p1 := ps[0].Quad.From
   287  	if p0 != p1 && lenPt(p0.Sub(p1)) < strokeTolerance {
   288  		qs = append(qs, StrokeQuad{
   289  			Quad: QuadSegment{
   290  				From: p0,
   291  				Ctrl: p0.Add(p1).Mul(0.5),
   292  				To:   p1,
   293  			},
   294  		})
   295  	}
   296  	return append(qs, ps...)
   297  }
   298  
   299  func (q QuadSegment) Transform(t f32.Affine2D) QuadSegment {
   300  	q.From = t.Transform(q.From)
   301  	q.Ctrl = t.Transform(q.Ctrl)
   302  	q.To = t.Transform(q.To)
   303  	return q
   304  }
   305  
   306  // strokePathNorm returns the normal vector at t.
   307  func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point {
   308  	switch t {
   309  	case 0:
   310  		n := p1.Sub(p0)
   311  		if n.X == 0 && n.Y == 0 {
   312  			return f32.Point{}
   313  		}
   314  		n = rot90CW(n)
   315  		return normPt(n, d)
   316  	case 1:
   317  		n := p2.Sub(p1)
   318  		if n.X == 0 && n.Y == 0 {
   319  			return f32.Point{}
   320  		}
   321  		n = rot90CW(n)
   322  		return normPt(n, d)
   323  	}
   324  	panic("impossible")
   325  }
   326  
   327  func rot90CW(p f32.Point) f32.Point { return f32.Pt(+p.Y, -p.X) }
   328  
   329  func normPt(p f32.Point, l float32) f32.Point {
   330  	d := math.Hypot(float64(p.X), float64(p.Y))
   331  	l64 := float64(l)
   332  	if math.Abs(d-l64) < 1e-10 {
   333  		return f32.Point{}
   334  	}
   335  	n := float32(l64 / d)
   336  	return f32.Point{X: p.X * n, Y: p.Y * n}
   337  }
   338  
   339  func lenPt(p f32.Point) float32 {
   340  	return float32(math.Hypot(float64(p.X), float64(p.Y)))
   341  }
   342  
   343  func perpDot(p, q f32.Point) float32 {
   344  	return p.X*q.Y - p.Y*q.X
   345  }
   346  
   347  func angleBetween(n0, n1 f32.Point) float64 {
   348  	return math.Atan2(float64(n1.Y), float64(n1.X)) -
   349  		math.Atan2(float64(n0.Y), float64(n0.X))
   350  }
   351  
   352  // strokePathCurv returns the curvature at t, along the quadratic Bézier
   353  // curve defined by the triplet (beg, ctl, end).
   354  func strokePathCurv(beg, ctl, end f32.Point, t float32) float32 {
   355  	var (
   356  		d1p = quadBezierD1(beg, ctl, end, t)
   357  		d2p = quadBezierD2(beg, ctl, end, t)
   358  
   359  		// Negative when bending right, ie: the curve is CW at this point.
   360  		a = float64(perpDot(d1p, d2p))
   361  	)
   362  
   363  	// We check early that the segment isn't too line-like and
   364  	// save a costly call to math.Pow that will be discarded by dividing
   365  	// with a too small 'a'.
   366  	if math.Abs(a) < 1e-10 {
   367  		return float32(math.NaN())
   368  	}
   369  	return float32(math.Pow(float64(d1p.X*d1p.X+d1p.Y*d1p.Y), 1.5) / a)
   370  }
   371  
   372  // quadBezierSample returns the point on the Bézier curve at t.
   373  //
   374  //	B(t) = (1-t)^2 P0 + 2(1-t)t P1 + t^2 P2
   375  func quadBezierSample(p0, p1, p2 f32.Point, t float32) f32.Point {
   376  	t1 := 1 - t
   377  	c0 := t1 * t1
   378  	c1 := 2 * t1 * t
   379  	c2 := t * t
   380  
   381  	o := p0.Mul(c0)
   382  	o = o.Add(p1.Mul(c1))
   383  	o = o.Add(p2.Mul(c2))
   384  	return o
   385  }
   386  
   387  // quadBezierD1 returns the first derivative of the Bézier curve with respect to t.
   388  //
   389  //	B'(t) = 2(1-t)(P1 - P0) + 2t(P2 - P1)
   390  func quadBezierD1(p0, p1, p2 f32.Point, t float32) f32.Point {
   391  	p10 := p1.Sub(p0).Mul(2 * (1 - t))
   392  	p21 := p2.Sub(p1).Mul(2 * t)
   393  
   394  	return p10.Add(p21)
   395  }
   396  
   397  // quadBezierD2 returns the second derivative of the Bézier curve with respect to t:
   398  //
   399  //	B''(t) = 2(P2 - 2P1 + P0)
   400  func quadBezierD2(p0, p1, p2 f32.Point, t float32) f32.Point {
   401  	p := p2.Sub(p1.Mul(2)).Add(p0)
   402  	return p.Mul(2)
   403  }
   404  
   405  func strokeQuadBezier(state strokeState, d, flatness float32) StrokeQuads {
   406  	// Gio strokes are only quadratic Bézier curves, w/o any inflection point.
   407  	// So we just have to flatten them.
   408  	var qs StrokeQuads
   409  	return flattenQuadBezier(qs, state.p0, state.ctl, state.p1, d, flatness)
   410  }
   411  
   412  // flattenQuadBezier splits a Bézier quadratic curve into linear sub-segments,
   413  // themselves also encoded as Bézier (degenerate, flat) quadratic curves.
   414  func flattenQuadBezier(qs StrokeQuads, p0, p1, p2 f32.Point, d, flatness float32) StrokeQuads {
   415  	var (
   416  		t      float32
   417  		flat64 = float64(flatness)
   418  	)
   419  	for t < 1 {
   420  		s2 := float64((p2.X-p0.X)*(p1.Y-p0.Y) - (p2.Y-p0.Y)*(p1.X-p0.X))
   421  		den := math.Hypot(float64(p1.X-p0.X), float64(p1.Y-p0.Y))
   422  		if s2*den == 0.0 {
   423  			break
   424  		}
   425  
   426  		s2 /= den
   427  		t = 2.0 * float32(math.Sqrt(flat64/3.0/math.Abs(s2)))
   428  		if t >= 1.0 {
   429  			break
   430  		}
   431  		var q0, q1, q2 f32.Point
   432  		q0, q1, q2, p0, p1, p2 = quadBezierSplit(p0, p1, p2, t)
   433  		qs.addLine(q0, q1, q2, 0, d)
   434  	}
   435  	qs.addLine(p0, p1, p2, 1, d)
   436  	return qs
   437  }
   438  
   439  func (qs *StrokeQuads) addLine(p0, ctrl, p1 f32.Point, t, d float32) {
   440  
   441  	switch i := len(*qs); i {
   442  	case 0:
   443  		p0 = p0.Add(strokePathNorm(p0, ctrl, p1, 0, d))
   444  	default:
   445  		// Address possible rounding errors and use previous point.
   446  		p0 = (*qs)[i-1].Quad.To
   447  	}
   448  
   449  	p1 = p1.Add(strokePathNorm(p0, ctrl, p1, 1, d))
   450  
   451  	*qs = append(*qs,
   452  		StrokeQuad{
   453  			Quad: QuadSegment{
   454  				From: p0,
   455  				Ctrl: p0.Add(p1).Mul(0.5),
   456  				To:   p1,
   457  			},
   458  		},
   459  	)
   460  }
   461  
   462  // quadInterp returns the interpolated point at t.
   463  func quadInterp(p, q f32.Point, t float32) f32.Point {
   464  	return f32.Pt(
   465  		(1-t)*p.X+t*q.X,
   466  		(1-t)*p.Y+t*q.Y,
   467  	)
   468  }
   469  
   470  // quadBezierSplit returns the pair of triplets (from,ctrl,to) Bézier curve,
   471  // split before (resp. after) the provided parametric t value.
   472  func quadBezierSplit(p0, p1, p2 f32.Point, t float32) (f32.Point, f32.Point, f32.Point, f32.Point, f32.Point, f32.Point) {
   473  
   474  	var (
   475  		b0 = p0
   476  		b1 = quadInterp(p0, p1, t)
   477  		b2 = quadBezierSample(p0, p1, p2, t)
   478  
   479  		a0 = b2
   480  		a1 = quadInterp(p1, p2, t)
   481  		a2 = p2
   482  	)
   483  
   484  	return b0, b1, b2, a0, a1, a2
   485  }
   486  
   487  // strokePathRoundJoin joins the two paths rhs and lhs, creating an arc.
   488  func strokePathRoundJoin(rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) {
   489  	rp := pivot.Add(n1)
   490  	lp := pivot.Sub(n1)
   491  	angle := angleBetween(n0, n1)
   492  	switch {
   493  	case angle <= 0:
   494  		// Path bends to the right, ie. CW (or 180 degree turn).
   495  		c := pivot.Sub(lhs.pen())
   496  		lhs.arc(c, c, float32(angle))
   497  		lhs.lineTo(lp) // Add a line to accommodate for rounding errors.
   498  		rhs.lineTo(rp)
   499  	default:
   500  		// Path bends to the left, ie. CCW.
   501  		c := pivot.Sub(rhs.pen())
   502  		rhs.arc(c, c, float32(angle))
   503  		rhs.lineTo(rp) // Add a line to accommodate for rounding errors.
   504  		lhs.lineTo(lp)
   505  	}
   506  }
   507  
   508  // strokePathCap caps the provided path qs, according to the provided stroke operation.
   509  func strokePathCap(stroke StrokeStyle, qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
   510  	strokePathRoundCap(qs, hw, pivot, n0)
   511  }
   512  
   513  // strokePathRoundCap caps the start or end of a path with a round cap.
   514  func strokePathRoundCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) {
   515  	c := pivot.Sub(qs.pen())
   516  	qs.arc(c, c, math.Pi)
   517  }
   518  
   519  // ArcTransform computes a transformation that can be used for generating quadratic bézier
   520  // curve approximations for an arc.
   521  //
   522  // The math is extracted from the following paper:
   523  //
   524  //	"Drawing an elliptical arc using polylines, quadratic or
   525  //	 cubic Bezier curves", L. Maisonobe
   526  //
   527  // An electronic version may be found at:
   528  //
   529  //	http://spaceroots.org/documents/ellipse/elliptical-arc.pdf
   530  func ArcTransform(p, f1, f2 f32.Point, angle float32) (transform f32.Affine2D, segments int) {
   531  	const segmentsPerCircle = 16
   532  	const anglePerSegment = 2 * math.Pi / segmentsPerCircle
   533  
   534  	s := angle / anglePerSegment
   535  	if s < 0 {
   536  		s = -s
   537  	}
   538  	segments = int(math.Ceil(float64(s)))
   539  	if segments <= 0 {
   540  		segments = 1
   541  	}
   542  
   543  	var rx, ry, alpha float64
   544  	if f1 == f2 {
   545  		// degenerate case of a circle.
   546  		rx = dist(f1, p)
   547  		ry = rx
   548  	} else {
   549  		// semi-major axis: 2a = |PF1| + |PF2|
   550  		a := 0.5 * (dist(f1, p) + dist(f2, p))
   551  		// semi-minor axis: c^2 = a^2 - b^2 (c: focal distance)
   552  		c := dist(f1, f2) * 0.5
   553  		b := math.Sqrt(a*a - c*c)
   554  		switch {
   555  		case a > b:
   556  			rx = a
   557  			ry = b
   558  		default:
   559  			rx = b
   560  			ry = a
   561  		}
   562  		if f1.X == f2.X {
   563  			// special case of a "vertical" ellipse.
   564  			alpha = math.Pi / 2
   565  			if f1.Y < f2.Y {
   566  				alpha = -alpha
   567  			}
   568  		} else {
   569  			x := float64(f1.X-f2.X) * 0.5
   570  			if x < 0 {
   571  				x = -x
   572  			}
   573  			alpha = math.Acos(x / c)
   574  		}
   575  	}
   576  
   577  	var (
   578  		θ   = angle / float32(segments)
   579  		ref f32.Affine2D // transform from absolute frame to ellipse-based one
   580  		rot f32.Affine2D // rotation matrix for each segment
   581  		inv f32.Affine2D // transform from ellipse-based frame to absolute one
   582  	)
   583  	center := f32.Point{
   584  		X: 0.5 * (f1.X + f2.X),
   585  		Y: 0.5 * (f1.Y + f2.Y),
   586  	}
   587  	ref = ref.Offset(f32.Point{}.Sub(center))
   588  	ref = ref.Rotate(f32.Point{}, float32(-alpha))
   589  	ref = ref.Scale(f32.Point{}, f32.Point{
   590  		X: float32(1 / rx),
   591  		Y: float32(1 / ry),
   592  	})
   593  	inv = ref.Invert()
   594  	rot = rot.Rotate(f32.Point{}, 0.5*θ)
   595  
   596  	// Instead of invoking math.Sincos for every segment, compute a rotation
   597  	// matrix once and apply for each segment.
   598  	// Before applying the rotation matrix rot, transform the coordinates
   599  	// to a frame centered to the ellipse (and warped into a unit circle), then rotate.
   600  	// Finally, transform back into the original frame.
   601  	return inv.Mul(rot).Mul(ref), segments
   602  }
   603  
   604  func dist(p1, p2 f32.Point) float64 {
   605  	var (
   606  		x1 = float64(p1.X)
   607  		y1 = float64(p1.Y)
   608  		x2 = float64(p2.X)
   609  		y2 = float64(p2.Y)
   610  		dx = x2 - x1
   611  		dy = y2 - y1
   612  	)
   613  	return math.Hypot(dx, dy)
   614  }
   615  
   616  func StrokePathCommands(style StrokeStyle, scene []byte) StrokeQuads {
   617  	quads := decodeToStrokeQuads(scene)
   618  	return quads.stroke(style)
   619  }
   620  
   621  // decodeToStrokeQuads decodes scene commands to quads ready to stroke.
   622  func decodeToStrokeQuads(pathData []byte) StrokeQuads {
   623  	quads := make(StrokeQuads, 0, 2*len(pathData)/(scene.CommandSize+4))
   624  	scratch := make([]QuadSegment, 0, 10)
   625  	for len(pathData) >= scene.CommandSize+4 {
   626  		contour := binary.LittleEndian.Uint32(pathData)
   627  		cmd := ops.DecodeCommand(pathData[4:])
   628  		switch cmd.Op() {
   629  		case scene.OpLine:
   630  			var q QuadSegment
   631  			q.From, q.To = scene.DecodeLine(cmd)
   632  			q.Ctrl = q.From.Add(q.To).Mul(.5)
   633  			quad := StrokeQuad{
   634  				Contour: contour,
   635  				Quad:    q,
   636  			}
   637  			quads = append(quads, quad)
   638  		case scene.OpGap:
   639  			// Ignore gaps for strokes.
   640  		case scene.OpQuad:
   641  			var q QuadSegment
   642  			q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd)
   643  			quad := StrokeQuad{
   644  				Contour: contour,
   645  				Quad:    q,
   646  			}
   647  			quads = append(quads, quad)
   648  		case scene.OpCubic:
   649  			from, ctrl0, ctrl1, to := scene.DecodeCubic(cmd)
   650  			scratch = SplitCubic(from, ctrl0, ctrl1, to, scratch[:0])
   651  			for _, q := range scratch {
   652  				quad := StrokeQuad{
   653  					Contour: contour,
   654  					Quad:    q,
   655  				}
   656  				quads = append(quads, quad)
   657  			}
   658  		default:
   659  			panic("unsupported scene command")
   660  		}
   661  		pathData = pathData[scene.CommandSize+4:]
   662  	}
   663  	return quads
   664  }
   665  
   666  func SplitCubic(from, ctrl0, ctrl1, to f32.Point, quads []QuadSegment) []QuadSegment {
   667  	// Set the maximum distance proportionally to the longest side
   668  	// of the bounding rectangle.
   669  	hull := f32.Rectangle{
   670  		Min: from,
   671  		Max: ctrl0,
   672  	}.Canon().Union(f32.Rectangle{
   673  		Min: ctrl1,
   674  		Max: to,
   675  	}.Canon())
   676  	l := hull.Dx()
   677  	if h := hull.Dy(); h > l {
   678  		l = h
   679  	}
   680  	maxDist := l * 0.001
   681  	approxCubeTo(&quads, 0, maxDist*maxDist, from, ctrl0, ctrl1, to)
   682  	return quads
   683  }
   684  
   685  // approxCubeTo approximates a cubic Bézier by a series of quadratic
   686  // curves.
   687  func approxCubeTo(quads *[]QuadSegment, splits int, maxDistSq float32, from, ctrl0, ctrl1, to f32.Point) int {
   688  	// The idea is from
   689  	// https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html
   690  	// where a quadratic approximates a cubic by eliminating its t³ term
   691  	// from its polynomial expression anchored at the starting point:
   692  	//
   693  	// P(t) = pen + 3t(ctrl0 - pen) + 3t²(ctrl1 - 2ctrl0 + pen) + t³(to - 3ctrl1 + 3ctrl0 - pen)
   694  	//
   695  	// The control point for the new quadratic Q1 that shares starting point, pen, with P is
   696  	//
   697  	// C1 = (3ctrl0 - pen)/2
   698  	//
   699  	// The reverse cubic anchored at the end point has the polynomial
   700  	//
   701  	// P'(t) = to + 3t(ctrl1 - to) + 3t²(ctrl0 - 2ctrl1 + to) + t³(pen - 3ctrl0 + 3ctrl1 - to)
   702  	//
   703  	// The corresponding quadratic Q2 that shares the end point, to, with P has control
   704  	// point
   705  	//
   706  	// C2 = (3ctrl1 - to)/2
   707  	//
   708  	// The combined quadratic Bézier, Q, shares both start and end points with its cubic
   709  	// and use the midpoint between the two curves Q1 and Q2 as control point:
   710  	//
   711  	// C = (3ctrl0 - pen + 3ctrl1 - to)/4
   712  	// using, q0 := 3ctrl0 - pen, q1 := 3ctrl1 - to
   713  	// C = (q0 + q1)/4
   714  	q0 := ctrl0.Mul(3).Sub(from)
   715  	q1 := ctrl1.Mul(3).Sub(to)
   716  	c := q0.Add(q1).Mul(1.0 / 4.0)
   717  	const maxSplits = 32
   718  	if splits >= maxSplits {
   719  		*quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to})
   720  		return splits
   721  	}
   722  	// The maximum distance between the cubic P and its approximation Q given t
   723  	// can be shown to be
   724  	//
   725  	// d = sqrt(3)/36 * |to - 3ctrl1 + 3ctrl0 - pen|
   726  	// reusing, q0 := 3ctrl0 - pen, q1 := 3ctrl1 - to
   727  	// d = sqrt(3)/36 * |-q1 + q0|
   728  	//
   729  	// To save a square root, compare d² with the squared tolerance.
   730  	v := q0.Sub(q1)
   731  	d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36)
   732  	if d2 <= maxDistSq {
   733  		*quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to})
   734  		return splits
   735  	}
   736  	// De Casteljau split the curve and approximate the halves.
   737  	t := float32(0.5)
   738  	c0 := from.Add(ctrl0.Sub(from).Mul(t))
   739  	c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t))
   740  	c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t))
   741  	c01 := c0.Add(c1.Sub(c0).Mul(t))
   742  	c12 := c1.Add(c2.Sub(c1).Mul(t))
   743  	c0112 := c01.Add(c12.Sub(c01).Mul(t))
   744  	splits++
   745  	splits = approxCubeTo(quads, splits, maxDistSq, from, c0, c01, c0112)
   746  	splits = approxCubeTo(quads, splits, maxDistSq, c0112, c12, c2, to)
   747  	return splits
   748  }