clip.go raw

   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/p9c/p9/pkg/gel/gio/f32"
  11  	"github.com/p9c/p9/pkg/gel/gio/internal/opconst"
  12  	"github.com/p9c/p9/pkg/gel/gio/internal/ops"
  13  	"github.com/p9c/p9/pkg/gel/gio/internal/scene"
  14  	"github.com/p9c/p9/pkg/gel/gio/internal/stroke"
  15  	"github.com/p9c/p9/pkg/gel/gio/op"
  16  )
  17  
  18  // Op represents a clip area. Op intersects the current clip area with
  19  // itself.
  20  type Op struct {
  21  	bounds image.Rectangle
  22  	path   PathSpec
  23  
  24  	outline bool
  25  	stroke  StrokeStyle
  26  	dashes  DashSpec
  27  }
  28  
  29  func (p Op) Add(o *op.Ops) {
  30  	str := p.stroke
  31  	dashes := p.dashes
  32  	path := p.path
  33  	outline := p.outline
  34  	approx := str.Width > 0 && !(dashes == DashSpec{} && str.Miter == 0 && str.Join == RoundJoin && str.Cap == RoundCap)
  35  	if approx {
  36  		// If the stroke is not natively supported by the compute renderer, construct a filled path
  37  		// that approximates it.
  38  		path = p.approximateStroke(o)
  39  		dashes = DashSpec{}
  40  		str = StrokeStyle{}
  41  		outline = true
  42  	}
  43  
  44  	if path.hasSegments {
  45  		data := o.Write(opconst.TypePathLen)
  46  		data[0] = byte(opconst.TypePath)
  47  		path.spec.Add(o)
  48  	}
  49  
  50  	if str.Width > 0 {
  51  		data := o.Write(opconst.TypeStrokeLen)
  52  		data[0] = byte(opconst.TypeStroke)
  53  		bo := binary.LittleEndian
  54  		bo.PutUint32(data[1:], math.Float32bits(str.Width))
  55  	}
  56  
  57  	data := o.Write(opconst.TypeClipLen)
  58  	data[0] = byte(opconst.TypeClip)
  59  	bo := binary.LittleEndian
  60  	bo.PutUint32(data[1:], uint32(p.bounds.Min.X))
  61  	bo.PutUint32(data[5:], uint32(p.bounds.Min.Y))
  62  	bo.PutUint32(data[9:], uint32(p.bounds.Max.X))
  63  	bo.PutUint32(data[13:], uint32(p.bounds.Max.Y))
  64  	if outline {
  65  		data[17] = byte(1)
  66  	}
  67  }
  68  
  69  func (p Op) approximateStroke(o *op.Ops) PathSpec {
  70  	if !p.path.hasSegments {
  71  		return PathSpec{}
  72  	}
  73  
  74  	var r ops.Reader
  75  	// Add path op for us to decode. Use a macro to omit it from later decodes.
  76  	ignore := op.Record(o)
  77  	r.ResetAt(o, ops.NewPC(o))
  78  	p.path.spec.Add(o)
  79  	ignore.Stop()
  80  	encOp, ok := r.Decode()
  81  	if !ok || opconst.OpType(encOp.Data[0]) != opconst.TypeAux {
  82  		panic("corrupt path data")
  83  	}
  84  	pathData := encOp.Data[opconst.TypeAuxLen:]
  85  
  86  	// Decode dashes in a similar way.
  87  	var dashes stroke.DashOp
  88  	if p.dashes.phase != 0 || p.dashes.size > 0 {
  89  		ignore := op.Record(o)
  90  		r.ResetAt(o, ops.NewPC(o))
  91  		p.dashes.spec.Add(o)
  92  		ignore.Stop()
  93  		encOp, ok := r.Decode()
  94  		if !ok || opconst.OpType(encOp.Data[0]) != opconst.TypeAux {
  95  			panic("corrupt dash data")
  96  		}
  97  		dashes.Dashes = make([]float32, p.dashes.size)
  98  		dashData := encOp.Data[opconst.TypeAuxLen:]
  99  		bo := binary.LittleEndian
 100  		for i := range dashes.Dashes {
 101  			dashes.Dashes[i] = math.Float32frombits(bo.Uint32(dashData[i*4:]))
 102  		}
 103  		dashes.Phase = p.dashes.phase
 104  	}
 105  
 106  	// Approximate and output path data.
 107  	var outline Path
 108  	outline.Begin(o)
 109  	ss := stroke.StrokeStyle{
 110  		Width: p.stroke.Width,
 111  		Miter: p.stroke.Miter,
 112  		Cap:   stroke.StrokeCap(p.stroke.Cap),
 113  		Join:  stroke.StrokeJoin(p.stroke.Join),
 114  	}
 115  	quads := stroke.StrokePathCommands(ss, dashes, pathData)
 116  	pen := f32.Pt(0, 0)
 117  	for _, quad := range quads {
 118  		q := quad.Quad
 119  		if q.From != pen {
 120  			pen = q.From
 121  			outline.MoveTo(pen)
 122  		}
 123  		outline.contour = int(quad.Contour)
 124  		outline.QuadTo(q.Ctrl, q.To)
 125  	}
 126  	return outline.End()
 127  }
 128  
 129  type PathSpec struct {
 130  	spec op.CallOp
 131  	// open is true if any path contour is not closed. A closed contour starts
 132  	// and ends in the same point.
 133  	open bool
 134  	// hasSegments tracks whether there are any segments in the path.
 135  	hasSegments bool
 136  }
 137  
 138  // Path constructs a Op clip path described by lines and
 139  // Bézier curves, where drawing outside the Path is discarded.
 140  // The inside-ness of a pixel is determines by the non-zero winding rule,
 141  // similar to the SVG rule of the same name.
 142  //
 143  // Path generates no garbage and can be used for dynamic paths; path
 144  // data is stored directly in the Ops list supplied to Begin.
 145  type Path struct {
 146  	ops         *op.Ops
 147  	open        bool
 148  	contour     int
 149  	pen         f32.Point
 150  	macro       op.MacroOp
 151  	start       f32.Point
 152  	hasSegments bool
 153  }
 154  
 155  // Pos returns the current pen position.
 156  func (p *Path) Pos() f32.Point { return p.pen }
 157  
 158  // Begin the path, storing the path data and final Op into ops.
 159  func (p *Path) Begin(ops *op.Ops) {
 160  	p.ops = ops
 161  	p.macro = op.Record(ops)
 162  	// Write the TypeAux opcode
 163  	data := ops.Write(opconst.TypeAuxLen)
 164  	data[0] = byte(opconst.TypeAux)
 165  }
 166  
 167  // End returns a PathSpec ready to use in clipping operations.
 168  func (p *Path) End() PathSpec {
 169  	c := p.macro.Stop()
 170  	return PathSpec{
 171  		spec:        c,
 172  		open:        p.open || p.pen != p.start,
 173  		hasSegments: p.hasSegments,
 174  	}
 175  }
 176  
 177  // Move moves the pen by the amount specified by delta.
 178  func (p *Path) Move(delta f32.Point) {
 179  	to := delta.Add(p.pen)
 180  	p.MoveTo(to)
 181  }
 182  
 183  // MoveTo moves the pen to the specified absolute coordinate.
 184  func (p *Path) MoveTo(to f32.Point) {
 185  	p.open = p.open || p.pen != p.start
 186  	p.end()
 187  	p.pen = to
 188  	p.start = to
 189  }
 190  
 191  // end completes the current contour.
 192  func (p *Path) end() {
 193  	p.contour++
 194  }
 195  
 196  // Line moves the pen by the amount specified by delta, recording a line.
 197  func (p *Path) Line(delta f32.Point) {
 198  	to := delta.Add(p.pen)
 199  	p.LineTo(to)
 200  }
 201  
 202  // LineTo moves the pen to the absolute point specified, recording a line.
 203  func (p *Path) LineTo(to f32.Point) {
 204  	data := p.ops.Write(scene.CommandSize + 4)
 205  	bo := binary.LittleEndian
 206  	bo.PutUint32(data[0:], uint32(p.contour))
 207  	ops.EncodeCommand(data[4:], scene.Line(p.pen, to))
 208  	p.pen = to
 209  	p.hasSegments = true
 210  }
 211  
 212  // Quad records a quadratic Bézier from the pen to end
 213  // with the control point ctrl.
 214  func (p *Path) Quad(ctrl, to f32.Point) {
 215  	ctrl = ctrl.Add(p.pen)
 216  	to = to.Add(p.pen)
 217  	p.QuadTo(ctrl, to)
 218  }
 219  
 220  // QuadTo records a quadratic Bézier from the pen to end
 221  // with the control point ctrl, with absolute coordinates.
 222  func (p *Path) QuadTo(ctrl, to f32.Point) {
 223  	data := p.ops.Write(scene.CommandSize + 4)
 224  	bo := binary.LittleEndian
 225  	bo.PutUint32(data[0:], uint32(p.contour))
 226  	ops.EncodeCommand(data[4:], scene.Quad(p.pen, ctrl, to))
 227  	p.pen = to
 228  	p.hasSegments = true
 229  }
 230  
 231  // Arc adds an elliptical arc to the path. The implied ellipse is defined
 232  // by its focus points f1 and f2.
 233  // The arc starts in the current point and ends angle radians along the ellipse boundary.
 234  // The sign of angle determines the direction; positive being counter-clockwise,
 235  // negative clockwise.
 236  func (p *Path) Arc(f1, f2 f32.Point, angle float32) {
 237  	f1 = f1.Add(p.pen)
 238  	f2 = f2.Add(p.pen)
 239  	const segments = 16
 240  	m := stroke.ArcTransform(p.pen, f1, f2, angle, segments)
 241  
 242  	for i := 0; i < segments; i++ {
 243  		p0 := p.pen
 244  		p1 := m.Transform(p0)
 245  		p2 := m.Transform(p1)
 246  		ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5))
 247  		p.QuadTo(ctl, p2)
 248  	}
 249  }
 250  
 251  // Cube records a cubic Bézier from the pen through
 252  // two control points ending in to.
 253  func (p *Path) Cube(ctrl0, ctrl1, to f32.Point) {
 254  	p.CubeTo(p.pen.Add(ctrl0), p.pen.Add(ctrl1), p.pen.Add(to))
 255  }
 256  
 257  // CubeTo records a cubic Bézier from the pen through
 258  // two control points ending in to, with absolute coordinates.
 259  func (p *Path) CubeTo(ctrl0, ctrl1, to f32.Point) {
 260  	if ctrl0 == p.pen && ctrl1 == p.pen && to == p.pen {
 261  		return
 262  	}
 263  	data := p.ops.Write(scene.CommandSize + 4)
 264  	bo := binary.LittleEndian
 265  	bo.PutUint32(data[0:], uint32(p.contour))
 266  	ops.EncodeCommand(data[4:], scene.Cubic(p.pen, ctrl0, ctrl1, to))
 267  	p.pen = to
 268  	p.hasSegments = true
 269  }
 270  
 271  // Close closes the path contour.
 272  func (p *Path) Close() {
 273  	if p.pen != p.start {
 274  		p.LineTo(p.start)
 275  	}
 276  	p.end()
 277  }
 278  
 279  // Outline represents the area inside of a path, according to the
 280  // non-zero winding rule.
 281  type Outline struct {
 282  	Path PathSpec
 283  }
 284  
 285  // Op returns a clip operation representing the outline.
 286  func (o Outline) Op() Op {
 287  	if o.Path.open {
 288  		panic("not all path contours are closed")
 289  	}
 290  	return Op{
 291  		path:    o.Path,
 292  		outline: true,
 293  	}
 294  }
 295