shapes.go raw

   1  // SPDX-License-Identifier: Unlicense OR MIT
   2  
   3  package clip
   4  
   5  import (
   6  	"image"
   7  	"math"
   8  
   9  	"github.com/p9c/p9/pkg/gel/gio/f32"
  10  	"github.com/p9c/p9/pkg/gel/gio/op"
  11  )
  12  
  13  // Rect represents the clip area of a pixel-aligned rectangle.
  14  type Rect image.Rectangle
  15  
  16  // Op returns the op for the rectangle.
  17  func (r Rect) Op() Op {
  18  	return Op{
  19  		bounds:  image.Rectangle(r),
  20  		outline: true,
  21  	}
  22  }
  23  
  24  // Add the clip operation.
  25  func (r Rect) Add(ops *op.Ops) {
  26  	r.Op().Add(ops)
  27  }
  28  
  29  // UniformRRect returns an RRect with all corner radii set to the
  30  // provided radius.
  31  func UniformRRect(rect f32.Rectangle, radius float32) RRect {
  32  	return RRect{
  33  		Rect: rect,
  34  		SE:   radius,
  35  		SW:   radius,
  36  		NE:   radius,
  37  		NW:   radius,
  38  	}
  39  }
  40  
  41  // RRect represents the clip area of a rectangle with rounded
  42  // corners.
  43  //
  44  // Specify a square with corner radii equal to half the square size to
  45  // construct a circular clip area.
  46  type RRect struct {
  47  	Rect f32.Rectangle
  48  	// The corner radii.
  49  	SE, SW, NW, NE float32
  50  }
  51  
  52  // Op returns the op for the rounded rectangle.
  53  func (rr RRect) Op(ops *op.Ops) Op {
  54  	if rr.SE == 0 && rr.SW == 0 && rr.NW == 0 && rr.NE == 0 {
  55  		r := image.Rectangle{
  56  			Min: image.Point{X: int(rr.Rect.Min.X), Y: int(rr.Rect.Min.Y)},
  57  			Max: image.Point{X: int(rr.Rect.Max.X), Y: int(rr.Rect.Max.Y)},
  58  		}
  59  		// Only use Rect if rr is pixel-aligned, as Rect is guaranteed to be.
  60  		if fPt(r.Min) == rr.Rect.Min && fPt(r.Max) == rr.Rect.Max {
  61  			return Rect(r).Op()
  62  		}
  63  	}
  64  	return Outline{Path: rr.Path(ops)}.Op()
  65  }
  66  
  67  // Add the rectangle clip.
  68  func (rr RRect) Add(ops *op.Ops) {
  69  	rr.Op(ops).Add(ops)
  70  }
  71  
  72  // Path returns the PathSpec for the rounded rectangle.
  73  func (rr RRect) Path(ops *op.Ops) PathSpec {
  74  	var p Path
  75  	p.Begin(ops)
  76  
  77  	// https://pomax.github.io/bezierinfo/#circles_cubic.
  78  	const q = 4 * (math.Sqrt2 - 1) / 3
  79  	const iq = 1 - q
  80  
  81  	se, sw, nw, ne := rr.SE, rr.SW, rr.NW, rr.NE
  82  	w, n, e, s := rr.Rect.Min.X, rr.Rect.Min.Y, rr.Rect.Max.X, rr.Rect.Max.Y
  83  
  84  	p.MoveTo(f32.Point{X: w + nw, Y: n})
  85  	p.LineTo(f32.Point{X: e - ne, Y: n}) // N
  86  	p.CubeTo(                            // NE
  87  		f32.Point{X: e - ne*iq, Y: n},
  88  		f32.Point{X: e, Y: n + ne*iq},
  89  		f32.Point{X: e, Y: n + ne})
  90  	p.LineTo(f32.Point{X: e, Y: s - se}) // E
  91  	p.CubeTo(                            // SE
  92  		f32.Point{X: e, Y: s - se*iq},
  93  		f32.Point{X: e - se*iq, Y: s},
  94  		f32.Point{X: e - se, Y: s})
  95  	p.LineTo(f32.Point{X: w + sw, Y: s}) // S
  96  	p.CubeTo(                            // SW
  97  		f32.Point{X: w + sw*iq, Y: s},
  98  		f32.Point{X: w, Y: s - sw*iq},
  99  		f32.Point{X: w, Y: s - sw})
 100  	p.LineTo(f32.Point{X: w, Y: n + nw}) // W
 101  	p.CubeTo(                            // NW
 102  		f32.Point{X: w, Y: n + nw*iq},
 103  		f32.Point{X: w + nw*iq, Y: n},
 104  		f32.Point{X: w + nw, Y: n})
 105  
 106  	return p.End()
 107  }
 108  
 109  // Circle represents the clip area of a circle.
 110  type Circle struct {
 111  	Center f32.Point
 112  	Radius float32
 113  }
 114  
 115  // Op returns the op for the circle.
 116  func (c Circle) Op(ops *op.Ops) Op {
 117  	return Outline{Path: c.Path(ops)}.Op()
 118  }
 119  
 120  // Add the circle clip.
 121  func (c Circle) Add(ops *op.Ops) {
 122  	c.Op(ops).Add(ops)
 123  }
 124  
 125  // Path returns the PathSpec for the circle.
 126  func (c Circle) Path(ops *op.Ops) PathSpec {
 127  	var p Path
 128  	p.Begin(ops)
 129  
 130  	center := c.Center
 131  	r := c.Radius
 132  
 133  	// https://pomax.github.io/bezierinfo/#circles_cubic.
 134  	const q = 4 * (math.Sqrt2 - 1) / 3
 135  
 136  	curve := r * q
 137  	top := f32.Point{X: center.X, Y: center.Y - r}
 138  
 139  	p.MoveTo(top)
 140  	p.CubeTo(
 141  		f32.Point{X: center.X + curve, Y: center.Y - r},
 142  		f32.Point{X: center.X + r, Y: center.Y - curve},
 143  		f32.Point{X: center.X + r, Y: center.Y},
 144  	)
 145  	p.CubeTo(
 146  		f32.Point{X: center.X + r, Y: center.Y + curve},
 147  		f32.Point{X: center.X + curve, Y: center.Y + r},
 148  		f32.Point{X: center.X, Y: center.Y + r},
 149  	)
 150  	p.CubeTo(
 151  		f32.Point{X: center.X - curve, Y: center.Y + r},
 152  		f32.Point{X: center.X - r, Y: center.Y + curve},
 153  		f32.Point{X: center.X - r, Y: center.Y},
 154  	)
 155  	p.CubeTo(
 156  		f32.Point{X: center.X - r, Y: center.Y - curve},
 157  		f32.Point{X: center.X - curve, Y: center.Y - r},
 158  		top,
 159  	)
 160  	return p.End()
 161  }
 162  
 163  func fPt(p image.Point) f32.Point {
 164  	return f32.Point{
 165  		X: float32(p.X), Y: float32(p.Y),
 166  	}
 167  }
 168