github.com/gop9/olt@v0.0.0-20200202132135-d956aad50b08/gio/op/clip/clip.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  package clip
     4  
     5  import (
     6  	"encoding/binary"
     7  	"image"
     8  	"math"
     9  
    10  	"github.com/gop9/olt/gio/f32"
    11  	"github.com/gop9/olt/gio/internal/opconst"
    12  	"github.com/gop9/olt/gio/internal/path"
    13  	"github.com/gop9/olt/gio/op"
    14  )
    15  
    16  // Path constructs a Op clip path described by lines and
    17  // Bézier curves, where drawing outside the Path is discarded.
    18  // The inside-ness of a pixel is determines by the even-odd rule,
    19  // similar to the SVG rule of the same name.
    20  //
    21  // Path generates no garbage and can be used for dynamic paths; path
    22  // data is stored directly in the Ops list supplied to Begin.
    23  type Path struct {
    24  	ops       *op.Ops
    25  	contour   int
    26  	pen       f32.Point
    27  	bounds    f32.Rectangle
    28  	hasBounds bool
    29  	macro     op.MacroOp
    30  }
    31  
    32  // Op sets the current clip to the intersection of
    33  // the existing clip with this clip.
    34  //
    35  // If you need to reset the clip to its previous values after
    36  // applying a Op, use op.StackOp.
    37  type Op struct {
    38  	macro  op.MacroOp
    39  	bounds f32.Rectangle
    40  }
    41  
    42  func (p Op) Add(o *op.Ops) {
    43  	p.macro.Add()
    44  	data := o.Write(opconst.TypeClipLen)
    45  	data[0] = byte(opconst.TypeClip)
    46  	bo := binary.LittleEndian
    47  	bo.PutUint32(data[1:], math.Float32bits(p.bounds.Min.X))
    48  	bo.PutUint32(data[5:], math.Float32bits(p.bounds.Min.Y))
    49  	bo.PutUint32(data[9:], math.Float32bits(p.bounds.Max.X))
    50  	bo.PutUint32(data[13:], math.Float32bits(p.bounds.Max.Y))
    51  }
    52  
    53  // Begin the path, storing the path data and final Op into ops.
    54  func (p *Path) Begin(ops *op.Ops) {
    55  	p.ops = ops
    56  	p.macro.Record(ops)
    57  	// Write the TypeAux opcode and a byte for marking whether the
    58  	// path has had its MaxY filled out. If not, the gpu will fill it
    59  	// before using it.
    60  	data := ops.Write(2)
    61  	data[0] = byte(opconst.TypeAux)
    62  }
    63  
    64  // MoveTo moves the pen to the given position.
    65  func (p *Path) Move(to f32.Point) {
    66  	p.end()
    67  	to = to.Add(p.pen)
    68  	p.pen = to
    69  }
    70  
    71  // end completes the current contour.
    72  func (p *Path) end() {
    73  	p.contour++
    74  }
    75  
    76  // Line moves the pen by the amount specified by delta, recording a line.
    77  func (p *Path) Line(delta f32.Point) {
    78  	to := delta.Add(p.pen)
    79  	p.lineTo(to)
    80  }
    81  
    82  func (p *Path) lineTo(to f32.Point) {
    83  	// Model lines as degenerate quadratic Béziers.
    84  	p.quadTo(to.Add(p.pen).Mul(.5), to)
    85  }
    86  
    87  // Quad records a quadratic Bézier from the pen to end
    88  // with the control point ctrl.
    89  func (p *Path) Quad(ctrl, to f32.Point) {
    90  	ctrl = ctrl.Add(p.pen)
    91  	to = to.Add(p.pen)
    92  	p.quadTo(ctrl, to)
    93  }
    94  
    95  func (p *Path) quadTo(ctrl, to f32.Point) {
    96  	// Zero width curves don't contribute to stenciling.
    97  	if p.pen.X == to.X && p.pen.X == ctrl.X {
    98  		p.pen = to
    99  		return
   100  	}
   101  
   102  	bounds := f32.Rectangle{
   103  		Min: p.pen,
   104  		Max: to,
   105  	}.Canon()
   106  
   107  	// If the curve contain areas where a vertical line
   108  	// intersects it twice, split the curve in two x monotone
   109  	// lower and upper curves. The stencil fragment program
   110  	// expects only one intersection per curve.
   111  
   112  	// Find the t where the derivative in x is 0.
   113  	v0 := ctrl.Sub(p.pen)
   114  	v1 := to.Sub(ctrl)
   115  	d := v0.X - v1.X
   116  	// t = v0 / d. Split if t is in ]0;1[.
   117  	if v0.X > 0 && d > v0.X || v0.X < 0 && d < v0.X {
   118  		t := v0.X / d
   119  		ctrl0 := p.pen.Mul(1 - t).Add(ctrl.Mul(t))
   120  		ctrl1 := ctrl.Mul(1 - t).Add(to.Mul(t))
   121  		mid := ctrl0.Mul(1 - t).Add(ctrl1.Mul(t))
   122  		p.simpleQuadTo(ctrl0, mid)
   123  		p.simpleQuadTo(ctrl1, to)
   124  		if mid.X > bounds.Max.X {
   125  			bounds.Max.X = mid.X
   126  		}
   127  		if mid.X < bounds.Min.X {
   128  			bounds.Min.X = mid.X
   129  		}
   130  	} else {
   131  		p.simpleQuadTo(ctrl, to)
   132  	}
   133  	// Find the y extremum, if any.
   134  	d = v0.Y - v1.Y
   135  	if v0.Y > 0 && d > v0.Y || v0.Y < 0 && d < v0.Y {
   136  		t := v0.Y / d
   137  		y := (1-t)*(1-t)*p.pen.Y + 2*(1-t)*t*ctrl.Y + t*t*to.Y
   138  		if y > bounds.Max.Y {
   139  			bounds.Max.Y = y
   140  		}
   141  		if y < bounds.Min.Y {
   142  			bounds.Min.Y = y
   143  		}
   144  	}
   145  	p.expand(bounds)
   146  }
   147  
   148  // Cube records a cubic Bézier from the pen through
   149  // two control points ending in to.
   150  func (p *Path) Cube(ctrl0, ctrl1, to f32.Point) {
   151  	ctrl0 = ctrl0.Add(p.pen)
   152  	ctrl1 = ctrl1.Add(p.pen)
   153  	to = to.Add(p.pen)
   154  	// Set the maximum distance proportionally to the longest side
   155  	// of the bounding rectangle.
   156  	hull := f32.Rectangle{
   157  		Min: p.pen,
   158  		Max: ctrl0,
   159  	}.Canon().Add(ctrl1).Add(to)
   160  	l := hull.Dx()
   161  	if h := hull.Dy(); h > l {
   162  		l = h
   163  	}
   164  	p.approxCubeTo(0, l*0.001, ctrl0, ctrl1, to)
   165  }
   166  
   167  // approxCube approximates a cubic Bézier by a series of quadratic
   168  // curves.
   169  func (p *Path) approxCubeTo(splits int, maxDist float32, ctrl0, ctrl1, to f32.Point) int {
   170  	// The idea is from
   171  	// https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html
   172  	// where a quadratic approximates a cubic by eliminating its t³ term
   173  	// from its polynomial expression anchored at the starting point:
   174  	//
   175  	// P(t) = pen + 3t(ctrl0 - pen) + 3t²(ctrl1 - 2ctrl0 + pen) + t³(to - 3ctrl1 + 3ctrl0 - pen)
   176  	//
   177  	// The control point for the new quadratic Q1 that shares starting point, pen, with P is
   178  	//
   179  	// C1 = (3ctrl0 - pen)/2
   180  	//
   181  	// The reverse cubic anchored at the end point has the polynomial
   182  	//
   183  	// P'(t) = to + 3t(ctrl1 - to) + 3t²(ctrl0 - 2ctrl1 + to) + t³(pen - 3ctrl0 + 3ctrl1 - to)
   184  	//
   185  	// The corresponding quadratic Q2 that shares the end point, to, with P has control
   186  	// point
   187  	//
   188  	// C2 = (3ctrl1 - to)/2
   189  	//
   190  	// The combined quadratic Bézier, Q, shares both start and end points with its cubic
   191  	// and use the midpoint between the two curves Q1 and Q2 as control point:
   192  	//
   193  	// C = (3ctrl0 - pen + 3ctrl1 - to)/4
   194  	c := ctrl0.Mul(3).Sub(p.pen).Add(ctrl1.Mul(3)).Sub(to).Mul(1.0 / 4.0)
   195  	const maxSplits = 32
   196  	if splits >= maxSplits {
   197  		p.quadTo(c, to)
   198  		return splits
   199  	}
   200  	// The maximum distance between the cubic P and its approximation Q given t
   201  	// can be shown to be
   202  	//
   203  	// d = sqrt(3)/36*|to - 3ctrl1 + 3ctrl0 - pen|
   204  	//
   205  	// To save a square root, compare d² with the squared tolerance.
   206  	v := to.Sub(ctrl1.Mul(3)).Add(ctrl0.Mul(3)).Sub(p.pen)
   207  	d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36)
   208  	if d2 <= maxDist*maxDist {
   209  		p.quadTo(c, to)
   210  		return splits
   211  	}
   212  	// De Casteljau split the curve and approximate the halves.
   213  	t := float32(0.5)
   214  	c0 := p.pen.Add(ctrl0.Sub(p.pen).Mul(t))
   215  	c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t))
   216  	c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t))
   217  	c01 := c0.Add(c1.Sub(c0).Mul(t))
   218  	c12 := c1.Add(c2.Sub(c1).Mul(t))
   219  	c0112 := c01.Add(c12.Sub(c01).Mul(t))
   220  	splits++
   221  	splits = p.approxCubeTo(splits, maxDist, c0, c01, c0112)
   222  	splits = p.approxCubeTo(splits, maxDist, c12, c2, to)
   223  	return splits
   224  }
   225  
   226  func (p *Path) expand(b f32.Rectangle) {
   227  	if !p.hasBounds {
   228  		p.hasBounds = true
   229  		inf := float32(math.Inf(+1))
   230  		p.bounds = f32.Rectangle{
   231  			Min: f32.Point{X: inf, Y: inf},
   232  			Max: f32.Point{X: -inf, Y: -inf},
   233  		}
   234  	}
   235  	p.bounds = p.bounds.Union(b)
   236  }
   237  
   238  func (p *Path) vertex(cornerx, cornery int16, ctrl, to f32.Point) {
   239  	v := path.Vertex{
   240  		CornerX: cornerx,
   241  		CornerY: cornery,
   242  		FromX:   p.pen.X,
   243  		FromY:   p.pen.Y,
   244  		CtrlX:   ctrl.X,
   245  		CtrlY:   ctrl.Y,
   246  		ToX:     to.X,
   247  		ToY:     to.Y,
   248  	}
   249  	data := p.ops.Write(path.VertStride)
   250  	bo := binary.LittleEndian
   251  	data[0] = byte(uint16(v.CornerX))
   252  	data[1] = byte(uint16(v.CornerX) >> 8)
   253  	data[2] = byte(uint16(v.CornerY))
   254  	data[3] = byte(uint16(v.CornerY) >> 8)
   255  	// Put the contour index in MaxY.
   256  	bo.PutUint32(data[4:], uint32(p.contour))
   257  	bo.PutUint32(data[8:], math.Float32bits(v.FromX))
   258  	bo.PutUint32(data[12:], math.Float32bits(v.FromY))
   259  	bo.PutUint32(data[16:], math.Float32bits(v.CtrlX))
   260  	bo.PutUint32(data[20:], math.Float32bits(v.CtrlY))
   261  	bo.PutUint32(data[24:], math.Float32bits(v.ToX))
   262  	bo.PutUint32(data[28:], math.Float32bits(v.ToY))
   263  }
   264  
   265  func (p *Path) simpleQuadTo(ctrl, to f32.Point) {
   266  	// NW.
   267  	p.vertex(-1, 1, ctrl, to)
   268  	// NE.
   269  	p.vertex(1, 1, ctrl, to)
   270  	// SW.
   271  	p.vertex(-1, -1, ctrl, to)
   272  	// SE.
   273  	p.vertex(1, -1, ctrl, to)
   274  	p.pen = to
   275  }
   276  
   277  // End the path and return a clip operation that represents it.
   278  func (p *Path) End() Op {
   279  	p.end()
   280  	p.macro.Stop()
   281  	return Op{
   282  		macro:  p.macro,
   283  		bounds: p.bounds,
   284  	}
   285  }
   286  
   287  // Rect represents the clip area of a rectangle with rounded
   288  // corners.The origin is in the upper left
   289  // corner.
   290  // Specify a square with corner radii equal to half the square size to
   291  // construct a circular clip area.
   292  type Rect struct {
   293  	Rect f32.Rectangle
   294  	// The corner radii.
   295  	SE, SW, NW, NE float32
   296  }
   297  
   298  // Op returns the Op for the rectangle.
   299  func (rr Rect) Op(ops *op.Ops) Op {
   300  	r := rr.Rect
   301  	// Optimize for the common pixel aligned rectangle with no
   302  	// corner rounding.
   303  	if rr.SE == 0 && rr.SW == 0 && rr.NW == 0 && rr.NE == 0 {
   304  		ri := image.Rectangle{
   305  			Min: image.Point{X: int(r.Min.X), Y: int(r.Min.Y)},
   306  			Max: image.Point{X: int(r.Max.X), Y: int(r.Max.Y)},
   307  		}
   308  		// Optimize pixel-aligned rectangles to just its bounds.
   309  		if r == toRectF(ri) {
   310  			return Op{bounds: r}
   311  		}
   312  	}
   313  	return roundRect(ops, r, rr.SE, rr.SW, rr.NW, rr.NE)
   314  }
   315  
   316  // roundRect returns the clip area of a rectangle with rounded
   317  // corners defined by their radii.
   318  func roundRect(ops *op.Ops, r f32.Rectangle, se, sw, nw, ne float32) Op {
   319  	size := r.Size()
   320  	// https://pomax.github.io/bezierinfo/#circles_cubic.
   321  	w, h := float32(size.X), float32(size.Y)
   322  	const c = 0.55228475 // 4*(sqrt(2)-1)/3
   323  	var p Path
   324  	p.Begin(ops)
   325  	p.Move(r.Min)
   326  	p.Move(f32.Point{X: w, Y: h - se})
   327  	p.Cube(f32.Point{X: 0, Y: se * c}, f32.Point{X: -se + se*c, Y: se}, f32.Point{X: -se, Y: se}) // SE
   328  	p.Line(f32.Point{X: sw - w + se, Y: 0})
   329  	p.Cube(f32.Point{X: -sw * c, Y: 0}, f32.Point{X: -sw, Y: -sw + sw*c}, f32.Point{X: -sw, Y: -sw}) // SW
   330  	p.Line(f32.Point{X: 0, Y: nw - h + sw})
   331  	p.Cube(f32.Point{X: 0, Y: -nw * c}, f32.Point{X: nw - nw*c, Y: -nw}, f32.Point{X: nw, Y: -nw}) // NW
   332  	p.Line(f32.Point{X: w - ne - nw, Y: 0})
   333  	p.Cube(f32.Point{X: ne * c, Y: 0}, f32.Point{X: ne, Y: ne - ne*c}, f32.Point{X: ne, Y: ne}) // NE
   334  	return p.End()
   335  }
   336  
   337  func toRectF(r image.Rectangle) f32.Rectangle {
   338  	return f32.Rectangle{
   339  		Min: f32.Point{X: float32(r.Min.X), Y: float32(r.Min.Y)},
   340  		Max: f32.Point{X: float32(r.Max.X), Y: float32(r.Max.Y)},
   341  	}
   342  }