gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/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 "gioui.org/f32" 12 f32internal "gioui.org/internal/f32" 13 "gioui.org/internal/ops" 14 "gioui.org/internal/scene" 15 "gioui.org/internal/stroke" 16 "gioui.org/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 // 142 // Caller must also call End to finish the drawing. 143 // Forgetting to call it will result in a "panic: cannot mix multi ops with single ones". 144 func (p *Path) Begin(o *op.Ops) { 145 *p = Path{ 146 ops: &o.Internal, 147 macro: op.Record(o), 148 contour: 1, 149 } 150 p.hash.SetSeed(pathSeed) 151 ops.BeginMulti(p.ops) 152 data := ops.WriteMulti(p.ops, ops.TypeAuxLen) 153 data[0] = byte(ops.TypeAux) 154 } 155 156 // End returns a PathSpec ready to use in clipping operations. 157 func (p *Path) End() PathSpec { 158 p.gap() 159 c := p.macro.Stop() 160 ops.EndMulti(p.ops) 161 return PathSpec{ 162 spec: c, 163 hasSegments: p.hasSegments, 164 bounds: p.bounds.Round(), 165 hash: p.hash.Sum64(), 166 } 167 } 168 169 // Move moves the pen by the amount specified by delta. 170 func (p *Path) Move(delta f32.Point) { 171 to := delta.Add(p.pen) 172 p.MoveTo(to) 173 } 174 175 // MoveTo moves the pen to the specified absolute coordinate. 176 func (p *Path) MoveTo(to f32.Point) { 177 if p.pen == to { 178 return 179 } 180 p.gap() 181 p.end() 182 p.pen = to 183 p.start = to 184 } 185 186 func (p *Path) gap() { 187 if p.pen != p.start { 188 // A closed contour starts and ends in the same point. 189 // This move creates a gap in the contour, register it. 190 data := ops.WriteMulti(p.ops, scene.CommandSize+4) 191 bo := binary.LittleEndian 192 bo.PutUint32(data[0:], uint32(p.contour)) 193 p.cmd(data[4:], scene.Gap(p.pen, p.start)) 194 } 195 } 196 197 // end completes the current contour. 198 func (p *Path) end() { 199 p.contour++ 200 } 201 202 // Line moves the pen by the amount specified by delta, recording a line. 203 func (p *Path) Line(delta f32.Point) { 204 to := delta.Add(p.pen) 205 p.LineTo(to) 206 } 207 208 // LineTo moves the pen to the absolute point specified, recording a line. 209 func (p *Path) LineTo(to f32.Point) { 210 if to == p.pen { 211 return 212 } 213 data := ops.WriteMulti(p.ops, scene.CommandSize+4) 214 bo := binary.LittleEndian 215 bo.PutUint32(data[0:], uint32(p.contour)) 216 p.cmd(data[4:], scene.Line(p.pen, to)) 217 p.pen = to 218 p.expand(to) 219 } 220 221 func (p *Path) cmd(data []byte, c scene.Command) { 222 ops.EncodeCommand(data, c) 223 p.hash.Write(data) 224 } 225 226 func (p *Path) expand(pt f32.Point) { 227 if !p.hasSegments { 228 p.hasSegments = true 229 p.bounds = f32internal.Rectangle{Min: pt, Max: pt} 230 } else { 231 b := p.bounds 232 if pt.X < b.Min.X { 233 b.Min.X = pt.X 234 } 235 if pt.Y < b.Min.Y { 236 b.Min.Y = pt.Y 237 } 238 if pt.X > b.Max.X { 239 b.Max.X = pt.X 240 } 241 if pt.Y > b.Max.Y { 242 b.Max.Y = pt.Y 243 } 244 p.bounds = b 245 } 246 } 247 248 // Quad records a quadratic Bézier from the pen to end 249 // with the control point ctrl. 250 func (p *Path) Quad(ctrl, to f32.Point) { 251 ctrl = ctrl.Add(p.pen) 252 to = to.Add(p.pen) 253 p.QuadTo(ctrl, to) 254 } 255 256 // QuadTo records a quadratic Bézier from the pen to end 257 // with the control point ctrl, with absolute coordinates. 258 func (p *Path) QuadTo(ctrl, to f32.Point) { 259 if ctrl == p.pen && to == p.pen { 260 return 261 } 262 data := ops.WriteMulti(p.ops, scene.CommandSize+4) 263 bo := binary.LittleEndian 264 bo.PutUint32(data[0:], uint32(p.contour)) 265 p.cmd(data[4:], scene.Quad(p.pen, ctrl, to)) 266 p.pen = to 267 p.expand(ctrl) 268 p.expand(to) 269 } 270 271 // ArcTo adds an elliptical arc to the path. The implied ellipse is defined 272 // by its focus points f1 and f2. 273 // The arc starts in the current point and ends angle radians along the ellipse boundary. 274 // The sign of angle determines the direction; positive being counter-clockwise, 275 // negative clockwise. 276 func (p *Path) ArcTo(f1, f2 f32.Point, angle float32) { 277 m, segments := stroke.ArcTransform(p.pen, f1, f2, angle) 278 for i := 0; i < segments; i++ { 279 p0 := p.pen 280 p1 := m.Transform(p0) 281 p2 := m.Transform(p1) 282 ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5)) 283 p.QuadTo(ctl, p2) 284 } 285 } 286 287 // Arc is like ArcTo where f1 and f2 are relative to the current position. 288 func (p *Path) Arc(f1, f2 f32.Point, angle float32) { 289 f1 = f1.Add(p.pen) 290 f2 = f2.Add(p.pen) 291 p.ArcTo(f1, f2, angle) 292 } 293 294 // Cube records a cubic Bézier from the pen through 295 // two control points ending in to. 296 func (p *Path) Cube(ctrl0, ctrl1, to f32.Point) { 297 p.CubeTo(p.pen.Add(ctrl0), p.pen.Add(ctrl1), p.pen.Add(to)) 298 } 299 300 // CubeTo records a cubic Bézier from the pen through 301 // two control points ending in to, with absolute coordinates. 302 func (p *Path) CubeTo(ctrl0, ctrl1, to f32.Point) { 303 if ctrl0 == p.pen && ctrl1 == p.pen && to == p.pen { 304 return 305 } 306 data := ops.WriteMulti(p.ops, scene.CommandSize+4) 307 bo := binary.LittleEndian 308 bo.PutUint32(data[0:], uint32(p.contour)) 309 p.cmd(data[4:], scene.Cubic(p.pen, ctrl0, ctrl1, to)) 310 p.pen = to 311 p.expand(ctrl0) 312 p.expand(ctrl1) 313 p.expand(to) 314 } 315 316 // Close closes the path contour. 317 func (p *Path) Close() { 318 if p.pen != p.start { 319 p.LineTo(p.start) 320 } 321 p.end() 322 } 323 324 // Stroke represents a stroked path. 325 type Stroke struct { 326 Path PathSpec 327 // Width of the stroked path. 328 Width float32 329 } 330 331 // Op returns a clip operation representing the stroke. 332 func (s Stroke) Op() Op { 333 return Op{ 334 path: s.Path, 335 width: s.Width, 336 } 337 } 338 339 // Outline represents the area inside of a path, according to the 340 // non-zero winding rule. 341 type Outline struct { 342 Path PathSpec 343 } 344 345 // Op returns a clip operation representing the outline. 346 func (o Outline) Op() Op { 347 return Op{ 348 path: o.Path, 349 outline: true, 350 } 351 }