gioui.org/ui@v0.0.0-20190926171558-ce74bc0cbaea/paint/path.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  package paint
     4  
     5  import (
     6  	"encoding/binary"
     7  	"math"
     8  	"unsafe"
     9  
    10  	"gioui.org/ui"
    11  	"gioui.org/ui/f32"
    12  	"gioui.org/ui/internal/opconst"
    13  	"gioui.org/ui/internal/path"
    14  )
    15  
    16  // PathBuilder builds and adds a general ClipOp clip path
    17  // from lines and curves.
    18  // PathBuilder generates no garbage and can be used for
    19  // dynamic paths; path data is stored directly in the Ops
    20  // list supplied to Init.
    21  type PathBuilder struct {
    22  	ops       *ui.Ops
    23  	firstVert int
    24  	nverts    int
    25  	maxy      float32
    26  	pen       f32.Point
    27  	bounds    f32.Rectangle
    28  	hasBounds bool
    29  }
    30  
    31  // ClipOp sets the current clip path.
    32  type ClipOp struct {
    33  	bounds f32.Rectangle
    34  }
    35  
    36  func (p ClipOp) Add(o *ui.Ops) {
    37  	data := make([]byte, opconst.TypeClipLen)
    38  	data[0] = byte(opconst.TypeClip)
    39  	bo := binary.LittleEndian
    40  	bo.PutUint32(data[1:], math.Float32bits(p.bounds.Min.X))
    41  	bo.PutUint32(data[5:], math.Float32bits(p.bounds.Min.Y))
    42  	bo.PutUint32(data[9:], math.Float32bits(p.bounds.Max.X))
    43  	bo.PutUint32(data[13:], math.Float32bits(p.bounds.Max.Y))
    44  	o.Write(data)
    45  }
    46  
    47  // Init the builder and specify the operations list for
    48  // storing the path data and final ClipOp.
    49  func (p *PathBuilder) Init(ops *ui.Ops) {
    50  	p.ops = ops
    51  }
    52  
    53  // MoveTo moves the pen to the given position.
    54  func (p *PathBuilder) Move(to f32.Point) {
    55  	p.end()
    56  	to = to.Add(p.pen)
    57  	p.maxy = to.Y
    58  	p.pen = to
    59  }
    60  
    61  // end completes the current contour.
    62  func (p *PathBuilder) end() {
    63  	aux := p.ops.Aux()
    64  	bo := binary.LittleEndian
    65  	// Fill in maximal Y coordinates of the NW and NE corners.
    66  	for i := p.firstVert; i < p.nverts; i++ {
    67  		off := path.VertStride*i + int(unsafe.Offsetof(((*path.Vertex)(nil)).MaxY))
    68  		bo.PutUint32(aux[off:], math.Float32bits(p.maxy))
    69  	}
    70  	p.firstVert = p.nverts
    71  }
    72  
    73  // Line records a line from the pen to end.
    74  func (p *PathBuilder) Line(to f32.Point) {
    75  	to = to.Add(p.pen)
    76  	p.lineTo(to)
    77  }
    78  
    79  func (p *PathBuilder) lineTo(to f32.Point) {
    80  	// Model lines as degenerate quadratic Béziers.
    81  	p.quadTo(to.Add(p.pen).Mul(.5), to)
    82  }
    83  
    84  // Quad records a quadratic Bézier from the pen to end
    85  // with the control point ctrl.
    86  func (p *PathBuilder) Quad(ctrl, to f32.Point) {
    87  	ctrl = ctrl.Add(p.pen)
    88  	to = to.Add(p.pen)
    89  	p.quadTo(ctrl, to)
    90  }
    91  
    92  func (p *PathBuilder) quadTo(ctrl, to f32.Point) {
    93  	// Zero width curves don't contribute to stenciling.
    94  	if p.pen.X == to.X && p.pen.X == ctrl.X {
    95  		p.pen = to
    96  		return
    97  	}
    98  
    99  	bounds := f32.Rectangle{
   100  		Min: p.pen,
   101  		Max: to,
   102  	}.Canon()
   103  
   104  	// If the curve contain areas where a vertical line
   105  	// intersects it twice, split the curve in two x monotone
   106  	// lower and upper curves. The stencil fragment program
   107  	// expects only one intersection per curve.
   108  
   109  	// Find the t where the derivative in x is 0.
   110  	v0 := ctrl.Sub(p.pen)
   111  	v1 := to.Sub(ctrl)
   112  	d := v0.X - v1.X
   113  	// t = v0 / d. Split if t is in ]0;1[.
   114  	if v0.X > 0 && d > v0.X || v0.X < 0 && d < v0.X {
   115  		t := v0.X / d
   116  		ctrl0 := p.pen.Mul(1 - t).Add(ctrl.Mul(t))
   117  		ctrl1 := ctrl.Mul(1 - t).Add(to.Mul(t))
   118  		mid := ctrl0.Mul(1 - t).Add(ctrl1.Mul(t))
   119  		p.simpleQuadTo(ctrl0, mid)
   120  		p.simpleQuadTo(ctrl1, to)
   121  		if mid.X > bounds.Max.X {
   122  			bounds.Max.X = mid.X
   123  		}
   124  		if mid.X < bounds.Min.X {
   125  			bounds.Min.X = mid.X
   126  		}
   127  	} else {
   128  		p.simpleQuadTo(ctrl, to)
   129  	}
   130  	// Find the y extremum, if any.
   131  	d = v0.Y - v1.Y
   132  	if v0.Y > 0 && d > v0.Y || v0.Y < 0 && d < v0.Y {
   133  		t := v0.Y / d
   134  		y := (1-t)*(1-t)*p.pen.Y + 2*(1-t)*t*ctrl.Y + t*t*to.Y
   135  		if y > bounds.Max.Y {
   136  			bounds.Max.Y = y
   137  		}
   138  		if y < bounds.Min.Y {
   139  			bounds.Min.Y = y
   140  		}
   141  	}
   142  	p.expand(bounds)
   143  }
   144  
   145  // Cube records a cubic Bézier from the pen through
   146  // two control points ending in to.
   147  func (p *PathBuilder) Cube(ctrl0, ctrl1, to f32.Point) {
   148  	ctrl0 = ctrl0.Add(p.pen)
   149  	ctrl1 = ctrl1.Add(p.pen)
   150  	to = to.Add(p.pen)
   151  	// Set the maximum distance proportionally to the longest side
   152  	// of the bounding rectangle.
   153  	hull := f32.Rectangle{
   154  		Min: p.pen,
   155  		Max: ctrl0,
   156  	}.Canon().Add(ctrl1).Add(to)
   157  	l := hull.Dx()
   158  	if h := hull.Dy(); h > l {
   159  		l = h
   160  	}
   161  	p.approxCubeTo(0, l*0.001, ctrl0, ctrl1, to)
   162  }
   163  
   164  // approxCube approximates a cubic Bézier by a series of quadratic
   165  // curves.
   166  func (p *PathBuilder) approxCubeTo(splits int, maxDist float32, ctrl0, ctrl1, to f32.Point) int {
   167  	// The idea is from
   168  	// https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html
   169  	// where a quadratic approximates a cubic by eliminating its t³ term
   170  	// from its polynomial expression anchored at the starting point:
   171  	//
   172  	// P(t) = pen + 3t(ctrl0 - pen) + 3t²(ctrl1 - 2ctrl0 + pen) + t³(to - 3ctrl1 + 3ctrl0 - pen)
   173  	//
   174  	// The control point for the new quadratic Q1 that shares starting point, pen, with P is
   175  	//
   176  	// C1 = (3ctrl0 - pen)/2
   177  	//
   178  	// The reverse cubic anchored at the end point has the polynomial
   179  	//
   180  	// P'(t) = to + 3t(ctrl1 - to) + 3t²(ctrl0 - 2ctrl1 + to) + t³(pen - 3ctrl0 + 3ctrl1 - to)
   181  	//
   182  	// The corresponding quadratic Q2 that shares the end point, to, with P has control
   183  	// point
   184  	//
   185  	// C2 = (3ctrl1 - to)/2
   186  	//
   187  	// The combined quadratic Bézier, Q, shares both start and end points with its cubic
   188  	// and use the midpoint between the two curves Q1 and Q2 as control point:
   189  	//
   190  	// C = (3ctrl0 - pen + 3ctrl1 - to)/4
   191  	c := ctrl0.Mul(3).Sub(p.pen).Add(ctrl1.Mul(3)).Sub(to).Mul(1.0 / 4.0)
   192  	const maxSplits = 32
   193  	if splits >= maxSplits {
   194  		p.quadTo(c, to)
   195  		return splits
   196  	}
   197  	// The maximum distance between the cubic P and its approximation Q given t
   198  	// can be shown to be
   199  	//
   200  	// d = sqrt(3)/36*|to - 3ctrl1 + 3ctrl0 - pen|
   201  	//
   202  	// To save a square root, compare d² with the squared tolerance.
   203  	v := to.Sub(ctrl1.Mul(3)).Add(ctrl0.Mul(3)).Sub(p.pen)
   204  	d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36)
   205  	if d2 <= maxDist*maxDist {
   206  		p.quadTo(c, to)
   207  		return splits
   208  	}
   209  	// De Casteljau split the curve and approximate the halves.
   210  	t := float32(0.5)
   211  	c0 := p.pen.Add(ctrl0.Sub(p.pen).Mul(t))
   212  	c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t))
   213  	c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t))
   214  	c01 := c0.Add(c1.Sub(c0).Mul(t))
   215  	c12 := c1.Add(c2.Sub(c1).Mul(t))
   216  	c0112 := c01.Add(c12.Sub(c01).Mul(t))
   217  	splits++
   218  	splits = p.approxCubeTo(splits, maxDist, c0, c01, c0112)
   219  	splits = p.approxCubeTo(splits, maxDist, c12, c2, to)
   220  	return splits
   221  }
   222  
   223  func (p *PathBuilder) expand(b f32.Rectangle) {
   224  	if !p.hasBounds {
   225  		p.hasBounds = true
   226  		inf := float32(math.Inf(+1))
   227  		p.bounds = f32.Rectangle{
   228  			Min: f32.Point{X: inf, Y: inf},
   229  			Max: f32.Point{X: -inf, Y: -inf},
   230  		}
   231  	}
   232  	p.bounds = p.bounds.Union(b)
   233  }
   234  
   235  func (p *PathBuilder) vertex(cornerx, cornery int16, ctrl, to f32.Point) {
   236  	p.nverts++
   237  	v := path.Vertex{
   238  		CornerX: cornerx,
   239  		CornerY: cornery,
   240  		FromX:   p.pen.X,
   241  		FromY:   p.pen.Y,
   242  		CtrlX:   ctrl.X,
   243  		CtrlY:   ctrl.Y,
   244  		ToX:     to.X,
   245  		ToY:     to.Y,
   246  	}
   247  	data := make([]byte, path.VertStride+1)
   248  	data[0] = byte(opconst.TypeAux)
   249  	bo := binary.LittleEndian
   250  	data[1] = byte(uint16(v.CornerX))
   251  	data[2] = byte(uint16(v.CornerX) >> 8)
   252  	data[3] = byte(uint16(v.CornerY))
   253  	data[4] = byte(uint16(v.CornerY) >> 8)
   254  	bo.PutUint32(data[5:], math.Float32bits(v.MaxY))
   255  	bo.PutUint32(data[9:], math.Float32bits(v.FromX))
   256  	bo.PutUint32(data[13:], math.Float32bits(v.FromY))
   257  	bo.PutUint32(data[17:], math.Float32bits(v.CtrlX))
   258  	bo.PutUint32(data[21:], math.Float32bits(v.CtrlY))
   259  	bo.PutUint32(data[25:], math.Float32bits(v.ToX))
   260  	bo.PutUint32(data[29:], math.Float32bits(v.ToY))
   261  	p.ops.Write(data)
   262  }
   263  
   264  func (p *PathBuilder) simpleQuadTo(ctrl, to f32.Point) {
   265  	if p.pen.Y > p.maxy {
   266  		p.maxy = p.pen.Y
   267  	}
   268  	if ctrl.Y > p.maxy {
   269  		p.maxy = ctrl.Y
   270  	}
   271  	if to.Y > p.maxy {
   272  		p.maxy = to.Y
   273  	}
   274  	// NW.
   275  	p.vertex(-1, 1, ctrl, to)
   276  	// NE.
   277  	p.vertex(1, 1, ctrl, to)
   278  	// SW.
   279  	p.vertex(-1, -1, ctrl, to)
   280  	// SE.
   281  	p.vertex(1, -1, ctrl, to)
   282  	p.pen = to
   283  }
   284  
   285  // End the path and add the resulting ClipOp to
   286  // the operation list passed to Init.
   287  func (p *PathBuilder) End() {
   288  	p.end()
   289  	ClipOp{
   290  		bounds: p.bounds,
   291  	}.Add(p.ops)
   292  }