github.com/gop9/olt@v0.0.0-20200202132135-d956aad50b08/gio/op/clip/clip.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package clip 4 5 import ( 6 "encoding/binary" 7 "image" 8 "math" 9 10 "github.com/gop9/olt/gio/f32" 11 "github.com/gop9/olt/gio/internal/opconst" 12 "github.com/gop9/olt/gio/internal/path" 13 "github.com/gop9/olt/gio/op" 14 ) 15 16 // Path constructs a Op clip path described by lines and 17 // Bézier curves, where drawing outside the Path is discarded. 18 // The inside-ness of a pixel is determines by the even-odd rule, 19 // similar to the SVG rule of the same name. 20 // 21 // Path generates no garbage and can be used for dynamic paths; path 22 // data is stored directly in the Ops list supplied to Begin. 23 type Path struct { 24 ops *op.Ops 25 contour int 26 pen f32.Point 27 bounds f32.Rectangle 28 hasBounds bool 29 macro op.MacroOp 30 } 31 32 // Op sets the current clip to the intersection of 33 // the existing clip with this clip. 34 // 35 // If you need to reset the clip to its previous values after 36 // applying a Op, use op.StackOp. 37 type Op struct { 38 macro op.MacroOp 39 bounds f32.Rectangle 40 } 41 42 func (p Op) Add(o *op.Ops) { 43 p.macro.Add() 44 data := o.Write(opconst.TypeClipLen) 45 data[0] = byte(opconst.TypeClip) 46 bo := binary.LittleEndian 47 bo.PutUint32(data[1:], math.Float32bits(p.bounds.Min.X)) 48 bo.PutUint32(data[5:], math.Float32bits(p.bounds.Min.Y)) 49 bo.PutUint32(data[9:], math.Float32bits(p.bounds.Max.X)) 50 bo.PutUint32(data[13:], math.Float32bits(p.bounds.Max.Y)) 51 } 52 53 // Begin the path, storing the path data and final Op into ops. 54 func (p *Path) Begin(ops *op.Ops) { 55 p.ops = ops 56 p.macro.Record(ops) 57 // Write the TypeAux opcode and a byte for marking whether the 58 // path has had its MaxY filled out. If not, the gpu will fill it 59 // before using it. 60 data := ops.Write(2) 61 data[0] = byte(opconst.TypeAux) 62 } 63 64 // MoveTo moves the pen to the given position. 65 func (p *Path) Move(to f32.Point) { 66 p.end() 67 to = to.Add(p.pen) 68 p.pen = to 69 } 70 71 // end completes the current contour. 72 func (p *Path) end() { 73 p.contour++ 74 } 75 76 // Line moves the pen by the amount specified by delta, recording a line. 77 func (p *Path) Line(delta f32.Point) { 78 to := delta.Add(p.pen) 79 p.lineTo(to) 80 } 81 82 func (p *Path) lineTo(to f32.Point) { 83 // Model lines as degenerate quadratic Béziers. 84 p.quadTo(to.Add(p.pen).Mul(.5), to) 85 } 86 87 // Quad records a quadratic Bézier from the pen to end 88 // with the control point ctrl. 89 func (p *Path) Quad(ctrl, to f32.Point) { 90 ctrl = ctrl.Add(p.pen) 91 to = to.Add(p.pen) 92 p.quadTo(ctrl, to) 93 } 94 95 func (p *Path) quadTo(ctrl, to f32.Point) { 96 // Zero width curves don't contribute to stenciling. 97 if p.pen.X == to.X && p.pen.X == ctrl.X { 98 p.pen = to 99 return 100 } 101 102 bounds := f32.Rectangle{ 103 Min: p.pen, 104 Max: to, 105 }.Canon() 106 107 // If the curve contain areas where a vertical line 108 // intersects it twice, split the curve in two x monotone 109 // lower and upper curves. The stencil fragment program 110 // expects only one intersection per curve. 111 112 // Find the t where the derivative in x is 0. 113 v0 := ctrl.Sub(p.pen) 114 v1 := to.Sub(ctrl) 115 d := v0.X - v1.X 116 // t = v0 / d. Split if t is in ]0;1[. 117 if v0.X > 0 && d > v0.X || v0.X < 0 && d < v0.X { 118 t := v0.X / d 119 ctrl0 := p.pen.Mul(1 - t).Add(ctrl.Mul(t)) 120 ctrl1 := ctrl.Mul(1 - t).Add(to.Mul(t)) 121 mid := ctrl0.Mul(1 - t).Add(ctrl1.Mul(t)) 122 p.simpleQuadTo(ctrl0, mid) 123 p.simpleQuadTo(ctrl1, to) 124 if mid.X > bounds.Max.X { 125 bounds.Max.X = mid.X 126 } 127 if mid.X < bounds.Min.X { 128 bounds.Min.X = mid.X 129 } 130 } else { 131 p.simpleQuadTo(ctrl, to) 132 } 133 // Find the y extremum, if any. 134 d = v0.Y - v1.Y 135 if v0.Y > 0 && d > v0.Y || v0.Y < 0 && d < v0.Y { 136 t := v0.Y / d 137 y := (1-t)*(1-t)*p.pen.Y + 2*(1-t)*t*ctrl.Y + t*t*to.Y 138 if y > bounds.Max.Y { 139 bounds.Max.Y = y 140 } 141 if y < bounds.Min.Y { 142 bounds.Min.Y = y 143 } 144 } 145 p.expand(bounds) 146 } 147 148 // Cube records a cubic Bézier from the pen through 149 // two control points ending in to. 150 func (p *Path) Cube(ctrl0, ctrl1, to f32.Point) { 151 ctrl0 = ctrl0.Add(p.pen) 152 ctrl1 = ctrl1.Add(p.pen) 153 to = to.Add(p.pen) 154 // Set the maximum distance proportionally to the longest side 155 // of the bounding rectangle. 156 hull := f32.Rectangle{ 157 Min: p.pen, 158 Max: ctrl0, 159 }.Canon().Add(ctrl1).Add(to) 160 l := hull.Dx() 161 if h := hull.Dy(); h > l { 162 l = h 163 } 164 p.approxCubeTo(0, l*0.001, ctrl0, ctrl1, to) 165 } 166 167 // approxCube approximates a cubic Bézier by a series of quadratic 168 // curves. 169 func (p *Path) approxCubeTo(splits int, maxDist float32, ctrl0, ctrl1, to f32.Point) int { 170 // The idea is from 171 // https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html 172 // where a quadratic approximates a cubic by eliminating its t³ term 173 // from its polynomial expression anchored at the starting point: 174 // 175 // P(t) = pen + 3t(ctrl0 - pen) + 3t²(ctrl1 - 2ctrl0 + pen) + t³(to - 3ctrl1 + 3ctrl0 - pen) 176 // 177 // The control point for the new quadratic Q1 that shares starting point, pen, with P is 178 // 179 // C1 = (3ctrl0 - pen)/2 180 // 181 // The reverse cubic anchored at the end point has the polynomial 182 // 183 // P'(t) = to + 3t(ctrl1 - to) + 3t²(ctrl0 - 2ctrl1 + to) + t³(pen - 3ctrl0 + 3ctrl1 - to) 184 // 185 // The corresponding quadratic Q2 that shares the end point, to, with P has control 186 // point 187 // 188 // C2 = (3ctrl1 - to)/2 189 // 190 // The combined quadratic Bézier, Q, shares both start and end points with its cubic 191 // and use the midpoint between the two curves Q1 and Q2 as control point: 192 // 193 // C = (3ctrl0 - pen + 3ctrl1 - to)/4 194 c := ctrl0.Mul(3).Sub(p.pen).Add(ctrl1.Mul(3)).Sub(to).Mul(1.0 / 4.0) 195 const maxSplits = 32 196 if splits >= maxSplits { 197 p.quadTo(c, to) 198 return splits 199 } 200 // The maximum distance between the cubic P and its approximation Q given t 201 // can be shown to be 202 // 203 // d = sqrt(3)/36*|to - 3ctrl1 + 3ctrl0 - pen| 204 // 205 // To save a square root, compare d² with the squared tolerance. 206 v := to.Sub(ctrl1.Mul(3)).Add(ctrl0.Mul(3)).Sub(p.pen) 207 d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36) 208 if d2 <= maxDist*maxDist { 209 p.quadTo(c, to) 210 return splits 211 } 212 // De Casteljau split the curve and approximate the halves. 213 t := float32(0.5) 214 c0 := p.pen.Add(ctrl0.Sub(p.pen).Mul(t)) 215 c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t)) 216 c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t)) 217 c01 := c0.Add(c1.Sub(c0).Mul(t)) 218 c12 := c1.Add(c2.Sub(c1).Mul(t)) 219 c0112 := c01.Add(c12.Sub(c01).Mul(t)) 220 splits++ 221 splits = p.approxCubeTo(splits, maxDist, c0, c01, c0112) 222 splits = p.approxCubeTo(splits, maxDist, c12, c2, to) 223 return splits 224 } 225 226 func (p *Path) expand(b f32.Rectangle) { 227 if !p.hasBounds { 228 p.hasBounds = true 229 inf := float32(math.Inf(+1)) 230 p.bounds = f32.Rectangle{ 231 Min: f32.Point{X: inf, Y: inf}, 232 Max: f32.Point{X: -inf, Y: -inf}, 233 } 234 } 235 p.bounds = p.bounds.Union(b) 236 } 237 238 func (p *Path) vertex(cornerx, cornery int16, ctrl, to f32.Point) { 239 v := path.Vertex{ 240 CornerX: cornerx, 241 CornerY: cornery, 242 FromX: p.pen.X, 243 FromY: p.pen.Y, 244 CtrlX: ctrl.X, 245 CtrlY: ctrl.Y, 246 ToX: to.X, 247 ToY: to.Y, 248 } 249 data := p.ops.Write(path.VertStride) 250 bo := binary.LittleEndian 251 data[0] = byte(uint16(v.CornerX)) 252 data[1] = byte(uint16(v.CornerX) >> 8) 253 data[2] = byte(uint16(v.CornerY)) 254 data[3] = byte(uint16(v.CornerY) >> 8) 255 // Put the contour index in MaxY. 256 bo.PutUint32(data[4:], uint32(p.contour)) 257 bo.PutUint32(data[8:], math.Float32bits(v.FromX)) 258 bo.PutUint32(data[12:], math.Float32bits(v.FromY)) 259 bo.PutUint32(data[16:], math.Float32bits(v.CtrlX)) 260 bo.PutUint32(data[20:], math.Float32bits(v.CtrlY)) 261 bo.PutUint32(data[24:], math.Float32bits(v.ToX)) 262 bo.PutUint32(data[28:], math.Float32bits(v.ToY)) 263 } 264 265 func (p *Path) simpleQuadTo(ctrl, to f32.Point) { 266 // NW. 267 p.vertex(-1, 1, ctrl, to) 268 // NE. 269 p.vertex(1, 1, ctrl, to) 270 // SW. 271 p.vertex(-1, -1, ctrl, to) 272 // SE. 273 p.vertex(1, -1, ctrl, to) 274 p.pen = to 275 } 276 277 // End the path and return a clip operation that represents it. 278 func (p *Path) End() Op { 279 p.end() 280 p.macro.Stop() 281 return Op{ 282 macro: p.macro, 283 bounds: p.bounds, 284 } 285 } 286 287 // Rect represents the clip area of a rectangle with rounded 288 // corners.The origin is in the upper left 289 // corner. 290 // Specify a square with corner radii equal to half the square size to 291 // construct a circular clip area. 292 type Rect struct { 293 Rect f32.Rectangle 294 // The corner radii. 295 SE, SW, NW, NE float32 296 } 297 298 // Op returns the Op for the rectangle. 299 func (rr Rect) Op(ops *op.Ops) Op { 300 r := rr.Rect 301 // Optimize for the common pixel aligned rectangle with no 302 // corner rounding. 303 if rr.SE == 0 && rr.SW == 0 && rr.NW == 0 && rr.NE == 0 { 304 ri := image.Rectangle{ 305 Min: image.Point{X: int(r.Min.X), Y: int(r.Min.Y)}, 306 Max: image.Point{X: int(r.Max.X), Y: int(r.Max.Y)}, 307 } 308 // Optimize pixel-aligned rectangles to just its bounds. 309 if r == toRectF(ri) { 310 return Op{bounds: r} 311 } 312 } 313 return roundRect(ops, r, rr.SE, rr.SW, rr.NW, rr.NE) 314 } 315 316 // roundRect returns the clip area of a rectangle with rounded 317 // corners defined by their radii. 318 func roundRect(ops *op.Ops, r f32.Rectangle, se, sw, nw, ne float32) Op { 319 size := r.Size() 320 // https://pomax.github.io/bezierinfo/#circles_cubic. 321 w, h := float32(size.X), float32(size.Y) 322 const c = 0.55228475 // 4*(sqrt(2)-1)/3 323 var p Path 324 p.Begin(ops) 325 p.Move(r.Min) 326 p.Move(f32.Point{X: w, Y: h - se}) 327 p.Cube(f32.Point{X: 0, Y: se * c}, f32.Point{X: -se + se*c, Y: se}, f32.Point{X: -se, Y: se}) // SE 328 p.Line(f32.Point{X: sw - w + se, Y: 0}) 329 p.Cube(f32.Point{X: -sw * c, Y: 0}, f32.Point{X: -sw, Y: -sw + sw*c}, f32.Point{X: -sw, Y: -sw}) // SW 330 p.Line(f32.Point{X: 0, Y: nw - h + sw}) 331 p.Cube(f32.Point{X: 0, Y: -nw * c}, f32.Point{X: nw - nw*c, Y: -nw}, f32.Point{X: nw, Y: -nw}) // NW 332 p.Line(f32.Point{X: w - ne - nw, Y: 0}) 333 p.Cube(f32.Point{X: ne * c, Y: 0}, f32.Point{X: ne, Y: ne - ne*c}, f32.Point{X: ne, Y: ne}) // NE 334 return p.End() 335 } 336 337 func toRectF(r image.Rectangle) f32.Rectangle { 338 return f32.Rectangle{ 339 Min: f32.Point{X: float32(r.Min.X), Y: float32(r.Min.Y)}, 340 Max: f32.Point{X: float32(r.Max.X), Y: float32(r.Max.Y)}, 341 } 342 }