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

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  // The algorithms to compute dashes have been extracted, adapted from
     4  // (and used as a reference implementation):
     5  //  - github.com/tdewolff/canvas (Licensed under MIT)
     6  
     7  package stroke
     8  
     9  import (
    10  	"math"
    11  	"sort"
    12  
    13  	"github.com/cybriq/giocore/f32"
    14  )
    15  
    16  type DashOp struct {
    17  	Phase  float32
    18  	Dashes []float32
    19  }
    20  
    21  func IsSolidLine(sty DashOp) bool {
    22  	return sty.Phase == 0 && len(sty.Dashes) == 0
    23  }
    24  
    25  func (qs StrokeQuads) dash(sty DashOp) StrokeQuads {
    26  	sty = dashCanonical(sty)
    27  
    28  	switch {
    29  	case len(sty.Dashes) == 0:
    30  		return qs
    31  	case len(sty.Dashes) == 1 && sty.Dashes[0] == 0.0:
    32  		return StrokeQuads{}
    33  	}
    34  
    35  	if len(sty.Dashes)%2 == 1 {
    36  		// If the dash pattern is of uneven length, dash and space lengths
    37  		// alternate. The following duplicates the pattern so that uneven
    38  		// indices are always spaces.
    39  		sty.Dashes = append(sty.Dashes, sty.Dashes...)
    40  	}
    41  
    42  	var (
    43  		i0, pos0 = dashStart(sty)
    44  		out      StrokeQuads
    45  
    46  		contour uint32 = 1
    47  	)
    48  
    49  	for _, ps := range qs.split() {
    50  		var (
    51  			i      = i0
    52  			pos    = pos0
    53  			t      []float64
    54  			length = ps.len()
    55  		)
    56  		for pos+sty.Dashes[i] < length {
    57  			pos += sty.Dashes[i]
    58  			if 0.0 < pos {
    59  				t = append(t, float64(pos))
    60  			}
    61  			i++
    62  			if i == len(sty.Dashes) {
    63  				i = 0
    64  			}
    65  		}
    66  
    67  		j0 := 0
    68  		endsInDash := i%2 == 0
    69  		if len(t)%2 == 1 && endsInDash || len(t)%2 == 0 && !endsInDash {
    70  			j0 = 1
    71  		}
    72  
    73  		var (
    74  			qd StrokeQuads
    75  			pd = ps.splitAt(&contour, t...)
    76  		)
    77  		for j := j0; j < len(pd)-1; j += 2 {
    78  			qd = qd.append(pd[j])
    79  		}
    80  		if endsInDash {
    81  			if ps.closed() {
    82  				qd = pd[len(pd)-1].append(qd)
    83  			} else {
    84  				qd = qd.append(pd[len(pd)-1])
    85  			}
    86  		}
    87  		out = out.append(qd)
    88  		contour++
    89  	}
    90  	return out
    91  }
    92  
    93  func dashCanonical(sty DashOp) DashOp {
    94  	var (
    95  		o  = sty
    96  		ds = o.Dashes
    97  	)
    98  
    99  	if len(sty.Dashes) == 0 {
   100  		return sty
   101  	}
   102  
   103  	// Remove zeros except first and last.
   104  	for i := 1; i < len(ds)-1; i++ {
   105  		if f32Eq(ds[i], 0.0) {
   106  			ds[i-1] += ds[i+1]
   107  			ds = append(ds[:i], ds[i+2:]...)
   108  			i--
   109  		}
   110  	}
   111  
   112  	// Remove first zero, collapse with second and last.
   113  	if f32Eq(ds[0], 0.0) {
   114  		if len(ds) < 3 {
   115  			return DashOp{
   116  				Phase:  0.0,
   117  				Dashes: []float32{0.0},
   118  			}
   119  		}
   120  		o.Phase -= ds[1]
   121  		ds[len(ds)-1] += ds[1]
   122  		ds = ds[2:]
   123  	}
   124  
   125  	// Remove last zero, collapse with fist and second to last.
   126  	if f32Eq(ds[len(ds)-1], 0.0) {
   127  		if len(ds) < 3 {
   128  			return DashOp{}
   129  		}
   130  		o.Phase += ds[len(ds)-2]
   131  		ds[0] += ds[len(ds)-2]
   132  		ds = ds[:len(ds)-2]
   133  	}
   134  
   135  	// If there are zeros or negatives, don't draw dashes.
   136  	for i := 0; i < len(ds); i++ {
   137  		if ds[i] < 0.0 || f32Eq(ds[i], 0.0) {
   138  			return DashOp{
   139  				Phase:  0.0,
   140  				Dashes: []float32{0.0},
   141  			}
   142  		}
   143  	}
   144  
   145  	// Remove repeated patterns.
   146  loop:
   147  	for len(ds)%2 == 0 {
   148  		mid := len(ds) / 2
   149  		for i := 0; i < mid; i++ {
   150  			if !f32Eq(ds[i], ds[mid+i]) {
   151  				break loop
   152  			}
   153  		}
   154  		ds = ds[:mid]
   155  	}
   156  	return o
   157  }
   158  
   159  func dashStart(sty DashOp) (int, float32) {
   160  	i0 := 0 // i0 is the index into dashes.
   161  	for sty.Dashes[i0] <= sty.Phase {
   162  		sty.Phase -= sty.Dashes[i0]
   163  		i0++
   164  		if i0 == len(sty.Dashes) {
   165  			i0 = 0
   166  		}
   167  	}
   168  	// pos0 may be negative if the offset lands halfway into dash.
   169  	pos0 := -sty.Phase
   170  	if sty.Phase < 0.0 {
   171  		var sum float32
   172  		for _, d := range sty.Dashes {
   173  			sum += d
   174  		}
   175  		pos0 = -(sum + sty.Phase) // handle negative offsets
   176  	}
   177  	return i0, pos0
   178  }
   179  
   180  func (qs StrokeQuads) len() float32 {
   181  	var sum float32
   182  	for i := range qs {
   183  		q := qs[i].Quad
   184  		sum += quadBezierLen(q.From, q.Ctrl, q.To)
   185  	}
   186  	return sum
   187  }
   188  
   189  // splitAt splits the path into separate paths at the specified intervals
   190  // along the path.
   191  // splitAt updates the provided contour counter as it splits the segments.
   192  func (qs StrokeQuads) splitAt(contour *uint32, ts ...float64) []StrokeQuads {
   193  	if len(ts) == 0 {
   194  		qs.setContour(*contour)
   195  		return []StrokeQuads{qs}
   196  	}
   197  
   198  	sort.Float64s(ts)
   199  	if ts[0] == 0 {
   200  		ts = ts[1:]
   201  	}
   202  
   203  	var (
   204  		j int     // index into ts
   205  		t float64 // current position along curve
   206  	)
   207  
   208  	var oo []StrokeQuads
   209  	var oi StrokeQuads
   210  	push := func() {
   211  		oo = append(oo, oi)
   212  		oi = nil
   213  	}
   214  
   215  	for _, ps := range qs.split() {
   216  		for _, q := range ps {
   217  			if j == len(ts) {
   218  				oi = append(oi, q)
   219  				continue
   220  			}
   221  			speed := func(t float64) float64 {
   222  				return float64(lenPt(quadBezierD1(q.Quad.From, q.Quad.Ctrl, q.Quad.To, float32(t))))
   223  			}
   224  			invL, dt := invSpeedPolynomialChebyshevApprox(20, gaussLegendre7, speed, 0, 1)
   225  
   226  			var (
   227  				t0 float64
   228  				r0 = q.Quad.From
   229  				r1 = q.Quad.Ctrl
   230  				r2 = q.Quad.To
   231  
   232  				// from keeps track of the start of the 'running' segment.
   233  				from = r0
   234  			)
   235  			for j < len(ts) && t < ts[j] && ts[j] <= t+dt {
   236  				tj := invL(ts[j] - t)
   237  				tsub := (tj - t0) / (1.0 - t0)
   238  				t0 = tj
   239  
   240  				var q1 f32.Point
   241  				_, q1, _, r0, r1, r2 = quadBezierSplit(r0, r1, r2, float32(tsub))
   242  
   243  				oi = append(oi, StrokeQuad{
   244  					Contour: *contour,
   245  					Quad: QuadSegment{
   246  						From: from,
   247  						Ctrl: q1,
   248  						To:   r0,
   249  					},
   250  				})
   251  				push()
   252  				(*contour)++
   253  
   254  				from = r0
   255  				j++
   256  			}
   257  			if !f64Eq(t0, 1) {
   258  				if len(oi) > 0 {
   259  					r0 = oi.pen()
   260  				}
   261  				oi = append(oi, StrokeQuad{
   262  					Contour: *contour,
   263  					Quad: QuadSegment{
   264  						From: r0,
   265  						Ctrl: r1,
   266  						To:   r2,
   267  					},
   268  				})
   269  			}
   270  			t += dt
   271  		}
   272  	}
   273  	if len(oi) > 0 {
   274  		push()
   275  		(*contour)++
   276  	}
   277  
   278  	return oo
   279  }
   280  
   281  func f32Eq(a, b float32) bool {
   282  	const epsilon = 1e-10
   283  	return math.Abs(float64(a-b)) < epsilon
   284  }
   285  
   286  func f64Eq(a, b float64) bool {
   287  	const epsilon = 1e-10
   288  	return math.Abs(a-b) < epsilon
   289  }
   290  
   291  func invSpeedPolynomialChebyshevApprox(N int, gaussLegendre gaussLegendreFunc, fp func(float64) float64, tmin, tmax float64) (func(float64) float64, float64) {
   292  	// The TODOs below are copied verbatim from tdewolff/canvas:
   293  	//
   294  	// TODO: find better way to determine N. For Arc 10 seems fine, for some
   295  	// Quads 10 is too low, for Cube depending on inflection points is
   296  	// maybe not the best indicator
   297  	//
   298  	// TODO: track efficiency, how many times is fp called?
   299  	// Does a look-up table make more sense?
   300  	fLength := func(t float64) float64 {
   301  		return math.Abs(gaussLegendre(fp, tmin, t))
   302  	}
   303  	totalLength := fLength(tmax)
   304  	t := func(L float64) float64 {
   305  		return bisectionMethod(fLength, L, tmin, tmax)
   306  	}
   307  	return polynomialChebyshevApprox(N, t, 0.0, totalLength, tmin, tmax), totalLength
   308  }
   309  
   310  func polynomialChebyshevApprox(N int, f func(float64) float64, xmin, xmax, ymin, ymax float64) func(float64) float64 {
   311  	var (
   312  		invN = 1.0 / float64(N)
   313  		fs   = make([]float64, N)
   314  	)
   315  	for k := 0; k < N; k++ {
   316  		u := math.Cos(math.Pi * (float64(k+1) - 0.5) * invN)
   317  		fs[k] = f(xmin + 0.5*(xmax-xmin)*(u+1))
   318  	}
   319  
   320  	c := make([]float64, N)
   321  	for j := 0; j < N; j++ {
   322  		var a float64
   323  		for k := 0; k < N; k++ {
   324  			a += fs[k] * math.Cos(float64(j)*math.Pi*(float64(k+1)-0.5)/float64(N))
   325  		}
   326  		c[j] = 2 * invN * a
   327  	}
   328  
   329  	if ymax < ymin {
   330  		ymin, ymax = ymax, ymin
   331  	}
   332  	return func(x float64) float64 {
   333  		x = math.Min(xmax, math.Max(xmin, x))
   334  		u := (x-xmin)/(xmax-xmin)*2 - 1
   335  		var a float64
   336  		for j := 0; j < N; j++ {
   337  			a += c[j] * math.Cos(float64(j)*math.Acos(u))
   338  		}
   339  		y := -0.5*c[0] + a
   340  		if !math.IsNaN(ymin) && !math.IsNaN(ymax) {
   341  			y = math.Min(ymax, math.Max(ymin, y))
   342  		}
   343  		return y
   344  	}
   345  }
   346  
   347  // bisectionMethod finds the value x for which f(x) = y in the interval x
   348  // in [xmin, xmax] using the bisection method.
   349  func bisectionMethod(f func(float64) float64, y, xmin, xmax float64) float64 {
   350  	const (
   351  		maxIter   = 100
   352  		tolerance = 0.001 // 0.1%
   353  	)
   354  
   355  	var (
   356  		n    = 0
   357  		x    float64
   358  		tolX = math.Abs(xmax-xmin) * tolerance
   359  		tolY = math.Abs(f(xmax)-f(xmin)) * tolerance
   360  	)
   361  	for {
   362  		x = 0.5 * (xmin + xmax)
   363  		if n >= maxIter {
   364  			return x
   365  		}
   366  
   367  		dy := f(x) - y
   368  		switch {
   369  		case math.Abs(dy) < tolY, math.Abs(0.5*(xmax-xmin)) < tolX:
   370  			return x
   371  		case dy > 0:
   372  			xmax = x
   373  		default:
   374  			xmin = x
   375  		}
   376  		n++
   377  	}
   378  }
   379  
   380  type gaussLegendreFunc func(func(float64) float64, float64, float64) float64
   381  
   382  // Gauss-Legendre quadrature integration from a to b with n=7
   383  func gaussLegendre7(f func(float64) float64, a, b float64) float64 {
   384  	c := 0.5 * (b - a)
   385  	d := 0.5 * (a + b)
   386  	Qd1 := f(-0.949108*c + d)
   387  	Qd2 := f(-0.741531*c + d)
   388  	Qd3 := f(-0.405845*c + d)
   389  	Qd4 := f(d)
   390  	Qd5 := f(0.405845*c + d)
   391  	Qd6 := f(0.741531*c + d)
   392  	Qd7 := f(0.949108*c + d)
   393  	return c * (0.129485*(Qd1+Qd7) + 0.279705*(Qd2+Qd6) + 0.381830*(Qd3+Qd5) + 0.417959*Qd4)
   394  }