github.com/utopiagio/gio@v0.0.8/op/clip/clip.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  package clip
     4  
     5  import (
     6  	"encoding/binary"
     7  	"hash/maphash"
     8  	"image"
     9  	"math"
    10  
    11  	"github.com/utopiagio/gio/f32"
    12  	f32internal "github.com/utopiagio/gio/internal/f32"
    13  	"github.com/utopiagio/gio/internal/ops"
    14  	"github.com/utopiagio/gio/internal/scene"
    15  	"github.com/utopiagio/gio/internal/stroke"
    16  	"github.com/utopiagio/gio/op"
    17  )
    18  
    19  // Op represents a clip area. Op intersects the current clip area with
    20  // itself.
    21  type Op struct {
    22  	path PathSpec
    23  
    24  	outline bool
    25  	width   float32
    26  }
    27  
    28  // Stack represents an Op pushed on the clip stack.
    29  type Stack struct {
    30  	ops     *ops.Ops
    31  	id      ops.StackID
    32  	macroID uint32
    33  }
    34  
    35  var pathSeed maphash.Seed
    36  
    37  func init() {
    38  	pathSeed = maphash.MakeSeed()
    39  }
    40  
    41  // Push saves the current clip state on the stack and updates the current
    42  // state to the intersection of the current p.
    43  func (p Op) Push(o *op.Ops) Stack {
    44  	id, macroID := ops.PushOp(&o.Internal, ops.ClipStack)
    45  	p.add(o)
    46  	return Stack{ops: &o.Internal, id: id, macroID: macroID}
    47  }
    48  
    49  func (p Op) add(o *op.Ops) {
    50  	path := p.path
    51  
    52  	if !path.hasSegments && p.width > 0 {
    53  		switch p.path.shape {
    54  		case ops.Rect:
    55  			b := f32internal.FRect(path.bounds)
    56  			var rect Path
    57  			rect.Begin(o)
    58  			rect.MoveTo(b.Min)
    59  			rect.LineTo(f32.Pt(b.Max.X, b.Min.Y))
    60  			rect.LineTo(b.Max)
    61  			rect.LineTo(f32.Pt(b.Min.X, b.Max.Y))
    62  			rect.Close()
    63  			path = rect.End()
    64  		case ops.Path:
    65  			// Nothing to do.
    66  		default:
    67  			panic("invalid empty path for shape")
    68  		}
    69  	}
    70  	bo := binary.LittleEndian
    71  	if path.hasSegments {
    72  		data := ops.Write(&o.Internal, ops.TypePathLen)
    73  		data[0] = byte(ops.TypePath)
    74  		bo.PutUint64(data[1:], path.hash)
    75  		path.spec.Add(o)
    76  	}
    77  
    78  	bounds := path.bounds
    79  	if p.width > 0 {
    80  		// Expand bounds to cover stroke.
    81  		half := int(p.width*.5 + .5)
    82  		bounds.Min.X -= half
    83  		bounds.Min.Y -= half
    84  		bounds.Max.X += half
    85  		bounds.Max.Y += half
    86  		data := ops.Write(&o.Internal, ops.TypeStrokeLen)
    87  		data[0] = byte(ops.TypeStroke)
    88  		bo := binary.LittleEndian
    89  		bo.PutUint32(data[1:], math.Float32bits(p.width))
    90  	}
    91  
    92  	data := ops.Write(&o.Internal, ops.TypeClipLen)
    93  	data[0] = byte(ops.TypeClip)
    94  	bo.PutUint32(data[1:], uint32(bounds.Min.X))
    95  	bo.PutUint32(data[5:], uint32(bounds.Min.Y))
    96  	bo.PutUint32(data[9:], uint32(bounds.Max.X))
    97  	bo.PutUint32(data[13:], uint32(bounds.Max.Y))
    98  	if p.outline {
    99  		data[17] = byte(1)
   100  	}
   101  	data[18] = byte(path.shape)
   102  }
   103  
   104  func (s Stack) Pop() {
   105  	ops.PopOp(s.ops, ops.ClipStack, s.id, s.macroID)
   106  	data := ops.Write(s.ops, ops.TypePopClipLen)
   107  	data[0] = byte(ops.TypePopClip)
   108  }
   109  
   110  type PathSpec struct {
   111  	spec op.CallOp
   112  	// hasSegments tracks whether there are any segments in the path.
   113  	hasSegments bool
   114  	bounds      image.Rectangle
   115  	shape       ops.Shape
   116  	hash        uint64
   117  }
   118  
   119  // Path constructs a Op clip path described by lines and
   120  // Bézier curves, where drawing outside the Path is discarded.
   121  // The inside-ness of a pixel is determines by the non-zero winding rule,
   122  // similar to the SVG rule of the same name.
   123  //
   124  // Path generates no garbage and can be used for dynamic paths; path
   125  // data is stored directly in the Ops list supplied to Begin.
   126  type Path struct {
   127  	ops         *ops.Ops
   128  	contour     int
   129  	pen         f32.Point
   130  	macro       op.MacroOp
   131  	start       f32.Point
   132  	hasSegments bool
   133  	bounds      f32internal.Rectangle
   134  	hash        maphash.Hash
   135  }
   136  
   137  // Pos returns the current pen position.
   138  func (p *Path) Pos() f32.Point { return p.pen }
   139  
   140  // Begin the path, storing the path data and final Op into ops.
   141  func (p *Path) Begin(o *op.Ops) {
   142  	*p = Path{
   143  		ops:     &o.Internal,
   144  		macro:   op.Record(o),
   145  		contour: 1,
   146  	}
   147  	p.hash.SetSeed(pathSeed)
   148  	ops.BeginMulti(p.ops)
   149  	data := ops.WriteMulti(p.ops, ops.TypeAuxLen)
   150  	data[0] = byte(ops.TypeAux)
   151  }
   152  
   153  // End returns a PathSpec ready to use in clipping operations.
   154  func (p *Path) End() PathSpec {
   155  	p.gap()
   156  	c := p.macro.Stop()
   157  	ops.EndMulti(p.ops)
   158  	return PathSpec{
   159  		spec:        c,
   160  		hasSegments: p.hasSegments,
   161  		bounds:      p.bounds.Round(),
   162  		hash:        p.hash.Sum64(),
   163  	}
   164  }
   165  
   166  // Move moves the pen by the amount specified by delta.
   167  func (p *Path) Move(delta f32.Point) {
   168  	to := delta.Add(p.pen)
   169  	p.MoveTo(to)
   170  }
   171  
   172  // MoveTo moves the pen to the specified absolute coordinate.
   173  func (p *Path) MoveTo(to f32.Point) {
   174  	if p.pen == to {
   175  		return
   176  	}
   177  	p.gap()
   178  	p.end()
   179  	p.pen = to
   180  	p.start = to
   181  }
   182  
   183  func (p *Path) gap() {
   184  	if p.pen != p.start {
   185  		// A closed contour starts and ends in the same point.
   186  		// This move creates a gap in the contour, register it.
   187  		data := ops.WriteMulti(p.ops, scene.CommandSize+4)
   188  		bo := binary.LittleEndian
   189  		bo.PutUint32(data[0:], uint32(p.contour))
   190  		p.cmd(data[4:], scene.Gap(p.pen, p.start))
   191  	}
   192  }
   193  
   194  // end completes the current contour.
   195  func (p *Path) end() {
   196  	p.contour++
   197  }
   198  
   199  // Line moves the pen by the amount specified by delta, recording a line.
   200  func (p *Path) Line(delta f32.Point) {
   201  	to := delta.Add(p.pen)
   202  	p.LineTo(to)
   203  }
   204  
   205  // LineTo moves the pen to the absolute point specified, recording a line.
   206  func (p *Path) LineTo(to f32.Point) {
   207  	if to == p.pen {
   208  		return
   209  	}
   210  	data := ops.WriteMulti(p.ops, scene.CommandSize+4)
   211  	bo := binary.LittleEndian
   212  	bo.PutUint32(data[0:], uint32(p.contour))
   213  	p.cmd(data[4:], scene.Line(p.pen, to))
   214  	p.pen = to
   215  	p.expand(to)
   216  }
   217  
   218  func (p *Path) cmd(data []byte, c scene.Command) {
   219  	ops.EncodeCommand(data, c)
   220  	p.hash.Write(data)
   221  }
   222  
   223  func (p *Path) expand(pt f32.Point) {
   224  	if !p.hasSegments {
   225  		p.hasSegments = true
   226  		p.bounds = f32internal.Rectangle{Min: pt, Max: pt}
   227  	} else {
   228  		b := p.bounds
   229  		if pt.X < b.Min.X {
   230  			b.Min.X = pt.X
   231  		}
   232  		if pt.Y < b.Min.Y {
   233  			b.Min.Y = pt.Y
   234  		}
   235  		if pt.X > b.Max.X {
   236  			b.Max.X = pt.X
   237  		}
   238  		if pt.Y > b.Max.Y {
   239  			b.Max.Y = pt.Y
   240  		}
   241  		p.bounds = b
   242  	}
   243  }
   244  
   245  // Quad records a quadratic Bézier from the pen to end
   246  // with the control point ctrl.
   247  func (p *Path) Quad(ctrl, to f32.Point) {
   248  	ctrl = ctrl.Add(p.pen)
   249  	to = to.Add(p.pen)
   250  	p.QuadTo(ctrl, to)
   251  }
   252  
   253  // QuadTo records a quadratic Bézier from the pen to end
   254  // with the control point ctrl, with absolute coordinates.
   255  func (p *Path) QuadTo(ctrl, to f32.Point) {
   256  	if ctrl == p.pen && to == p.pen {
   257  		return
   258  	}
   259  	data := ops.WriteMulti(p.ops, scene.CommandSize+4)
   260  	bo := binary.LittleEndian
   261  	bo.PutUint32(data[0:], uint32(p.contour))
   262  	p.cmd(data[4:], scene.Quad(p.pen, ctrl, to))
   263  	p.pen = to
   264  	p.expand(ctrl)
   265  	p.expand(to)
   266  }
   267  
   268  // ArcTo adds an elliptical arc to the path. The implied ellipse is defined
   269  // by its focus points f1 and f2.
   270  // The arc starts in the current point and ends angle radians along the ellipse boundary.
   271  // The sign of angle determines the direction; positive being counter-clockwise,
   272  // negative clockwise.
   273  func (p *Path) ArcTo(f1, f2 f32.Point, angle float32) {
   274  	m, segments := stroke.ArcTransform(p.pen, f1, f2, angle)
   275  	for i := 0; i < segments; i++ {
   276  		p0 := p.pen
   277  		p1 := m.Transform(p0)
   278  		p2 := m.Transform(p1)
   279  		ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5))
   280  		p.QuadTo(ctl, p2)
   281  	}
   282  }
   283  
   284  // Arc is like ArcTo where f1 and f2 are relative to the current position.
   285  func (p *Path) Arc(f1, f2 f32.Point, angle float32) {
   286  	f1 = f1.Add(p.pen)
   287  	f2 = f2.Add(p.pen)
   288  	p.ArcTo(f1, f2, angle)
   289  }
   290  
   291  // Cube records a cubic Bézier from the pen through
   292  // two control points ending in to.
   293  func (p *Path) Cube(ctrl0, ctrl1, to f32.Point) {
   294  	p.CubeTo(p.pen.Add(ctrl0), p.pen.Add(ctrl1), p.pen.Add(to))
   295  }
   296  
   297  // CubeTo records a cubic Bézier from the pen through
   298  // two control points ending in to, with absolute coordinates.
   299  func (p *Path) CubeTo(ctrl0, ctrl1, to f32.Point) {
   300  	if ctrl0 == p.pen && ctrl1 == p.pen && to == p.pen {
   301  		return
   302  	}
   303  	data := ops.WriteMulti(p.ops, scene.CommandSize+4)
   304  	bo := binary.LittleEndian
   305  	bo.PutUint32(data[0:], uint32(p.contour))
   306  	p.cmd(data[4:], scene.Cubic(p.pen, ctrl0, ctrl1, to))
   307  	p.pen = to
   308  	p.expand(ctrl0)
   309  	p.expand(ctrl1)
   310  	p.expand(to)
   311  }
   312  
   313  // Close closes the path contour.
   314  func (p *Path) Close() {
   315  	if p.pen != p.start {
   316  		p.LineTo(p.start)
   317  	}
   318  	p.end()
   319  }
   320  
   321  // Stroke represents a stroked path.
   322  type Stroke struct {
   323  	Path PathSpec
   324  	// Width of the stroked path.
   325  	Width float32
   326  }
   327  
   328  // Op returns a clip operation representing the stroke.
   329  func (s Stroke) Op() Op {
   330  	return Op{
   331  		path:  s.Path,
   332  		width: s.Width,
   333  	}
   334  }
   335  
   336  // Outline represents the area inside of a path, according to the
   337  // non-zero winding rule.
   338  type Outline struct {
   339  	Path PathSpec
   340  }
   341  
   342  // Op returns a clip operation representing the outline.
   343  func (o Outline) Op() Op {
   344  	return Op{
   345  		path:    o.Path,
   346  		outline: true,
   347  	}
   348  }