github.com/cybriq/giocore@v0.0.7-0.20210703034601-cfb9cb5f3900/gpu/internal/rendertest/render_test.go (about) 1 package rendertest 2 3 import ( 4 "image" 5 "image/color" 6 "math" 7 "testing" 8 9 "golang.org/x/image/colornames" 10 11 "github.com/cybriq/giocore/f32" 12 "github.com/cybriq/giocore/internal/f32color" 13 "github.com/cybriq/giocore/op" 14 "github.com/cybriq/giocore/op/clip" 15 "github.com/cybriq/giocore/op/paint" 16 ) 17 18 func TestTransformMacro(t *testing.T) { 19 // testcase resulting from original bug when rendering layout.Stacked 20 21 // Build clip-path. 22 c := constSqPath() 23 24 run(t, func(o *op.Ops) { 25 26 // render the first Stacked item 27 m1 := op.Record(o) 28 dr := image.Rect(0, 0, 128, 50) 29 paint.FillShape(o, black, clip.Rect(dr).Op()) 30 c1 := m1.Stop() 31 32 // Render the second stacked item 33 m2 := op.Record(o) 34 paint.ColorOp{Color: red}.Add(o) 35 // Simulate a draw text call 36 stack := op.Save(o) 37 op.Offset(f32.Pt(0, 10)).Add(o) 38 39 // Apply the clip-path. 40 c.Add(o) 41 42 paint.PaintOp{}.Add(o) 43 stack.Load() 44 45 c2 := m2.Stop() 46 47 // Call each of them in a transform 48 s1 := op.Save(o) 49 op.Offset(f32.Pt(0, 0)).Add(o) 50 c1.Add(o) 51 s1.Load() 52 s2 := op.Save(o) 53 op.Offset(f32.Pt(0, 0)).Add(o) 54 c2.Add(o) 55 s2.Load() 56 }, func(r result) { 57 r.expect(5, 15, colornames.Red) 58 r.expect(15, 15, colornames.Black) 59 r.expect(11, 51, transparent) 60 }) 61 } 62 63 func TestRepeatedPaintsZ(t *testing.T) { 64 run(t, func(o *op.Ops) { 65 // Draw a rectangle 66 paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 128, 50)).Op()) 67 68 builder := clip.Path{} 69 builder.Begin(o) 70 builder.Move(f32.Pt(0, 0)) 71 builder.Line(f32.Pt(10, 0)) 72 builder.Line(f32.Pt(0, 10)) 73 builder.Line(f32.Pt(-10, 0)) 74 builder.Line(f32.Pt(0, -10)) 75 p := builder.End() 76 clip.Outline{ 77 Path: p, 78 }.Op().Add(o) 79 paint.Fill(o, red) 80 }, func(r result) { 81 r.expect(5, 5, colornames.Red) 82 r.expect(11, 15, colornames.Black) 83 r.expect(11, 51, transparent) 84 }) 85 } 86 87 func TestNoClipFromPaint(t *testing.T) { 88 // ensure that a paint operation does not pollute the state 89 // by leaving any clip paths in place. 90 run(t, func(o *op.Ops) { 91 a := f32.Affine2D{}.Rotate(f32.Pt(20, 20), math.Pi/4) 92 op.Affine(a).Add(o) 93 paint.FillShape(o, red, clip.Rect(image.Rect(10, 10, 30, 30)).Op()) 94 a = f32.Affine2D{}.Rotate(f32.Pt(20, 20), -math.Pi/4) 95 op.Affine(a).Add(o) 96 97 paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 50, 50)).Op()) 98 }, func(r result) { 99 r.expect(1, 1, colornames.Black) 100 r.expect(20, 20, colornames.Black) 101 r.expect(49, 49, colornames.Black) 102 r.expect(51, 51, transparent) 103 }) 104 } 105 106 func TestDeferredPaint(t *testing.T) { 107 run(t, func(o *op.Ops) { 108 state := op.Save(o) 109 clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o) 110 paint.ColorOp{Color: color.NRGBA{A: 0xff, G: 0xff}}.Add(o) 111 paint.PaintOp{}.Add(o) 112 113 op.Affine(f32.Affine2D{}.Offset(f32.Pt(20, 20))).Add(o) 114 m := op.Record(o) 115 clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o) 116 paint.ColorOp{Color: color.NRGBA{A: 0xff, R: 0xff, G: 0xff}}.Add(o) 117 paint.PaintOp{}.Add(o) 118 paintMacro := m.Stop() 119 op.Defer(o, paintMacro) 120 121 state.Load() 122 op.Affine(f32.Affine2D{}.Offset(f32.Pt(10, 10))).Add(o) 123 clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o) 124 paint.ColorOp{Color: color.NRGBA{A: 0xff, B: 0xff}}.Add(o) 125 paint.PaintOp{}.Add(o) 126 }, func(r result) { 127 }) 128 } 129 130 func constSqPath() op.CallOp { 131 innerOps := new(op.Ops) 132 m := op.Record(innerOps) 133 builder := clip.Path{} 134 builder.Begin(innerOps) 135 builder.Move(f32.Pt(0, 0)) 136 builder.Line(f32.Pt(10, 0)) 137 builder.Line(f32.Pt(0, 10)) 138 builder.Line(f32.Pt(-10, 0)) 139 builder.Line(f32.Pt(0, -10)) 140 p := builder.End() 141 clip.Outline{Path: p}.Op().Add(innerOps) 142 return m.Stop() 143 } 144 145 func constSqCirc() op.CallOp { 146 innerOps := new(op.Ops) 147 m := op.Record(innerOps) 148 clip.RRect{Rect: f32.Rect(0, 0, 40, 40), 149 NW: 20, NE: 20, SW: 20, SE: 20}.Add(innerOps) 150 return m.Stop() 151 } 152 153 func drawChild(ops *op.Ops, text op.CallOp) op.CallOp { 154 r1 := op.Record(ops) 155 text.Add(ops) 156 paint.PaintOp{}.Add(ops) 157 return r1.Stop() 158 } 159 160 func TestReuseStencil(t *testing.T) { 161 txt := constSqPath() 162 run(t, func(ops *op.Ops) { 163 c1 := drawChild(ops, txt) 164 c2 := drawChild(ops, txt) 165 166 // lay out the children 167 stack1 := op.Save(ops) 168 c1.Add(ops) 169 stack1.Load() 170 171 stack2 := op.Save(ops) 172 op.Offset(f32.Pt(0, 50)).Add(ops) 173 c2.Add(ops) 174 stack2.Load() 175 }, func(r result) { 176 r.expect(5, 5, colornames.Black) 177 r.expect(5, 55, colornames.Black) 178 }) 179 } 180 181 func TestBuildOffscreen(t *testing.T) { 182 // Check that something we in one frame build outside the screen 183 // still is rendered correctly if moved into the screen in a later 184 // frame. 185 186 txt := constSqCirc() 187 draw := func(off float32, o *op.Ops) { 188 s := op.Save(o) 189 op.Offset(f32.Pt(0, off)).Add(o) 190 txt.Add(o) 191 paint.PaintOp{}.Add(o) 192 s.Load() 193 } 194 195 multiRun(t, 196 frame( 197 func(ops *op.Ops) { 198 draw(-100, ops) 199 }, func(r result) { 200 r.expect(5, 5, transparent) 201 r.expect(20, 20, transparent) 202 }), 203 frame( 204 func(ops *op.Ops) { 205 draw(0, ops) 206 }, func(r result) { 207 r.expect(2, 2, transparent) 208 r.expect(20, 20, colornames.Black) 209 r.expect(38, 38, transparent) 210 })) 211 } 212 213 func TestNegativeOverlaps(t *testing.T) { 214 run(t, func(ops *op.Ops) { 215 clip.RRect{Rect: f32.Rect(50, 50, 100, 100)}.Add(ops) 216 clip.Rect(image.Rect(0, 120, 100, 122)).Add(ops) 217 paint.PaintOp{}.Add(ops) 218 }, func(r result) { 219 r.expect(60, 60, transparent) 220 r.expect(60, 110, transparent) 221 r.expect(60, 120, transparent) 222 r.expect(60, 122, transparent) 223 }) 224 } 225 226 func TestDepthOverlap(t *testing.T) { 227 run(t, func(ops *op.Ops) { 228 stack := op.Save(ops) 229 paint.FillShape(ops, red, clip.Rect{Max: image.Pt(128, 64)}.Op()) 230 stack.Load() 231 232 stack = op.Save(ops) 233 paint.FillShape(ops, green, clip.Rect{Max: image.Pt(64, 128)}.Op()) 234 stack.Load() 235 }, func(r result) { 236 r.expect(96, 32, colornames.Red) 237 r.expect(32, 96, colornames.Green) 238 r.expect(32, 32, colornames.Green) 239 }) 240 } 241 242 type Gradient struct { 243 From, To color.NRGBA 244 } 245 246 var gradients = []Gradient{ 247 {From: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}}, 248 {From: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}}, 249 {From: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}}, 250 {From: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}}, 251 {From: color.NRGBA{R: 0x19, G: 0xFF, B: 0xFF, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}}, 252 {From: color.NRGBA{R: 0xFF, G: 0xFF, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}}, 253 } 254 255 func TestLinearGradient(t *testing.T) { 256 t.Skip("linear gradients don't support transformations") 257 258 const gradienth = 8 259 // 0.5 offset from ends to ensure that the center of the pixel 260 // aligns with gradient from and to colors. 261 pixelAligned := f32.Rect(0.5, 0, 127.5, gradienth) 262 samples := []int{0, 12, 32, 64, 96, 115, 127} 263 264 run(t, func(ops *op.Ops) { 265 gr := f32.Rect(0, 0, 128, gradienth) 266 for _, g := range gradients { 267 paint.LinearGradientOp{ 268 Stop1: f32.Pt(gr.Min.X, gr.Min.Y), 269 Color1: g.From, 270 Stop2: f32.Pt(gr.Max.X, gr.Min.Y), 271 Color2: g.To, 272 }.Add(ops) 273 st := op.Save(ops) 274 clip.RRect{Rect: gr}.Add(ops) 275 op.Affine(f32.Affine2D{}.Offset(pixelAligned.Min)).Add(ops) 276 scale(pixelAligned.Dx()/128, 1).Add(ops) 277 paint.PaintOp{}.Add(ops) 278 st.Load() 279 gr = gr.Add(f32.Pt(0, gradienth)) 280 } 281 }, func(r result) { 282 gr := pixelAligned 283 for _, g := range gradients { 284 from := f32color.LinearFromSRGB(g.From) 285 to := f32color.LinearFromSRGB(g.To) 286 for _, p := range samples { 287 exp := lerp(from, to, float32(p)/float32(r.img.Bounds().Dx()-1)) 288 r.expect(p, int(gr.Min.Y+gradienth/2), f32color.NRGBAToRGBA(exp.SRGB())) 289 } 290 gr = gr.Add(f32.Pt(0, gradienth)) 291 } 292 }) 293 } 294 295 func TestLinearGradientAngled(t *testing.T) { 296 run(t, func(ops *op.Ops) { 297 paint.LinearGradientOp{ 298 Stop1: f32.Pt(64, 64), 299 Color1: black, 300 Stop2: f32.Pt(0, 0), 301 Color2: red, 302 }.Add(ops) 303 st := op.Save(ops) 304 clip.Rect(image.Rect(0, 0, 64, 64)).Add(ops) 305 paint.PaintOp{}.Add(ops) 306 st.Load() 307 308 paint.LinearGradientOp{ 309 Stop1: f32.Pt(64, 64), 310 Color1: white, 311 Stop2: f32.Pt(128, 0), 312 Color2: green, 313 }.Add(ops) 314 st = op.Save(ops) 315 clip.Rect(image.Rect(64, 0, 128, 64)).Add(ops) 316 paint.PaintOp{}.Add(ops) 317 st.Load() 318 319 paint.LinearGradientOp{ 320 Stop1: f32.Pt(64, 64), 321 Color1: black, 322 Stop2: f32.Pt(128, 128), 323 Color2: blue, 324 }.Add(ops) 325 st = op.Save(ops) 326 clip.Rect(image.Rect(64, 64, 128, 128)).Add(ops) 327 paint.PaintOp{}.Add(ops) 328 st.Load() 329 330 paint.LinearGradientOp{ 331 Stop1: f32.Pt(64, 64), 332 Color1: white, 333 Stop2: f32.Pt(0, 128), 334 Color2: magenta, 335 }.Add(ops) 336 st = op.Save(ops) 337 clip.Rect(image.Rect(0, 64, 64, 128)).Add(ops) 338 paint.PaintOp{}.Add(ops) 339 st.Load() 340 }, func(r result) {}) 341 } 342 343 func TestZeroImage(t *testing.T) { 344 ops := new(op.Ops) 345 w := newWindow(t, 10, 10) 346 paint.ImageOp{}.Add(ops) 347 paint.PaintOp{}.Add(ops) 348 if err := w.Frame(ops); err != nil { 349 t.Error(err) 350 } 351 } 352 353 // lerp calculates linear interpolation with color b and p. 354 func lerp(a, b f32color.RGBA, p float32) f32color.RGBA { 355 return f32color.RGBA{ 356 R: a.R*(1-p) + b.R*p, 357 G: a.G*(1-p) + b.G*p, 358 B: a.B*(1-p) + b.B*p, 359 A: a.A*(1-p) + b.A*p, 360 } 361 }