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  }