github.com/Seikaijyu/gio@v0.0.1/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/Seikaijyu/gio/f32" 12 f32internal "github.com/Seikaijyu/gio/internal/f32" 13 "github.com/Seikaijyu/gio/internal/ops" 14 "github.com/Seikaijyu/gio/internal/scene" 15 "github.com/Seikaijyu/gio/internal/stroke" 16 "github.com/Seikaijyu/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 }