Files
gio/internal/rendertest/render_test.go
T
Elias Naur 94d242d18c op/paint: remove support for PaintOp.Rect
PaintOp.Rect is the wrong abstraction; it implies a clip operation
better handled by package clip, and not all paints need it (colors).
Furthermore, it's awkward to specify a PaintOp that fills up the
current clip area, regardless of its size.

Redefine PathOp to mean "fill current clip area".

API change. Replace uses of PaintOp.Rect with a TransformOp applied
before the PaintOp.

Leave a TODO for the PathOp infinity area.

Fixes gio#167

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2020-11-05 16:32:19 +01:00

307 lines
7.7 KiB
Go

package rendertest
import (
"image"
"image/color"
"math"
"testing"
"gioui.org/f32"
"gioui.org/internal/f32color"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"golang.org/x/image/colornames"
)
func TestTransformMacro(t *testing.T) {
// testcase resulting from original bug when rendering layout.Stacked
// Build clip-path.
c := constSqPath()
run(t, func(o *op.Ops) {
// render the first Stacked item
m1 := op.Record(o)
dr := image.Rect(0, 0, 128, 50)
paint.FillShape(o, colornames.Black, clip.Rect(dr).Op())
c1 := m1.Stop()
// Render the second stacked item
m2 := op.Record(o)
paint.ColorOp{Color: colornames.Red}.Add(o)
// Simulate a draw text call
stack := op.Push(o)
op.Offset(f32.Pt(0, 10)).Add(o)
// Apply the clip-path.
c.Add(o)
paint.PaintOp{}.Add(o)
stack.Pop()
c2 := m2.Stop()
// Call each of them in a transform
s1 := op.Push(o)
op.Offset(f32.Pt(0, 0)).Add(o)
c1.Add(o)
s1.Pop()
s2 := op.Push(o)
op.Offset(f32.Pt(0, 0)).Add(o)
c2.Add(o)
s2.Pop()
}, func(r result) {
r.expect(5, 15, colornames.Red)
r.expect(15, 15, colornames.Black)
r.expect(11, 51, colornames.White)
})
}
func TestRepeatedPaintsZ(t *testing.T) {
run(t, func(o *op.Ops) {
// Draw a rectangle
paint.FillShape(o, colornames.Black, clip.Rect(image.Rect(0, 0, 128, 50)).Op())
builder := clip.Path{}
builder.Begin(o)
builder.Move(f32.Pt(0, 0))
builder.Line(f32.Pt(10, 0))
builder.Line(f32.Pt(0, 10))
builder.Line(f32.Pt(-10, 0))
builder.Line(f32.Pt(0, -10))
builder.End().Add(o)
paint.Fill(o, colornames.Red)
}, func(r result) {
r.expect(5, 5, colornames.Red)
r.expect(11, 15, colornames.Black)
r.expect(11, 51, colornames.White)
})
}
func TestNoClipFromPaint(t *testing.T) {
// ensure that a paint operation does not polute the state
// by leaving any clip paths in place.
run(t, func(o *op.Ops) {
a := f32.Affine2D{}.Rotate(f32.Pt(20, 20), math.Pi/4)
op.Affine(a).Add(o)
paint.FillShape(o, colornames.Red, clip.Rect(image.Rect(10, 10, 30, 30)).Op())
a = f32.Affine2D{}.Rotate(f32.Pt(20, 20), -math.Pi/4)
op.Affine(a).Add(o)
paint.FillShape(o, colornames.Black, clip.Rect(image.Rect(0, 0, 50, 50)).Op())
}, func(r result) {
r.expect(1, 1, colornames.Black)
r.expect(20, 20, colornames.Black)
r.expect(49, 49, colornames.Black)
r.expect(51, 51, colornames.White)
})
}
func constSqPath() op.CallOp {
innerOps := new(op.Ops)
m := op.Record(innerOps)
builder := clip.Path{}
builder.Begin(innerOps)
builder.Move(f32.Pt(0, 0))
builder.Line(f32.Pt(10, 0))
builder.Line(f32.Pt(0, 10))
builder.Line(f32.Pt(-10, 0))
builder.Line(f32.Pt(0, -10))
builder.End().Add(innerOps)
return m.Stop()
}
func constSqCirc() op.CallOp {
innerOps := new(op.Ops)
m := op.Record(innerOps)
clip.RRect{Rect: f32.Rect(0, 0, 40, 40),
NW: 20, NE: 20, SW: 20, SE: 20}.Add(innerOps)
return m.Stop()
}
func drawChild(ops *op.Ops, text op.CallOp) op.CallOp {
r1 := op.Record(ops)
text.Add(ops)
paint.PaintOp{}.Add(ops)
return r1.Stop()
}
func TestReuseStencil(t *testing.T) {
txt := constSqPath()
run(t, func(ops *op.Ops) {
c1 := drawChild(ops, txt)
c2 := drawChild(ops, txt)
// lay out the children
stack1 := op.Push(ops)
c1.Add(ops)
stack1.Pop()
stack2 := op.Push(ops)
op.Offset(f32.Pt(0, 50)).Add(ops)
c2.Add(ops)
stack2.Pop()
}, func(r result) {
r.expect(5, 5, colornames.Black)
r.expect(5, 55, colornames.Black)
})
}
func TestBuildOffscreen(t *testing.T) {
// Check that something we in one frame build outside the screen
// still is rendered correctly if moved into the screen in a later
// frame.
txt := constSqCirc()
draw := func(off float32, o *op.Ops) {
s := op.Push(o)
op.Offset(f32.Pt(0, off)).Add(o)
txt.Add(o)
paint.PaintOp{}.Add(o)
s.Pop()
}
multiRun(t,
frame(
func(ops *op.Ops) {
draw(-100, ops)
}, func(r result) {
r.expect(5, 5, colornames.White)
r.expect(20, 20, colornames.White)
}),
frame(
func(ops *op.Ops) {
draw(0, ops)
}, func(r result) {
r.expect(2, 2, colornames.White)
r.expect(20, 20, colornames.Black)
r.expect(38, 38, colornames.White)
}))
}
func TestNegativeOverlaps(t *testing.T) {
run(t, func(ops *op.Ops) {
clip.RRect{Rect: f32.Rect(50, 50, 100, 100)}.Add(ops)
clip.Rect(image.Rect(0, 120, 100, 122)).Add(ops)
paint.PaintOp{}.Add(ops)
}, func(r result) {
r.expect(60, 60, colornames.White)
r.expect(60, 110, colornames.White)
r.expect(60, 120, colornames.White)
r.expect(60, 122, colornames.White)
})
}
type Gradient struct {
From, To color.RGBA
}
var gradients = []Gradient{
{From: color.RGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF}, To: color.RGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}},
{From: color.RGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}, To: color.RGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}},
{From: color.RGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}, To: color.RGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}},
{From: color.RGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}, To: color.RGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}},
{From: color.RGBA{R: 0x19, G: 0xFF, B: 0xFF, A: 0xFF}, To: color.RGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}},
{From: color.RGBA{R: 0xFF, G: 0xFF, B: 0x19, A: 0xFF}, To: color.RGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}},
}
func TestLinearGradient(t *testing.T) {
t.Skip("linear gradients don't support transformations")
const gradienth = 8
// 0.5 offset from ends to ensure that the center of the pixel
// aligns with gradient from and to colors.
pixelAligned := f32.Rect(0.5, 0, 127.5, gradienth)
samples := []int{0, 12, 32, 64, 96, 115, 127}
run(t, func(ops *op.Ops) {
gr := f32.Rect(0, 0, 128, gradienth)
for _, g := range gradients {
paint.LinearGradientOp{
Stop1: f32.Pt(gr.Min.X, gr.Min.Y),
Color1: g.From,
Stop2: f32.Pt(gr.Max.X, gr.Min.Y),
Color2: g.To,
}.Add(ops)
st := op.Push(ops)
clip.RRect{Rect: gr}.Add(ops)
op.Affine(f32.Affine2D{}.Offset(pixelAligned.Min)).Add(ops)
scale(pixelAligned.Dx()/128, 1).Add(ops)
paint.PaintOp{}.Add(ops)
st.Pop()
gr = gr.Add(f32.Pt(0, gradienth))
}
}, func(r result) {
gr := pixelAligned
for _, g := range gradients {
from := f32color.RGBAFromSRGB(g.From)
to := f32color.RGBAFromSRGB(g.To)
for _, p := range samples {
exp := lerp(from, to, float32(p)/float32(r.img.Bounds().Dx()-1))
r.expect(p, int(gr.Min.Y+gradienth/2), exp.SRGB())
}
gr = gr.Add(f32.Pt(0, gradienth))
}
})
}
func TestLinearGradientAngled(t *testing.T) {
run(t, func(ops *op.Ops) {
paint.LinearGradientOp{
Stop1: f32.Pt(64, 64),
Color1: colornames.Black,
Stop2: f32.Pt(0, 0),
Color2: colornames.Red,
}.Add(ops)
st := op.Push(ops)
clip.Rect(image.Rect(0, 0, 64, 64)).Add(ops)
paint.PaintOp{}.Add(ops)
st.Pop()
paint.LinearGradientOp{
Stop1: f32.Pt(64, 64),
Color1: colornames.White,
Stop2: f32.Pt(128, 0),
Color2: colornames.Green,
}.Add(ops)
st = op.Push(ops)
clip.Rect(image.Rect(64, 0, 128, 64)).Add(ops)
paint.PaintOp{}.Add(ops)
st.Pop()
paint.LinearGradientOp{
Stop1: f32.Pt(64, 64),
Color1: colornames.Black,
Stop2: f32.Pt(128, 128),
Color2: colornames.Blue,
}.Add(ops)
st = op.Push(ops)
clip.Rect(image.Rect(64, 64, 128, 128)).Add(ops)
paint.PaintOp{}.Add(ops)
st.Pop()
paint.LinearGradientOp{
Stop1: f32.Pt(64, 64),
Color1: colornames.White,
Stop2: f32.Pt(0, 128),
Color2: colornames.Magenta,
}.Add(ops)
st = op.Push(ops)
clip.Rect(image.Rect(0, 64, 64, 128)).Add(ops)
paint.PaintOp{}.Add(ops)
st.Pop()
}, func(r result) {})
}
// lerp calculates linear interpolation with color b and p.
func lerp(a, b f32color.RGBA, p float32) f32color.RGBA {
return f32color.RGBA{
R: a.R*(1-p) + b.R*p,
G: a.G*(1-p) + b.G*p,
B: a.B*(1-p) + b.B*p,
A: a.A*(1-p) + b.A*p,
}
}