render_test.go raw

   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/p9c/p9/pkg/gel/gio/f32"
  12  	"github.com/p9c/p9/pkg/gel/gio/internal/f32color"
  13  	"github.com/p9c/p9/pkg/gel/gio/op"
  14  	"github.com/p9c/p9/pkg/gel/gio/op/clip"
  15  	"github.com/p9c/p9/pkg/gel/gio/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  // lerp calculates linear interpolation with color b and p.
 344  func lerp(a, b f32color.RGBA, p float32) f32color.RGBA {
 345  	return f32color.RGBA{
 346  		R: a.R*(1-p) + b.R*p,
 347  		G: a.G*(1-p) + b.G*p,
 348  		B: a.B*(1-p) + b.B*p,
 349  		A: a.A*(1-p) + b.A*p,
 350  	}
 351  }
 352