op.mx raw

   1  // SPDX-License-Identifier: Unlicense OR MIT
   2  
   3  // Public op API for Gio. Merges op.go, clip/*.go, and paint/*.go into
   4  // package gio alongside the internal ops (ops.mx).
   5  //
   6  // Name mapping from the original multi-package layout:
   7  //   op.Ops           -> OpList       (wraps internal Ops via .Internal field)
   8  //   op.MacroOp       -> MacroOp
   9  //   op.CallOp        -> CallOp
  10  //   op.TransformOp   -> TransformOp
  11  //   op.TransformStack-> TransformStack
  12  //   op.InvalidateCmd -> InvalidateCmd
  13  //   clip.Op          -> ClipOp
  14  //   clip.Stack       -> ClipStack
  15  //   clip.PathSpec    -> PathSpec
  16  //   clip.Path        -> ClipPath
  17  //   clip.Stroke      -> ClipStroke
  18  //   clip.Outline     -> ClipOutline
  19  //   clip.Rect        -> ClipRect
  20  //   clip.RRect       -> ClipRRect
  21  //   clip.Ellipse     -> ClipEllipse
  22  //   paint.ImageFilter      -> ImageFilter
  23  //   paint.ImageOp          -> PaintImageOp
  24  //   paint.ColorOp          -> PaintColorOp
  25  //   paint.LinearGradientOp -> PaintLinearGradientOp
  26  //   paint.PaintOp          -> PaintPaintOp
  27  //   paint.OpacityStack     -> PaintOpacityStack
  28  
  29  package gio
  30  
  31  import (
  32  	"encoding/binary"
  33  	"hash"
  34  	"hash/fnv"
  35  	"image"
  36  	"image/color"
  37  	"image/draw"
  38  	"math"
  39  	"time"
  40  )
  41  
  42  // ---------------------------------------------------------------------------
  43  // op.go - OpList and friends
  44  // ---------------------------------------------------------------------------
  45  
  46  // OpList is the public ops list (was op.Ops). It wraps the internal Ops.
  47  type OpList struct {
  48  	Internal Ops
  49  }
  50  
  51  // MacroOp records a list of operations for later use.
  52  type MacroOp struct {
  53  	ops *Ops
  54  	id  StackID
  55  	pc  PC
  56  }
  57  
  58  // CallOp invokes the operations recorded by Record.
  59  type CallOp struct {
  60  	ops   *Ops
  61  	start PC
  62  	end   PC
  63  }
  64  
  65  // InvalidateCmd requests a redraw at the given time. Use
  66  // the zero value to request an immediate redraw.
  67  type InvalidateCmd struct {
  68  	At time.Time
  69  }
  70  
  71  func (InvalidateCmd) ImplementsCommand() {}
  72  
  73  // TransformOp represents a transformation that can be pushed on the
  74  // transformation stack.
  75  type TransformOp struct {
  76  	t Affine2D
  77  }
  78  
  79  // TransformStack represents a TransformOp pushed on the transformation stack.
  80  type TransformStack struct {
  81  	id      StackID
  82  	macroID uint32
  83  	ops     *Ops
  84  }
  85  
  86  // Defer executes c after all other operations have completed, including
  87  // previously deferred operations.
  88  func Defer(o *OpList, c CallOp) {
  89  	if c.ops == nil {
  90  		return
  91  	}
  92  	state := SaveOps(&o.Internal)
  93  	m := Record(o)
  94  	state.Load()
  95  	c.Add(o)
  96  	c = m.Stop()
  97  	data := WriteOps(&o.Internal, TypeDeferLen)
  98  	data[0] = byte(TypeDefer)
  99  	c.Add(o)
 100  }
 101  
 102  // Reset the OpList, preparing it for re-use.
 103  func (o *OpList) Reset() {
 104  	ResetOps(&o.Internal)
 105  }
 106  
 107  // Record a macro of operations.
 108  func Record(o *OpList) MacroOp {
 109  	m := MacroOp{
 110  		ops: &o.Internal,
 111  		id:  PushMacro(&o.Internal),
 112  		pc:  PCFor(&o.Internal),
 113  	}
 114  	data := WriteOps(m.ops, TypeMacroLen)
 115  	data[0] = byte(TypeMacro)
 116  	return m
 117  }
 118  
 119  // Stop ends a previously started recording and returns a CallOp for replaying it.
 120  func (m MacroOp) Stop() CallOp {
 121  	PopMacro(m.ops, m.id)
 122  	FillMacro(m.ops, m.pc)
 123  	return CallOp{
 124  		ops:   m.ops,
 125  		start: m.pc.Add(TypeMacro),
 126  		end:   PCFor(m.ops),
 127  	}
 128  }
 129  
 130  // Add the recorded list of operations.
 131  func (c CallOp) Add(o *OpList) {
 132  	if c.ops == nil {
 133  		return
 134  	}
 135  	AddCall(&o.Internal, c.ops, c.start, c.end)
 136  }
 137  
 138  // Offset converts an offset to a TransformOp.
 139  func Offset(off image.Point) TransformOp {
 140  	offf := Pt(float32(off.X), float32(off.Y))
 141  	return AffineTransform(AffineId().Offset(offf))
 142  }
 143  
 144  // AffineTransform creates a TransformOp representing the transformation a.
 145  func AffineTransform(a Affine2D) TransformOp {
 146  	return TransformOp{t: a}
 147  }
 148  
 149  // Push the current transformation to the stack and then multiply the
 150  // current transformation with t.
 151  func (t TransformOp) Push(o *OpList) TransformStack {
 152  	id, macroID := PushOp(&o.Internal, TransStack)
 153  	t.add(o, true)
 154  	return TransformStack{ops: &o.Internal, id: id, macroID: macroID}
 155  }
 156  
 157  // Add is like Push except it doesn't push the current transformation to the
 158  // stack.
 159  func (t TransformOp) Add(o *OpList) {
 160  	t.add(o, false)
 161  }
 162  
 163  func (t TransformOp) add(o *OpList, push bool) {
 164  	data := WriteOps(&o.Internal, TypeTransformLen)
 165  	data[0] = byte(TypeTransform)
 166  	if push {
 167  		data[1] = 1
 168  	}
 169  	bo := binary.LittleEndian()
 170  	a, b, c, d, e, f := t.t.Elems()
 171  	bo.PutUint32(data[2:], math.Float32bits(a))
 172  	bo.PutUint32(data[2+4*1:], math.Float32bits(b))
 173  	bo.PutUint32(data[2+4*2:], math.Float32bits(c))
 174  	bo.PutUint32(data[2+4*3:], math.Float32bits(d))
 175  	bo.PutUint32(data[2+4*4:], math.Float32bits(e))
 176  	bo.PutUint32(data[2+4*5:], math.Float32bits(f))
 177  }
 178  
 179  func (t TransformStack) Pop() {
 180  	PopOp(t.ops, TransStack, t.id, t.macroID)
 181  	data := WriteOps(t.ops, TypePopTransformLen)
 182  	data[0] = byte(TypePopTransform)
 183  }
 184  
 185  // ---------------------------------------------------------------------------
 186  // clip/clip.go - ClipOp, ClipStack, PathSpec, ClipPath
 187  // ---------------------------------------------------------------------------
 188  
 189  // ClipOp represents a clip area. ClipOp intersects the current clip area
 190  // with itself. (was clip.Op)
 191  type ClipOp struct {
 192  	path    PathSpec
 193  	outline bool
 194  	width   float32
 195  }
 196  
 197  // ClipStack represents a ClipOp pushed on the clip stack. (was clip.Stack)
 198  type ClipStack struct {
 199  	ops     *Ops
 200  	id      StackID
 201  	macroID uint32
 202  }
 203  
 204  // PathSpec describes a completed path for use in clip operations.
 205  type PathSpec struct {
 206  	spec        CallOp
 207  	hasSegments bool
 208  	bounds      image.Rectangle
 209  	shape       Shape
 210  	hash        uint64
 211  }
 212  
 213  // ClipPath constructs a clip path described by lines and Bezier curves.
 214  // (was clip.Path)
 215  type ClipPath struct {
 216  	ops         *Ops
 217  	contour     int32
 218  	pen         Point
 219  	macro       MacroOp
 220  	start       Point
 221  	hasSegments bool
 222  	bounds      Rectangle
 223  	hash        hash.Hash64
 224  }
 225  
 226  // Push saves the current clip state on the stack and updates the current
 227  // state to the intersection of the current p.
 228  func (p ClipOp) Push(o *OpList) ClipStack {
 229  	id, macroID := PushOp(&o.Internal, ClipStackKind)
 230  	p.add(o)
 231  	return ClipStack{ops: &o.Internal, id: id, macroID: macroID}
 232  }
 233  
 234  func (p ClipOp) add(o *OpList) {
 235  	path := p.path
 236  
 237  	if !path.hasSegments && p.width > 0 {
 238  		switch p.path.shape {
 239  		case ShapeRect:
 240  			b := FRect(path.bounds)
 241  			var rect ClipPath
 242  			rect.Begin(o)
 243  			rect.MoveTo(b.Min)
 244  			rect.LineTo(Pt(b.Max.X, b.Min.Y))
 245  			rect.LineTo(b.Max)
 246  			rect.LineTo(Pt(b.Min.X, b.Max.Y))
 247  			rect.Close()
 248  			path = rect.End()
 249  		case ShapePath:
 250  			// Nothing to do.
 251  		default:
 252  			panic("invalid empty path for shape")
 253  		}
 254  	}
 255  	bo := binary.LittleEndian()
 256  	if path.hasSegments {
 257  		data := WriteOps(&o.Internal, TypePathLen)
 258  		data[0] = byte(TypePath)
 259  		bo.PutUint64(data[1:], path.hash)
 260  		path.spec.Add(o)
 261  	}
 262  
 263  	bounds := path.bounds
 264  	if p.width > 0 {
 265  		half := int(p.width*.5 + .5)
 266  		bounds.Min.X -= half
 267  		bounds.Min.Y -= half
 268  		bounds.Max.X += half
 269  		bounds.Max.Y += half
 270  		data := WriteOps(&o.Internal, TypeStrokeLen)
 271  		data[0] = byte(TypeStroke)
 272  		bo.PutUint32(data[1:], math.Float32bits(p.width))
 273  	}
 274  
 275  	data := WriteOps(&o.Internal, TypeClipLen)
 276  	data[0] = byte(TypeClip)
 277  	bo.PutUint32(data[1:], uint32(bounds.Min.X))
 278  	bo.PutUint32(data[5:], uint32(bounds.Min.Y))
 279  	bo.PutUint32(data[9:], uint32(bounds.Max.X))
 280  	bo.PutUint32(data[13:], uint32(bounds.Max.Y))
 281  	if p.outline {
 282  		data[17] = byte(1)
 283  	}
 284  	data[18] = byte(path.shape)
 285  }
 286  
 287  func (s ClipStack) Pop() {
 288  	PopOp(s.ops, ClipStackKind, s.id, s.macroID)
 289  	data := WriteOps(s.ops, TypePopClipLen)
 290  	data[0] = byte(TypePopClip)
 291  }
 292  
 293  // Pos returns the current pen position.
 294  func (p *ClipPath) Pos() Point { return p.pen }
 295  
 296  // Begin the path, storing the path data and final op into o.
 297  func (p *ClipPath) Begin(o *OpList) {
 298  	h := fnv.New64a()
 299  	*p = ClipPath{
 300  		ops:     &o.Internal,
 301  		macro:   Record(o),
 302  		contour: 1,
 303  		hash:    h,
 304  	}
 305  	BeginMulti(p.ops)
 306  	data := WriteMulti(p.ops, TypeAuxLen)
 307  	data[0] = byte(TypeAux)
 308  }
 309  
 310  // End returns a PathSpec ready to use in clipping operations.
 311  func (p *ClipPath) End() PathSpec {
 312  	p.gap()
 313  	c := p.macro.Stop()
 314  	EndMulti(p.ops)
 315  	return PathSpec{
 316  		spec:        c,
 317  		hasSegments: p.hasSegments,
 318  		bounds:      p.bounds.Round(),
 319  		hash:        p.hash.Sum64(),
 320  	}
 321  }
 322  
 323  // Move moves the pen by delta.
 324  func (p *ClipPath) Move(delta Point) {
 325  	to := delta.Add(p.pen)
 326  	p.MoveTo(to)
 327  }
 328  
 329  // MoveTo moves the pen to the specified absolute coordinate.
 330  func (p *ClipPath) MoveTo(to Point) {
 331  	if p.pen == to {
 332  		return
 333  	}
 334  	p.gap()
 335  	p.end()
 336  	p.pen = to
 337  	p.start = to
 338  }
 339  
 340  func (p *ClipPath) gap() {
 341  	if p.pen != p.start {
 342  		data := WriteMulti(p.ops, CommandSize+4)
 343  		bo := binary.LittleEndian()
 344  		bo.PutUint32(data[0:], uint32(p.contour))
 345  		p.cmd(data[4:], Gap(p.pen, p.start))
 346  	}
 347  }
 348  
 349  func (p *ClipPath) end() {
 350  	p.contour++
 351  }
 352  
 353  // Line moves the pen by delta, recording a line.
 354  func (p *ClipPath) Line(delta Point) {
 355  	to := delta.Add(p.pen)
 356  	p.LineTo(to)
 357  }
 358  
 359  // LineTo moves the pen to 'to', recording a line.
 360  func (p *ClipPath) LineTo(to Point) {
 361  	if to == p.pen {
 362  		return
 363  	}
 364  	data := WriteMulti(p.ops, CommandSize+4)
 365  	bo := binary.LittleEndian()
 366  	bo.PutUint32(data[0:], uint32(p.contour))
 367  	p.cmd(data[4:], Line(p.pen, to))
 368  	p.expand(p.pen)
 369  	p.expand(to)
 370  	p.pen = to
 371  }
 372  
 373  func (p *ClipPath) cmd(data []byte, c Command) {
 374  	EncodeCommand(data, c)
 375  	p.hash.Write(data)
 376  }
 377  
 378  func (p *ClipPath) expand(pt Point) {
 379  	if !p.hasSegments {
 380  		p.hasSegments = true
 381  		p.bounds = Rectangle{Min: pt, Max: pt}
 382  	} else {
 383  		b := p.bounds
 384  		if pt.X < b.Min.X {
 385  			b.Min.X = pt.X
 386  		}
 387  		if pt.Y < b.Min.Y {
 388  			b.Min.Y = pt.Y
 389  		}
 390  		if pt.X > b.Max.X {
 391  			b.Max.X = pt.X
 392  		}
 393  		if pt.Y > b.Max.Y {
 394  			b.Max.Y = pt.Y
 395  		}
 396  		p.bounds = b
 397  	}
 398  }
 399  
 400  // Quad records a quadratic Bezier from the pen to 'to'
 401  // with the control point ctrl (relative).
 402  func (p *ClipPath) Quad(ctrl, to Point) {
 403  	ctrl = ctrl.Add(p.pen)
 404  	to = to.Add(p.pen)
 405  	p.QuadTo(ctrl, to)
 406  }
 407  
 408  // QuadTo records a quadratic Bezier from the pen to 'to'
 409  // with the control point ctrl, with absolute coordinates.
 410  func (p *ClipPath) QuadTo(ctrl, to Point) {
 411  	if ctrl == p.pen && to == p.pen {
 412  		return
 413  	}
 414  	data := WriteMulti(p.ops, CommandSize+4)
 415  	bo := binary.LittleEndian()
 416  	bo.PutUint32(data[0:], uint32(p.contour))
 417  	p.cmd(data[4:], Quad(p.pen, ctrl, to))
 418  	p.expand(p.pen)
 419  	p.expand(ctrl)
 420  	p.expand(to)
 421  	p.pen = to
 422  }
 423  
 424  // ArcTo adds an elliptical arc to the path.
 425  func (p *ClipPath) ArcTo(f1, f2 Point, angle float32) {
 426  	m, segments := ArcTransform(p.pen, f1, f2, angle)
 427  	for i := 0; i < segments; i++ {
 428  		p0 := p.pen
 429  		p1 := m.Transform(p0)
 430  		p2 := m.Transform(p1)
 431  		ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5))
 432  		p.QuadTo(ctl, p2)
 433  	}
 434  }
 435  
 436  // Arc is like ArcTo where f1 and f2 are relative to the current position.
 437  func (p *ClipPath) Arc(f1, f2 Point, angle float32) {
 438  	f1 = f1.Add(p.pen)
 439  	f2 = f2.Add(p.pen)
 440  	p.ArcTo(f1, f2, angle)
 441  }
 442  
 443  // Cube records a cubic Bezier from the pen through
 444  // two control points ending in 'to' (relative).
 445  func (p *ClipPath) Cube(ctrl0, ctrl1, to Point) {
 446  	p.CubeTo(p.pen.Add(ctrl0), p.pen.Add(ctrl1), p.pen.Add(to))
 447  }
 448  
 449  // CubeTo records a cubic Bezier from the pen through
 450  // two control points ending in 'to', with absolute coordinates.
 451  func (p *ClipPath) CubeTo(ctrl0, ctrl1, to Point) {
 452  	if ctrl0 == p.pen && ctrl1 == p.pen && to == p.pen {
 453  		return
 454  	}
 455  	data := WriteMulti(p.ops, CommandSize+4)
 456  	bo := binary.LittleEndian()
 457  	bo.PutUint32(data[0:], uint32(p.contour))
 458  	p.cmd(data[4:], Cubic(p.pen, ctrl0, ctrl1, to))
 459  	p.expand(p.pen)
 460  	p.expand(ctrl0)
 461  	p.expand(ctrl1)
 462  	p.expand(to)
 463  	p.pen = to
 464  }
 465  
 466  // Close closes the path contour.
 467  func (p *ClipPath) Close() {
 468  	if p.pen != p.start {
 469  		p.LineTo(p.start)
 470  	}
 471  	p.end()
 472  }
 473  
 474  // ---------------------------------------------------------------------------
 475  // clip/clip.go - ClipStroke, ClipOutline
 476  // ---------------------------------------------------------------------------
 477  
 478  // ClipStroke represents a stroked path. (was clip.Stroke)
 479  type ClipStroke struct {
 480  	Path  PathSpec
 481  	Width float32
 482  }
 483  
 484  // Op returns a ClipOp representing the stroke.
 485  func (s ClipStroke) Op() ClipOp {
 486  	return ClipOp{
 487  		path:  s.Path,
 488  		width: s.Width,
 489  	}
 490  }
 491  
 492  // ClipOutline represents the area inside of a path, according to the
 493  // non-zero winding rule. (was clip.Outline)
 494  type ClipOutline struct {
 495  	Path PathSpec
 496  }
 497  
 498  // Op returns a ClipOp representing the outline.
 499  func (ol ClipOutline) Op() ClipOp {
 500  	return ClipOp{
 501  		path:    ol.Path,
 502  		outline: true,
 503  	}
 504  }
 505  
 506  // ---------------------------------------------------------------------------
 507  // clip/shapes.go - ClipRect, ClipRRect, ClipEllipse
 508  // ---------------------------------------------------------------------------
 509  
 510  // ClipRect represents the clip area of a pixel-aligned rectangle.
 511  type ClipRect image.Rectangle
 512  
 513  // Op returns the ClipOp for the rectangle.
 514  func (r ClipRect) Op() ClipOp {
 515  	return ClipOp{
 516  		outline: true,
 517  		path:    r.Path(),
 518  	}
 519  }
 520  
 521  // Push the clip operation on the clip stack.
 522  func (r ClipRect) Push(o *OpList) ClipStack {
 523  	return r.Op().Push(o)
 524  }
 525  
 526  // Path returns the PathSpec for the rectangle.
 527  func (r ClipRect) Path() PathSpec {
 528  	return PathSpec{
 529  		shape:  ShapeRect,
 530  		bounds: image.Rectangle(r),
 531  	}
 532  }
 533  
 534  // UniformClipRRect returns a ClipRRect with all corner radii set to radius.
 535  func UniformClipRRect(rect image.Rectangle, radius int) ClipRRect {
 536  	return ClipRRect{
 537  		Rect: rect,
 538  		SE:   radius,
 539  		SW:   radius,
 540  		NE:   radius,
 541  		NW:   radius,
 542  	}
 543  }
 544  
 545  // ClipRRect represents the clip area of a rectangle with rounded corners.
 546  type ClipRRect struct {
 547  	Rect         image.Rectangle
 548  	SE, SW, NW, NE int
 549  }
 550  
 551  // Op returns the ClipOp for the rounded rectangle.
 552  func (rr ClipRRect) Op(o *OpList) ClipOp {
 553  	if rr.SE == 0 && rr.SW == 0 && rr.NW == 0 && rr.NE == 0 {
 554  		return ClipRect(rr.Rect).Op()
 555  	}
 556  	return ClipOutline{Path: rr.Path(o)}.Op()
 557  }
 558  
 559  // Push the rectangle clip on the clip stack.
 560  func (rr ClipRRect) Push(o *OpList) ClipStack {
 561  	return rr.Op(o).Push(o)
 562  }
 563  
 564  // Path returns the PathSpec for the rounded rectangle.
 565  func (rr ClipRRect) Path(o *OpList) PathSpec {
 566  	var p ClipPath
 567  	p.Begin(o)
 568  
 569  	const q = 4 * (math.Sqrt2 - 1) / 3
 570  	const iq = 1 - q
 571  
 572  	se, sw, nw, ne := float32(rr.SE), float32(rr.SW), float32(rr.NW), float32(rr.NE)
 573  	rrf := FRect(rr.Rect)
 574  	w, n, e, s := rrf.Min.X, rrf.Min.Y, rrf.Max.X, rrf.Max.Y
 575  
 576  	p.MoveTo(Point{X: w + nw, Y: n})
 577  	p.LineTo(Point{X: e - ne, Y: n})
 578  	p.CubeTo(
 579  		Point{X: e - ne*iq, Y: n},
 580  		Point{X: e, Y: n + ne*iq},
 581  		Point{X: e, Y: n + ne})
 582  	p.LineTo(Point{X: e, Y: s - se})
 583  	p.CubeTo(
 584  		Point{X: e, Y: s - se*iq},
 585  		Point{X: e - se*iq, Y: s},
 586  		Point{X: e - se, Y: s})
 587  	p.LineTo(Point{X: w + sw, Y: s})
 588  	p.CubeTo(
 589  		Point{X: w + sw*iq, Y: s},
 590  		Point{X: w, Y: s - sw*iq},
 591  		Point{X: w, Y: s - sw})
 592  	p.LineTo(Point{X: w, Y: n + nw})
 593  	p.CubeTo(
 594  		Point{X: w, Y: n + nw*iq},
 595  		Point{X: w + nw*iq, Y: n},
 596  		Point{X: w + nw, Y: n})
 597  
 598  	return p.End()
 599  }
 600  
 601  // ClipEllipse represents the largest axis-aligned ellipse contained in its bounds.
 602  type ClipEllipse image.Rectangle
 603  
 604  // Op returns the ClipOp for the filled ellipse.
 605  func (el ClipEllipse) Op(o *OpList) ClipOp {
 606  	return ClipOutline{Path: el.Path(o)}.Op()
 607  }
 608  
 609  // Push the filled ellipse clip op on the clip stack.
 610  func (el ClipEllipse) Push(o *OpList) ClipStack {
 611  	return el.Op(o).Push(o)
 612  }
 613  
 614  // Path constructs a path for the ellipse.
 615  func (el ClipEllipse) Path(o *OpList) PathSpec {
 616  	bounds := image.Rectangle(el)
 617  	if bounds.Dx() == 0 || bounds.Dy() == 0 {
 618  		return PathSpec{shape: ShapeRect}
 619  	}
 620  
 621  	var p ClipPath
 622  	p.Begin(o)
 623  
 624  	bf := FRect(bounds)
 625  	center := bf.Max.Add(bf.Min).Mul(.5)
 626  	diam := bf.Dx()
 627  	r := diam * .5
 628  	scale := bf.Dy() / diam
 629  
 630  	const q = 4 * (math.Sqrt2 - 1) / 3
 631  
 632  	curve := r * q
 633  	top := Point{X: center.X, Y: center.Y - r*scale}
 634  
 635  	p.MoveTo(top)
 636  	p.CubeTo(
 637  		Point{X: center.X + curve, Y: center.Y - r*scale},
 638  		Point{X: center.X + r, Y: center.Y - curve*scale},
 639  		Point{X: center.X + r, Y: center.Y},
 640  	)
 641  	p.CubeTo(
 642  		Point{X: center.X + r, Y: center.Y + curve*scale},
 643  		Point{X: center.X + curve, Y: center.Y + r*scale},
 644  		Point{X: center.X, Y: center.Y + r*scale},
 645  	)
 646  	p.CubeTo(
 647  		Point{X: center.X - curve, Y: center.Y + r*scale},
 648  		Point{X: center.X - r, Y: center.Y + curve*scale},
 649  		Point{X: center.X - r, Y: center.Y},
 650  	)
 651  	p.CubeTo(
 652  		Point{X: center.X - r, Y: center.Y - curve*scale},
 653  		Point{X: center.X - curve, Y: center.Y - r*scale},
 654  		top,
 655  	)
 656  	ellipse := p.End()
 657  	ellipse.shape = ShapeEllipse
 658  	return ellipse
 659  }
 660  
 661  // ---------------------------------------------------------------------------
 662  // paint/paint.go
 663  // ---------------------------------------------------------------------------
 664  
 665  // ImageFilter is the scaling filter for images.
 666  type ImageFilter byte
 667  
 668  const (
 669  	ImageFilterLinear ImageFilter = iota
 670  	ImageFilterNearest
 671  )
 672  
 673  // PaintImageOp sets the brush to an image. (was paint.ImageOp)
 674  type PaintImageOp struct {
 675  	Filter  ImageFilter
 676  	uniform bool
 677  	color   color.NRGBA
 678  	src     *image.RGBA
 679  	handle  any
 680  }
 681  
 682  // PaintColorOp sets the brush to a constant color.
 683  type PaintColorOp struct {
 684  	Color color.NRGBA
 685  }
 686  
 687  // PaintLinearGradientOp sets the brush to a gradient.
 688  type PaintLinearGradientOp struct {
 689  	Stop1  Point
 690  	Color1 color.NRGBA
 691  	Stop2  Point
 692  	Color2 color.NRGBA
 693  }
 694  
 695  // PaintPaintOp fills the current clip area with the current brush.
 696  type PaintPaintOp struct{}
 697  
 698  // PaintOpacityStack represents an opacity applied to all painting operations
 699  // until Pop is called.
 700  type PaintOpacityStack struct {
 701  	id      StackID
 702  	macroID uint32
 703  	ops     *Ops
 704  }
 705  
 706  // NewPaintImageOp creates a PaintImageOp backed by src.
 707  func NewPaintImageOp(src image.Image) PaintImageOp {
 708  	switch src := src.(type) {
 709  	case *image.Uniform:
 710  		col := color.NRGBAModel().Convert(src.C).(color.NRGBA)
 711  		return PaintImageOp{
 712  			uniform: true,
 713  			color:   col,
 714  		}
 715  	case *image.RGBA:
 716  		// Use src pointer as handle so GPU texture is cached across frames
 717  		// (same *image.RGBA = same cache key = texture reuse, not re-upload).
 718  		return PaintImageOp{
 719  			src:    src,
 720  			handle: src,
 721  		}
 722  	}
 723  
 724  	sz := src.Bounds().Size()
 725  	dst := image.NewRGBA(image.Rectangle{
 726  		Max: sz,
 727  	})
 728  	draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.Src)
 729  	return PaintImageOp{
 730  		src:    dst,
 731  		handle: dst, // use pointer as stable cache key
 732  	}
 733  }
 734  
 735  func (i PaintImageOp) Size() image.Point {
 736  	if i.src == nil {
 737  		return image.Point{}
 738  	}
 739  	return i.src.Bounds().Size()
 740  }
 741  
 742  func (i PaintImageOp) Add(o *OpList) {
 743  	if i.uniform {
 744  		PaintColorOp{
 745  			Color: i.color,
 746  		}.Add(o)
 747  		return
 748  	} else if i.src == nil || i.src.Bounds().Empty() {
 749  		return
 750  	}
 751  	data := WriteOps2(&o.Internal, TypeImageLen, i.src, i.handle)
 752  	data[0] = byte(TypeImage)
 753  	data[1] = byte(i.Filter)
 754  }
 755  
 756  func (c PaintColorOp) Add(o *OpList) {
 757  	data := WriteOps(&o.Internal, TypeColorLen)
 758  	data[0] = byte(TypeColor)
 759  	data[1] = c.Color.R
 760  	data[2] = c.Color.G
 761  	data[3] = c.Color.B
 762  	data[4] = c.Color.A
 763  }
 764  
 765  func (c PaintLinearGradientOp) Add(o *OpList) {
 766  	data := WriteOps(&o.Internal, TypeLinearGradientLen)
 767  	data[0] = byte(TypeLinearGradient)
 768  
 769  	bo := binary.LittleEndian()
 770  	bo.PutUint32(data[1:], math.Float32bits(c.Stop1.X))
 771  	bo.PutUint32(data[5:], math.Float32bits(c.Stop1.Y))
 772  	bo.PutUint32(data[9:], math.Float32bits(c.Stop2.X))
 773  	bo.PutUint32(data[13:], math.Float32bits(c.Stop2.Y))
 774  
 775  	data[17+0] = c.Color1.R
 776  	data[17+1] = c.Color1.G
 777  	data[17+2] = c.Color1.B
 778  	data[17+3] = c.Color1.A
 779  	data[21+0] = c.Color2.R
 780  	data[21+1] = c.Color2.G
 781  	data[21+2] = c.Color2.B
 782  	data[21+3] = c.Color2.A
 783  }
 784  
 785  func (d PaintPaintOp) Add(o *OpList) {
 786  	data := WriteOps(&o.Internal, TypePaintLen)
 787  	data[0] = byte(TypePaint)
 788  }
 789  
 790  // FillShape fills the clip shape with a color.
 791  func FillShape(o *OpList, c color.NRGBA, shape ClipOp) {
 792  	defer shape.Push(o).Pop()
 793  	Fill(o, c)
 794  }
 795  
 796  // Fill paints an infinitely large plane with the provided color.
 797  func Fill(o *OpList, c color.NRGBA) {
 798  	PaintColorOp{Color: c}.Add(o)
 799  	PaintPaintOp{}.Add(o)
 800  }
 801  
 802  // PushOpacity creates a drawing layer with an opacity in the range [0;1].
 803  func PushOpacity(o *OpList, opacity float32) PaintOpacityStack {
 804  	if opacity > 1 {
 805  		opacity = 1
 806  	}
 807  	if opacity < 0 {
 808  		opacity = 0
 809  	}
 810  	id, macroID := PushOp(&o.Internal, OpacityStackKind)
 811  	data := WriteOps(&o.Internal, TypePushOpacityLen)
 812  	bo := binary.LittleEndian()
 813  	data[0] = byte(TypePushOpacity)
 814  	bo.PutUint32(data[1:], math.Float32bits(opacity))
 815  	return PaintOpacityStack{ops: &o.Internal, id: id, macroID: macroID}
 816  }
 817  
 818  func (t PaintOpacityStack) Pop() {
 819  	PopOp(t.ops, OpacityStackKind, t.id, t.macroID)
 820  	data := WriteOps(t.ops, TypePopOpacityLen)
 821  	data[0] = byte(TypePopOpacity)
 822  }
 823