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