gioui.org/ui@v0.0.0-20190926171558-ce74bc0cbaea/paint/path.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package paint 4 5 import ( 6 "encoding/binary" 7 "math" 8 "unsafe" 9 10 "gioui.org/ui" 11 "gioui.org/ui/f32" 12 "gioui.org/ui/internal/opconst" 13 "gioui.org/ui/internal/path" 14 ) 15 16 // PathBuilder builds and adds a general ClipOp clip path 17 // from lines and curves. 18 // PathBuilder generates no garbage and can be used for 19 // dynamic paths; path data is stored directly in the Ops 20 // list supplied to Init. 21 type PathBuilder struct { 22 ops *ui.Ops 23 firstVert int 24 nverts int 25 maxy float32 26 pen f32.Point 27 bounds f32.Rectangle 28 hasBounds bool 29 } 30 31 // ClipOp sets the current clip path. 32 type ClipOp struct { 33 bounds f32.Rectangle 34 } 35 36 func (p ClipOp) Add(o *ui.Ops) { 37 data := make([]byte, opconst.TypeClipLen) 38 data[0] = byte(opconst.TypeClip) 39 bo := binary.LittleEndian 40 bo.PutUint32(data[1:], math.Float32bits(p.bounds.Min.X)) 41 bo.PutUint32(data[5:], math.Float32bits(p.bounds.Min.Y)) 42 bo.PutUint32(data[9:], math.Float32bits(p.bounds.Max.X)) 43 bo.PutUint32(data[13:], math.Float32bits(p.bounds.Max.Y)) 44 o.Write(data) 45 } 46 47 // Init the builder and specify the operations list for 48 // storing the path data and final ClipOp. 49 func (p *PathBuilder) Init(ops *ui.Ops) { 50 p.ops = ops 51 } 52 53 // MoveTo moves the pen to the given position. 54 func (p *PathBuilder) Move(to f32.Point) { 55 p.end() 56 to = to.Add(p.pen) 57 p.maxy = to.Y 58 p.pen = to 59 } 60 61 // end completes the current contour. 62 func (p *PathBuilder) end() { 63 aux := p.ops.Aux() 64 bo := binary.LittleEndian 65 // Fill in maximal Y coordinates of the NW and NE corners. 66 for i := p.firstVert; i < p.nverts; i++ { 67 off := path.VertStride*i + int(unsafe.Offsetof(((*path.Vertex)(nil)).MaxY)) 68 bo.PutUint32(aux[off:], math.Float32bits(p.maxy)) 69 } 70 p.firstVert = p.nverts 71 } 72 73 // Line records a line from the pen to end. 74 func (p *PathBuilder) Line(to f32.Point) { 75 to = to.Add(p.pen) 76 p.lineTo(to) 77 } 78 79 func (p *PathBuilder) lineTo(to f32.Point) { 80 // Model lines as degenerate quadratic Béziers. 81 p.quadTo(to.Add(p.pen).Mul(.5), to) 82 } 83 84 // Quad records a quadratic Bézier from the pen to end 85 // with the control point ctrl. 86 func (p *PathBuilder) Quad(ctrl, to f32.Point) { 87 ctrl = ctrl.Add(p.pen) 88 to = to.Add(p.pen) 89 p.quadTo(ctrl, to) 90 } 91 92 func (p *PathBuilder) quadTo(ctrl, to f32.Point) { 93 // Zero width curves don't contribute to stenciling. 94 if p.pen.X == to.X && p.pen.X == ctrl.X { 95 p.pen = to 96 return 97 } 98 99 bounds := f32.Rectangle{ 100 Min: p.pen, 101 Max: to, 102 }.Canon() 103 104 // If the curve contain areas where a vertical line 105 // intersects it twice, split the curve in two x monotone 106 // lower and upper curves. The stencil fragment program 107 // expects only one intersection per curve. 108 109 // Find the t where the derivative in x is 0. 110 v0 := ctrl.Sub(p.pen) 111 v1 := to.Sub(ctrl) 112 d := v0.X - v1.X 113 // t = v0 / d. Split if t is in ]0;1[. 114 if v0.X > 0 && d > v0.X || v0.X < 0 && d < v0.X { 115 t := v0.X / d 116 ctrl0 := p.pen.Mul(1 - t).Add(ctrl.Mul(t)) 117 ctrl1 := ctrl.Mul(1 - t).Add(to.Mul(t)) 118 mid := ctrl0.Mul(1 - t).Add(ctrl1.Mul(t)) 119 p.simpleQuadTo(ctrl0, mid) 120 p.simpleQuadTo(ctrl1, to) 121 if mid.X > bounds.Max.X { 122 bounds.Max.X = mid.X 123 } 124 if mid.X < bounds.Min.X { 125 bounds.Min.X = mid.X 126 } 127 } else { 128 p.simpleQuadTo(ctrl, to) 129 } 130 // Find the y extremum, if any. 131 d = v0.Y - v1.Y 132 if v0.Y > 0 && d > v0.Y || v0.Y < 0 && d < v0.Y { 133 t := v0.Y / d 134 y := (1-t)*(1-t)*p.pen.Y + 2*(1-t)*t*ctrl.Y + t*t*to.Y 135 if y > bounds.Max.Y { 136 bounds.Max.Y = y 137 } 138 if y < bounds.Min.Y { 139 bounds.Min.Y = y 140 } 141 } 142 p.expand(bounds) 143 } 144 145 // Cube records a cubic Bézier from the pen through 146 // two control points ending in to. 147 func (p *PathBuilder) Cube(ctrl0, ctrl1, to f32.Point) { 148 ctrl0 = ctrl0.Add(p.pen) 149 ctrl1 = ctrl1.Add(p.pen) 150 to = to.Add(p.pen) 151 // Set the maximum distance proportionally to the longest side 152 // of the bounding rectangle. 153 hull := f32.Rectangle{ 154 Min: p.pen, 155 Max: ctrl0, 156 }.Canon().Add(ctrl1).Add(to) 157 l := hull.Dx() 158 if h := hull.Dy(); h > l { 159 l = h 160 } 161 p.approxCubeTo(0, l*0.001, ctrl0, ctrl1, to) 162 } 163 164 // approxCube approximates a cubic Bézier by a series of quadratic 165 // curves. 166 func (p *PathBuilder) approxCubeTo(splits int, maxDist float32, ctrl0, ctrl1, to f32.Point) int { 167 // The idea is from 168 // https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html 169 // where a quadratic approximates a cubic by eliminating its t³ term 170 // from its polynomial expression anchored at the starting point: 171 // 172 // P(t) = pen + 3t(ctrl0 - pen) + 3t²(ctrl1 - 2ctrl0 + pen) + t³(to - 3ctrl1 + 3ctrl0 - pen) 173 // 174 // The control point for the new quadratic Q1 that shares starting point, pen, with P is 175 // 176 // C1 = (3ctrl0 - pen)/2 177 // 178 // The reverse cubic anchored at the end point has the polynomial 179 // 180 // P'(t) = to + 3t(ctrl1 - to) + 3t²(ctrl0 - 2ctrl1 + to) + t³(pen - 3ctrl0 + 3ctrl1 - to) 181 // 182 // The corresponding quadratic Q2 that shares the end point, to, with P has control 183 // point 184 // 185 // C2 = (3ctrl1 - to)/2 186 // 187 // The combined quadratic Bézier, Q, shares both start and end points with its cubic 188 // and use the midpoint between the two curves Q1 and Q2 as control point: 189 // 190 // C = (3ctrl0 - pen + 3ctrl1 - to)/4 191 c := ctrl0.Mul(3).Sub(p.pen).Add(ctrl1.Mul(3)).Sub(to).Mul(1.0 / 4.0) 192 const maxSplits = 32 193 if splits >= maxSplits { 194 p.quadTo(c, to) 195 return splits 196 } 197 // The maximum distance between the cubic P and its approximation Q given t 198 // can be shown to be 199 // 200 // d = sqrt(3)/36*|to - 3ctrl1 + 3ctrl0 - pen| 201 // 202 // To save a square root, compare d² with the squared tolerance. 203 v := to.Sub(ctrl1.Mul(3)).Add(ctrl0.Mul(3)).Sub(p.pen) 204 d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36) 205 if d2 <= maxDist*maxDist { 206 p.quadTo(c, to) 207 return splits 208 } 209 // De Casteljau split the curve and approximate the halves. 210 t := float32(0.5) 211 c0 := p.pen.Add(ctrl0.Sub(p.pen).Mul(t)) 212 c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t)) 213 c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t)) 214 c01 := c0.Add(c1.Sub(c0).Mul(t)) 215 c12 := c1.Add(c2.Sub(c1).Mul(t)) 216 c0112 := c01.Add(c12.Sub(c01).Mul(t)) 217 splits++ 218 splits = p.approxCubeTo(splits, maxDist, c0, c01, c0112) 219 splits = p.approxCubeTo(splits, maxDist, c12, c2, to) 220 return splits 221 } 222 223 func (p *PathBuilder) expand(b f32.Rectangle) { 224 if !p.hasBounds { 225 p.hasBounds = true 226 inf := float32(math.Inf(+1)) 227 p.bounds = f32.Rectangle{ 228 Min: f32.Point{X: inf, Y: inf}, 229 Max: f32.Point{X: -inf, Y: -inf}, 230 } 231 } 232 p.bounds = p.bounds.Union(b) 233 } 234 235 func (p *PathBuilder) vertex(cornerx, cornery int16, ctrl, to f32.Point) { 236 p.nverts++ 237 v := path.Vertex{ 238 CornerX: cornerx, 239 CornerY: cornery, 240 FromX: p.pen.X, 241 FromY: p.pen.Y, 242 CtrlX: ctrl.X, 243 CtrlY: ctrl.Y, 244 ToX: to.X, 245 ToY: to.Y, 246 } 247 data := make([]byte, path.VertStride+1) 248 data[0] = byte(opconst.TypeAux) 249 bo := binary.LittleEndian 250 data[1] = byte(uint16(v.CornerX)) 251 data[2] = byte(uint16(v.CornerX) >> 8) 252 data[3] = byte(uint16(v.CornerY)) 253 data[4] = byte(uint16(v.CornerY) >> 8) 254 bo.PutUint32(data[5:], math.Float32bits(v.MaxY)) 255 bo.PutUint32(data[9:], math.Float32bits(v.FromX)) 256 bo.PutUint32(data[13:], math.Float32bits(v.FromY)) 257 bo.PutUint32(data[17:], math.Float32bits(v.CtrlX)) 258 bo.PutUint32(data[21:], math.Float32bits(v.CtrlY)) 259 bo.PutUint32(data[25:], math.Float32bits(v.ToX)) 260 bo.PutUint32(data[29:], math.Float32bits(v.ToY)) 261 p.ops.Write(data) 262 } 263 264 func (p *PathBuilder) simpleQuadTo(ctrl, to f32.Point) { 265 if p.pen.Y > p.maxy { 266 p.maxy = p.pen.Y 267 } 268 if ctrl.Y > p.maxy { 269 p.maxy = ctrl.Y 270 } 271 if to.Y > p.maxy { 272 p.maxy = to.Y 273 } 274 // NW. 275 p.vertex(-1, 1, ctrl, to) 276 // NE. 277 p.vertex(1, 1, ctrl, to) 278 // SW. 279 p.vertex(-1, -1, ctrl, to) 280 // SE. 281 p.vertex(1, -1, ctrl, to) 282 p.pen = to 283 } 284 285 // End the path and add the resulting ClipOp to 286 // the operation list passed to Init. 287 func (p *PathBuilder) End() { 288 p.end() 289 ClipOp{ 290 bounds: p.bounds, 291 }.Add(p.ops) 292 }