gpu/internal/rendertest: move rendertest package below gpu
Signed-off-by: Elias Naur <mail@eliasnaur.com>
@@ -0,0 +1,311 @@
|
||||
package rendertest
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/font/gofont"
|
||||
"gioui.org/gpu/headless"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/widget/material"
|
||||
)
|
||||
|
||||
// use some global variables for benchmarking so as to not pollute
|
||||
// the reported allocs with allocations that we do not want to count.
|
||||
var (
|
||||
c1, c2, c3 = make(chan op.CallOp), make(chan op.CallOp), make(chan op.CallOp)
|
||||
op1, op2, op3 op.Ops
|
||||
)
|
||||
|
||||
func setupBenchmark(b *testing.B) (layout.Context, *headless.Window, *material.Theme) {
|
||||
sz := image.Point{X: 1024, Y: 1200}
|
||||
w := newWindow(b, sz.X, sz.Y)
|
||||
ops := new(op.Ops)
|
||||
gtx := layout.Context{
|
||||
Ops: ops,
|
||||
Constraints: layout.Exact(sz),
|
||||
}
|
||||
th := material.NewTheme(gofont.Collection())
|
||||
return gtx, w, th
|
||||
}
|
||||
|
||||
func resetOps(gtx layout.Context) {
|
||||
gtx.Ops.Reset()
|
||||
op1.Reset()
|
||||
op2.Reset()
|
||||
op3.Reset()
|
||||
}
|
||||
|
||||
func finishBenchmark(b *testing.B, w *headless.Window) {
|
||||
b.StopTimer()
|
||||
if *dumpImages {
|
||||
img, err := w.Screenshot()
|
||||
w.Release()
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
if err := saveImage(b.Name()+".png", img); err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDrawUICached(b *testing.B) {
|
||||
// As BenchmarkDraw but the same op.Ops every time that is not reset - this
|
||||
// should thus allow for maximal cache usage.
|
||||
gtx, w, th := setupBenchmark(b)
|
||||
drawCore(gtx, th)
|
||||
w.Frame(gtx.Ops)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
w.Frame(gtx.Ops)
|
||||
}
|
||||
finishBenchmark(b, w)
|
||||
}
|
||||
|
||||
func BenchmarkDrawUI(b *testing.B) {
|
||||
// BenchmarkDraw is intended as a reasonable overall benchmark for
|
||||
// the drawing performance of the full drawing pipeline, in each iteration
|
||||
// resetting the ops and drawing, similar to how a typical UI would function.
|
||||
// This will allow font caching across frames.
|
||||
gtx, w, th := setupBenchmark(b)
|
||||
drawCore(gtx, th)
|
||||
w.Frame(gtx.Ops)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
resetOps(gtx)
|
||||
|
||||
p := op.Save(gtx.Ops)
|
||||
off := float32(math.Mod(float64(i)/10, 10))
|
||||
op.Offset(f32.Pt(off, off)).Add(gtx.Ops)
|
||||
|
||||
drawCore(gtx, th)
|
||||
|
||||
p.Load()
|
||||
w.Frame(gtx.Ops)
|
||||
}
|
||||
finishBenchmark(b, w)
|
||||
}
|
||||
|
||||
func BenchmarkDrawUITransformed(b *testing.B) {
|
||||
// Like BenchmarkDraw UI but transformed at every frame
|
||||
gtx, w, th := setupBenchmark(b)
|
||||
drawCore(gtx, th)
|
||||
w.Frame(gtx.Ops)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
resetOps(gtx)
|
||||
|
||||
p := op.Save(gtx.Ops)
|
||||
angle := float32(math.Mod(float64(i)/1000, 0.05))
|
||||
a := f32.Affine2D{}.Shear(f32.Point{}, angle, angle).Rotate(f32.Point{}, angle)
|
||||
op.Affine(a).Add(gtx.Ops)
|
||||
|
||||
drawCore(gtx, th)
|
||||
|
||||
p.Load()
|
||||
w.Frame(gtx.Ops)
|
||||
}
|
||||
finishBenchmark(b, w)
|
||||
}
|
||||
|
||||
func Benchmark1000Circles(b *testing.B) {
|
||||
// Benchmark1000Shapes draws 1000 individual shapes such that no caching between
|
||||
// shapes will be possible and resets buffers on each operation to prevent caching
|
||||
// between frames.
|
||||
gtx, w, _ := setupBenchmark(b)
|
||||
draw1000Circles(gtx)
|
||||
w.Frame(gtx.Ops)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
resetOps(gtx)
|
||||
draw1000Circles(gtx)
|
||||
w.Frame(gtx.Ops)
|
||||
}
|
||||
finishBenchmark(b, w)
|
||||
}
|
||||
|
||||
func Benchmark1000CirclesInstanced(b *testing.B) {
|
||||
// Like Benchmark1000Circles but will record them and thus allow for caching between
|
||||
// them.
|
||||
gtx, w, _ := setupBenchmark(b)
|
||||
draw1000CirclesInstanced(gtx)
|
||||
w.Frame(gtx.Ops)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
resetOps(gtx)
|
||||
draw1000CirclesInstanced(gtx)
|
||||
w.Frame(gtx.Ops)
|
||||
}
|
||||
finishBenchmark(b, w)
|
||||
}
|
||||
|
||||
func draw1000Circles(gtx layout.Context) {
|
||||
ops := gtx.Ops
|
||||
for x := 0; x < 100; x++ {
|
||||
p := op.Save(ops)
|
||||
op.Offset(f32.Pt(float32(x*10), 0)).Add(ops)
|
||||
for y := 0; y < 10; y++ {
|
||||
paint.FillShape(ops,
|
||||
color.NRGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100, A: 120},
|
||||
clip.RRect{Rect: f32.Rect(0, 0, 10, 10), NE: 5, SE: 5, SW: 5, NW: 5}.Op(ops),
|
||||
)
|
||||
op.Offset(f32.Pt(0, float32(100))).Add(ops)
|
||||
}
|
||||
p.Load()
|
||||
}
|
||||
}
|
||||
|
||||
func draw1000CirclesInstanced(gtx layout.Context) {
|
||||
ops := gtx.Ops
|
||||
|
||||
r := op.Record(ops)
|
||||
clip.RRect{Rect: f32.Rect(0, 0, 10, 10), NE: 5, SE: 5, SW: 5, NW: 5}.Add(ops)
|
||||
paint.PaintOp{}.Add(ops)
|
||||
c := r.Stop()
|
||||
|
||||
for x := 0; x < 100; x++ {
|
||||
p := op.Save(ops)
|
||||
op.Offset(f32.Pt(float32(x*10), 0)).Add(ops)
|
||||
for y := 0; y < 10; y++ {
|
||||
pi := op.Save(ops)
|
||||
paint.ColorOp{Color: color.NRGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100, A: 120}}.Add(ops)
|
||||
c.Add(ops)
|
||||
pi.Load()
|
||||
op.Offset(f32.Pt(0, float32(100))).Add(ops)
|
||||
}
|
||||
p.Load()
|
||||
}
|
||||
}
|
||||
|
||||
func drawCore(gtx layout.Context, th *material.Theme) {
|
||||
c1 := drawIndividualShapes(gtx, th)
|
||||
c2 := drawShapeInstances(gtx, th)
|
||||
c3 := drawText(gtx, th)
|
||||
|
||||
(<-c1).Add(gtx.Ops)
|
||||
(<-c2).Add(gtx.Ops)
|
||||
(<-c3).Add(gtx.Ops)
|
||||
}
|
||||
|
||||
func drawIndividualShapes(gtx layout.Context, th *material.Theme) chan op.CallOp {
|
||||
// draw 81 rounded rectangles of different solid colors - each one individually
|
||||
go func() {
|
||||
ops := &op1
|
||||
c := op.Record(ops)
|
||||
for x := 0; x < 9; x++ {
|
||||
p := op.Save(ops)
|
||||
op.Offset(f32.Pt(float32(x*50), 0)).Add(ops)
|
||||
for y := 0; y < 9; y++ {
|
||||
paint.FillShape(ops,
|
||||
color.NRGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100, A: 120},
|
||||
clip.RRect{Rect: f32.Rect(0, 0, 25, 25), NE: 10, SE: 10, SW: 10, NW: 10}.Op(ops),
|
||||
)
|
||||
op.Offset(f32.Pt(0, float32(50))).Add(ops)
|
||||
}
|
||||
p.Load()
|
||||
}
|
||||
c1 <- c.Stop()
|
||||
}()
|
||||
return c1
|
||||
}
|
||||
|
||||
func drawShapeInstances(gtx layout.Context, th *material.Theme) chan op.CallOp {
|
||||
// draw 400 textured circle instances, each with individual transform
|
||||
go func() {
|
||||
ops := &op2
|
||||
co := op.Record(ops)
|
||||
|
||||
r := op.Record(ops)
|
||||
clip.RRect{Rect: f32.Rect(0, 0, 25, 25), NE: 10, SE: 10, SW: 10, NW: 10}.Add(ops)
|
||||
paint.PaintOp{}.Add(ops)
|
||||
c := r.Stop()
|
||||
|
||||
squares.Add(ops)
|
||||
rad := float32(0)
|
||||
for x := 0; x < 20; x++ {
|
||||
for y := 0; y < 20; y++ {
|
||||
p := op.Save(ops)
|
||||
op.Offset(f32.Pt(float32(x*50+25), float32(y*50+25))).Add(ops)
|
||||
c.Add(ops)
|
||||
p.Load()
|
||||
rad += math.Pi * 2 / 400
|
||||
}
|
||||
}
|
||||
c2 <- co.Stop()
|
||||
}()
|
||||
return c2
|
||||
}
|
||||
|
||||
func drawText(gtx layout.Context, th *material.Theme) chan op.CallOp {
|
||||
// draw 40 lines of text with different transforms.
|
||||
go func() {
|
||||
ops := &op3
|
||||
c := op.Record(ops)
|
||||
|
||||
txt := material.H6(th, "")
|
||||
for x := 0; x < 40; x++ {
|
||||
txt.Text = textRows[x]
|
||||
p := op.Save(ops)
|
||||
op.Offset(f32.Pt(float32(0), float32(24*x))).Add(ops)
|
||||
gtx.Ops = ops
|
||||
txt.Layout(gtx)
|
||||
p.Load()
|
||||
}
|
||||
c3 <- c.Stop()
|
||||
}()
|
||||
return c3
|
||||
}
|
||||
|
||||
var textRows = []string{
|
||||
"1. I learned from my grandfather, Verus, to use good manners, and to",
|
||||
"put restraint on anger. 2. In the famous memory of my father I had a",
|
||||
"pattern of modesty and manliness. 3. Of my mother I learned to be",
|
||||
"pious and generous; to keep myself not only from evil deeds, but even",
|
||||
"from evil thoughts; and to live with a simplicity which is far from",
|
||||
"customary among the rich. 4. I owe it to my great-grandfather that I",
|
||||
"did not attend public lectures and discussions, but had good and able",
|
||||
"teachers at home; and I owe him also the knowledge that for things of",
|
||||
"this nature a man should count no expense too great.",
|
||||
"5. My tutor taught me not to favour either green or blue at the",
|
||||
"chariot races, nor, in the contests of gladiators, to be a supporter",
|
||||
"either of light or heavy armed. He taught me also to endure labour;",
|
||||
"not to need many things; to serve myself without troubling others; not",
|
||||
"to intermeddle in the affairs of others, and not easily to listen to",
|
||||
"slanders against them.",
|
||||
"6. Of Diognetus I had the lesson not to busy myself about vain things;",
|
||||
"not to credit the great professions of such as pretend to work",
|
||||
"wonders, or of sorcerers about their charms, and their expelling of",
|
||||
"Demons and the like; not to keep quails (for fighting or divination),",
|
||||
"nor to run after such things; to suffer freedom of speech in others,",
|
||||
"and to apply myself heartily to philosophy. Him also I must thank for",
|
||||
"my hearing first Bacchius, then Tandasis and Marcianus; that I wrote",
|
||||
"dialogues in my youth, and took a liking to the philosopher's pallet",
|
||||
"and skins, and to the other things which, by the Grecian discipline,",
|
||||
"belong to that profession.",
|
||||
"7. To Rusticus I owe my first apprehensions that my nature needed",
|
||||
"reform and cure; and that I did not fall into the ambition of the",
|
||||
"common Sophists, either by composing speculative writings or by",
|
||||
"declaiming harangues of exhortation in public; further, that I never",
|
||||
"strove to be admired by ostentation of great patience in an ascetic",
|
||||
"life, or by display of activity and application; that I gave over the",
|
||||
"study of rhetoric, poetry, and the graces of language; and that I did",
|
||||
"not pace my house in my senatorial robes, or practise any similar",
|
||||
"affectation. I observed also the simplicity of style in his letters,",
|
||||
"particularly in that which he wrote to my mother from Sinuessa. I",
|
||||
"learned from him to be easily appeased, and to be readily reconciled",
|
||||
"with those who had displeased me or given cause of offence, so soon as",
|
||||
"they inclined to make their peace; to read with care; not to rest",
|
||||
"satisfied with a slight and superficial knowledge; nor quickly to",
|
||||
"assent to great talkers. I have him to thank that I met with the",
|
||||
}
|
||||
@@ -0,0 +1,548 @@
|
||||
package rendertest
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/image/colornames"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
)
|
||||
|
||||
func TestPaintRect(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op())
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.Red)
|
||||
r.expect(49, 0, colornames.Red)
|
||||
r.expect(50, 0, colornames.White)
|
||||
r.expect(10, 50, colornames.White)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaintClippedRect(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
clip.RRect{Rect: f32.Rect(25, 25, 60, 60)}.Add(o)
|
||||
paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op())
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.White)
|
||||
r.expect(24, 35, colornames.White)
|
||||
r.expect(25, 35, colornames.Red)
|
||||
r.expect(50, 0, colornames.White)
|
||||
r.expect(10, 50, colornames.White)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaintClippedBorder(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
var dashes clip.Dash
|
||||
dashes.Begin(o)
|
||||
dashes.Phase(1)
|
||||
dashes.Dash(2)
|
||||
dashes.Dash(1)
|
||||
|
||||
clip.Border{
|
||||
Rect: f32.Rect(25, 25, 60, 60),
|
||||
Width: 4,
|
||||
Dashes: dashes.End(),
|
||||
}.Add(o)
|
||||
paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op())
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.White)
|
||||
r.expect(25, 25, colornames.Red)
|
||||
r.expect(50, 0, colornames.White)
|
||||
r.expect(10, 50, colornames.White)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaintClippedCircle(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
r := float32(10)
|
||||
clip.RRect{Rect: f32.Rect(20, 20, 40, 40), SE: r, SW: r, NW: r, NE: r}.Add(o)
|
||||
clip.Rect(image.Rect(0, 0, 30, 50)).Add(o)
|
||||
paint.Fill(o, red)
|
||||
}, func(r result) {
|
||||
r.expect(21, 21, colornames.White)
|
||||
r.expect(25, 30, colornames.Red)
|
||||
r.expect(31, 30, colornames.White)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaintArc(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
p := new(clip.Path)
|
||||
p.Begin(o)
|
||||
p.Move(f32.Pt(0, 20))
|
||||
p.Line(f32.Pt(10, 0))
|
||||
p.Arc(f32.Pt(10, 0), f32.Pt(40, 0), math.Pi)
|
||||
p.Line(f32.Pt(30, 0))
|
||||
p.Line(f32.Pt(0, 25))
|
||||
p.Arc(f32.Pt(-10, 5), f32.Pt(10, 15), -math.Pi)
|
||||
p.Line(f32.Pt(0, 25))
|
||||
p.Arc(f32.Pt(10, 10), f32.Pt(10, 10), 2*math.Pi)
|
||||
p.Line(f32.Pt(-10, 0))
|
||||
p.Arc(f32.Pt(-10, 0), f32.Pt(-40, 0), -math.Pi)
|
||||
p.Line(f32.Pt(-10, 0))
|
||||
p.Line(f32.Pt(0, -10))
|
||||
p.Arc(f32.Pt(-10, -20), f32.Pt(10, -5), math.Pi)
|
||||
p.Line(f32.Pt(0, -10))
|
||||
p.Line(f32.Pt(-50, 0))
|
||||
clip.Outline{
|
||||
Path: p.End(),
|
||||
}.Op().Add(o)
|
||||
|
||||
paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 128, 128)).Op())
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.White)
|
||||
r.expect(0, 25, colornames.Red)
|
||||
r.expect(0, 15, colornames.White)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaintAbsolute(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
p := new(clip.Path)
|
||||
p.Begin(o)
|
||||
p.Move(f32.Pt(100, 100)) // offset the initial pen position to test "MoveTo"
|
||||
|
||||
p.MoveTo(f32.Pt(20, 20))
|
||||
p.LineTo(f32.Pt(80, 20))
|
||||
p.QuadTo(f32.Pt(80, 80), f32.Pt(20, 80))
|
||||
clip.Outline{
|
||||
Path: p.End(),
|
||||
}.Op().Add(o)
|
||||
|
||||
paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 128, 128)).Op())
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.White)
|
||||
r.expect(30, 30, colornames.Red)
|
||||
r.expect(79, 79, colornames.White)
|
||||
r.expect(90, 90, colornames.White)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaintTexture(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
squares.Add(o)
|
||||
scale(80.0/512, 80.0/512).Add(o)
|
||||
paint.PaintOp{}.Add(o)
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.Blue)
|
||||
r.expect(79, 10, colornames.Green)
|
||||
r.expect(80, 0, colornames.White)
|
||||
r.expect(10, 80, colornames.White)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaintClippedTexture(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
squares.Add(o)
|
||||
clip.RRect{Rect: f32.Rect(0, 0, 40, 40)}.Add(o)
|
||||
scale(80.0/512, 80.0/512).Add(o)
|
||||
paint.PaintOp{}.Add(o)
|
||||
}, func(r result) {
|
||||
r.expect(40, 40, colornames.White)
|
||||
r.expect(25, 35, colornames.Blue)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStrokedPathBevelFlat(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
p := newStrokedPath(o)
|
||||
clip.Stroke{
|
||||
Path: p,
|
||||
Style: clip.StrokeStyle{
|
||||
Width: 2.5,
|
||||
Cap: clip.FlatCap,
|
||||
Join: clip.BevelJoin,
|
||||
},
|
||||
}.Op().Add(o)
|
||||
|
||||
paint.Fill(o, red)
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.White)
|
||||
r.expect(10, 50, colornames.Red)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStrokedPathBevelRound(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
p := newStrokedPath(o)
|
||||
clip.Stroke{
|
||||
Path: p,
|
||||
Style: clip.StrokeStyle{
|
||||
Width: 2.5,
|
||||
Cap: clip.RoundCap,
|
||||
Join: clip.BevelJoin,
|
||||
},
|
||||
}.Op().Add(o)
|
||||
|
||||
paint.Fill(o, red)
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.White)
|
||||
r.expect(10, 50, colornames.Red)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStrokedPathBevelSquare(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
p := newStrokedPath(o)
|
||||
clip.Stroke{
|
||||
Path: p,
|
||||
Style: clip.StrokeStyle{
|
||||
Width: 2.5,
|
||||
Cap: clip.SquareCap,
|
||||
Join: clip.BevelJoin,
|
||||
},
|
||||
}.Op().Add(o)
|
||||
|
||||
paint.Fill(o, red)
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.White)
|
||||
r.expect(10, 50, colornames.Red)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStrokedPathRoundRound(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
p := newStrokedPath(o)
|
||||
clip.Stroke{
|
||||
Path: p,
|
||||
Style: clip.StrokeStyle{
|
||||
Width: 2.5,
|
||||
Cap: clip.RoundCap,
|
||||
Join: clip.RoundJoin,
|
||||
},
|
||||
}.Op().Add(o)
|
||||
|
||||
paint.Fill(o, red)
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.White)
|
||||
r.expect(10, 50, colornames.Red)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStrokedPathFlatMiter(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
{
|
||||
stk := op.Save(o)
|
||||
p := newZigZagPath(o)
|
||||
clip.Stroke{
|
||||
Path: p,
|
||||
Style: clip.StrokeStyle{
|
||||
Width: 10,
|
||||
Cap: clip.FlatCap,
|
||||
Join: clip.BevelJoin,
|
||||
Miter: 5,
|
||||
},
|
||||
}.Op().Add(o)
|
||||
paint.Fill(o, red)
|
||||
stk.Load()
|
||||
}
|
||||
{
|
||||
stk := op.Save(o)
|
||||
p := newZigZagPath(o)
|
||||
clip.Stroke{
|
||||
Path: p,
|
||||
Style: clip.StrokeStyle{
|
||||
Width: 2,
|
||||
},
|
||||
}.Op().Add(o)
|
||||
paint.Fill(o, black)
|
||||
stk.Load()
|
||||
}
|
||||
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.White)
|
||||
r.expect(40, 10, colornames.Black)
|
||||
r.expect(40, 12, colornames.Red)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStrokedPathFlatMiterInf(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
{
|
||||
stk := op.Save(o)
|
||||
p := newZigZagPath(o)
|
||||
clip.Stroke{
|
||||
Path: p,
|
||||
Style: clip.StrokeStyle{
|
||||
Width: 10,
|
||||
Cap: clip.FlatCap,
|
||||
Join: clip.BevelJoin,
|
||||
Miter: float32(math.Inf(+1)),
|
||||
},
|
||||
}.Op().Add(o)
|
||||
paint.Fill(o, red)
|
||||
stk.Load()
|
||||
}
|
||||
{
|
||||
stk := op.Save(o)
|
||||
p := newZigZagPath(o)
|
||||
clip.Stroke{
|
||||
Path: p,
|
||||
Style: clip.StrokeStyle{
|
||||
Width: 2,
|
||||
},
|
||||
}.Op().Add(o)
|
||||
paint.Fill(o, black)
|
||||
stk.Load()
|
||||
}
|
||||
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.White)
|
||||
r.expect(40, 10, colornames.Black)
|
||||
r.expect(40, 12, colornames.Red)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStrokedPathZeroWidth(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
{
|
||||
stk := op.Save(o)
|
||||
p := new(clip.Path)
|
||||
p.Begin(o)
|
||||
p.Move(f32.Pt(10, 50))
|
||||
p.Line(f32.Pt(50, 0))
|
||||
clip.Stroke{
|
||||
Path: p.End(),
|
||||
Style: clip.StrokeStyle{
|
||||
Width: 2,
|
||||
},
|
||||
}.Op().Add(o)
|
||||
|
||||
paint.Fill(o, black)
|
||||
stk.Load()
|
||||
}
|
||||
|
||||
{
|
||||
stk := op.Save(o)
|
||||
p := new(clip.Path)
|
||||
p.Begin(o)
|
||||
p.Move(f32.Pt(10, 50))
|
||||
p.Line(f32.Pt(30, 0))
|
||||
clip.Stroke{
|
||||
Path: p.End(),
|
||||
}.Op().Add(o) // width=0, disable stroke
|
||||
|
||||
paint.Fill(o, red)
|
||||
stk.Load()
|
||||
}
|
||||
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.White)
|
||||
r.expect(10, 50, colornames.Black)
|
||||
r.expect(30, 50, colornames.Black)
|
||||
r.expect(65, 50, colornames.White)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDashedPathFlatCapEllipse(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
{
|
||||
stk := op.Save(o)
|
||||
p := newEllipsePath(o)
|
||||
|
||||
var dash clip.Dash
|
||||
dash.Begin(o)
|
||||
dash.Dash(5)
|
||||
dash.Dash(3)
|
||||
|
||||
clip.Stroke{
|
||||
Path: p,
|
||||
Style: clip.StrokeStyle{
|
||||
Width: 10,
|
||||
Cap: clip.FlatCap,
|
||||
Join: clip.BevelJoin,
|
||||
Miter: float32(math.Inf(+1)),
|
||||
},
|
||||
Dashes: dash.End(),
|
||||
}.Op().Add(o)
|
||||
|
||||
paint.Fill(
|
||||
o,
|
||||
red,
|
||||
)
|
||||
stk.Load()
|
||||
}
|
||||
{
|
||||
stk := op.Save(o)
|
||||
p := newEllipsePath(o)
|
||||
clip.Stroke{
|
||||
Path: p,
|
||||
Style: clip.StrokeStyle{
|
||||
Width: 2,
|
||||
},
|
||||
}.Op().Add(o)
|
||||
|
||||
paint.Fill(
|
||||
o,
|
||||
black,
|
||||
)
|
||||
stk.Load()
|
||||
}
|
||||
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.White)
|
||||
r.expect(0, 62, colornames.Red)
|
||||
r.expect(0, 65, colornames.Black)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDashedPathFlatCapZ(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
{
|
||||
stk := op.Save(o)
|
||||
p := newZigZagPath(o)
|
||||
var dash clip.Dash
|
||||
dash.Begin(o)
|
||||
dash.Dash(5)
|
||||
dash.Dash(3)
|
||||
|
||||
clip.Stroke{
|
||||
Path: p,
|
||||
Style: clip.StrokeStyle{
|
||||
Width: 10,
|
||||
Cap: clip.FlatCap,
|
||||
Join: clip.BevelJoin,
|
||||
Miter: float32(math.Inf(+1)),
|
||||
},
|
||||
Dashes: dash.End(),
|
||||
}.Op().Add(o)
|
||||
paint.Fill(o, red)
|
||||
stk.Load()
|
||||
}
|
||||
|
||||
{
|
||||
stk := op.Save(o)
|
||||
p := newZigZagPath(o)
|
||||
clip.Stroke{
|
||||
Path: p,
|
||||
Style: clip.StrokeStyle{Width: 2},
|
||||
}.Op().Add(o)
|
||||
paint.Fill(o, black)
|
||||
stk.Load()
|
||||
}
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.White)
|
||||
r.expect(40, 10, colornames.Black)
|
||||
r.expect(40, 12, colornames.Red)
|
||||
r.expect(46, 12, colornames.White)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDashedPathFlatCapZNoDash(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
{
|
||||
stk := op.Save(o)
|
||||
p := newZigZagPath(o)
|
||||
var dash clip.Dash
|
||||
dash.Begin(o)
|
||||
dash.Phase(1)
|
||||
|
||||
clip.Stroke{
|
||||
Path: p,
|
||||
Style: clip.StrokeStyle{
|
||||
Width: 10,
|
||||
Cap: clip.FlatCap,
|
||||
Join: clip.BevelJoin,
|
||||
Miter: float32(math.Inf(+1)),
|
||||
},
|
||||
Dashes: dash.End(),
|
||||
}.Op().Add(o)
|
||||
paint.Fill(o, red)
|
||||
stk.Load()
|
||||
}
|
||||
{
|
||||
stk := op.Save(o)
|
||||
clip.Stroke{
|
||||
Path: newZigZagPath(o),
|
||||
Style: clip.StrokeStyle{Width: 2},
|
||||
}.Op().Add(o)
|
||||
paint.Fill(o, black)
|
||||
stk.Load()
|
||||
}
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.White)
|
||||
r.expect(40, 10, colornames.Black)
|
||||
r.expect(40, 12, colornames.Red)
|
||||
r.expect(46, 12, colornames.Red)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDashedPathFlatCapZNoPath(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
{
|
||||
stk := op.Save(o)
|
||||
var dash clip.Dash
|
||||
dash.Begin(o)
|
||||
dash.Dash(0)
|
||||
clip.Stroke{
|
||||
Path: newZigZagPath(o),
|
||||
Style: clip.StrokeStyle{
|
||||
Width: 10,
|
||||
Cap: clip.FlatCap,
|
||||
Join: clip.BevelJoin,
|
||||
Miter: float32(math.Inf(+1)),
|
||||
},
|
||||
Dashes: dash.End(),
|
||||
}.Op().Add(o)
|
||||
paint.Fill(o, red)
|
||||
stk.Load()
|
||||
}
|
||||
{
|
||||
stk := op.Save(o)
|
||||
p := newZigZagPath(o)
|
||||
clip.Stroke{
|
||||
Path: p,
|
||||
Style: clip.StrokeStyle{Width: 2},
|
||||
}.Op().Add(o)
|
||||
paint.Fill(o, black)
|
||||
stk.Load()
|
||||
}
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.White)
|
||||
r.expect(40, 10, colornames.Black)
|
||||
r.expect(40, 12, colornames.White)
|
||||
r.expect(46, 12, colornames.White)
|
||||
})
|
||||
}
|
||||
|
||||
func newStrokedPath(o *op.Ops) clip.PathSpec {
|
||||
p := new(clip.Path)
|
||||
p.Begin(o)
|
||||
p.Move(f32.Pt(10, 50))
|
||||
p.Line(f32.Pt(10, 0))
|
||||
p.Arc(f32.Pt(10, 0), f32.Pt(20, 0), math.Pi)
|
||||
p.Line(f32.Pt(10, 0))
|
||||
p.Line(f32.Pt(10, 10))
|
||||
p.Arc(f32.Pt(0, 30), f32.Pt(0, 30), 2*math.Pi)
|
||||
p.Line(f32.Pt(-20, 0))
|
||||
p.Quad(f32.Pt(-10, -10), f32.Pt(-30, 30))
|
||||
return p.End()
|
||||
}
|
||||
|
||||
func newZigZagPath(o *op.Ops) clip.PathSpec {
|
||||
p := new(clip.Path)
|
||||
p.Begin(o)
|
||||
p.Move(f32.Pt(40, 10))
|
||||
p.Line(f32.Pt(50, 0))
|
||||
p.Line(f32.Pt(-50, 50))
|
||||
p.Line(f32.Pt(50, 0))
|
||||
p.Quad(f32.Pt(-50, 20), f32.Pt(-50, 50))
|
||||
p.Line(f32.Pt(50, 0))
|
||||
return p.End()
|
||||
}
|
||||
|
||||
func newEllipsePath(o *op.Ops) clip.PathSpec {
|
||||
p := new(clip.Path)
|
||||
p.Begin(o)
|
||||
p.Move(f32.Pt(0, 65))
|
||||
p.Line(f32.Pt(20, 0))
|
||||
p.Arc(f32.Pt(20, 0), f32.Pt(70, 0), 2*math.Pi)
|
||||
return p.End()
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package rendertest is intended for testing of drawing ops only.
|
||||
package rendertest
|
||||
|
After Width: | Height: | Size: 364 B |
|
After Width: | Height: | Size: 929 B |
|
After Width: | Height: | Size: 383 B |
|
After Width: | Height: | Size: 383 B |
|
After Width: | Height: | Size: 546 B |
|
After Width: | Height: | Size: 383 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 420 B |
|
After Width: | Height: | Size: 716 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 364 B |
|
After Width: | Height: | Size: 374 B |
|
After Width: | Height: | Size: 458 B |
|
After Width: | Height: | Size: 463 B |
|
After Width: | Height: | Size: 920 B |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 582 B |
|
After Width: | Height: | Size: 571 B |
|
After Width: | Height: | Size: 571 B |
|
After Width: | Height: | Size: 382 B |
|
After Width: | Height: | Size: 392 B |
|
After Width: | Height: | Size: 387 B |
|
After Width: | Height: | Size: 374 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 511 B |
|
After Width: | Height: | Size: 408 B |
|
After Width: | Height: | Size: 376 B |
|
After Width: | Height: | Size: 383 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 864 B |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 390 B |
|
After Width: | Height: | Size: 375 B |
|
After Width: | Height: | Size: 384 B |
@@ -0,0 +1,335 @@
|
||||
package rendertest
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/image/colornames"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/internal/f32color"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
)
|
||||
|
||||
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, black, clip.Rect(dr).Op())
|
||||
c1 := m1.Stop()
|
||||
|
||||
// Render the second stacked item
|
||||
m2 := op.Record(o)
|
||||
paint.ColorOp{Color: red}.Add(o)
|
||||
// Simulate a draw text call
|
||||
stack := op.Save(o)
|
||||
op.Offset(f32.Pt(0, 10)).Add(o)
|
||||
|
||||
// Apply the clip-path.
|
||||
c.Add(o)
|
||||
|
||||
paint.PaintOp{}.Add(o)
|
||||
stack.Load()
|
||||
|
||||
c2 := m2.Stop()
|
||||
|
||||
// Call each of them in a transform
|
||||
s1 := op.Save(o)
|
||||
op.Offset(f32.Pt(0, 0)).Add(o)
|
||||
c1.Add(o)
|
||||
s1.Load()
|
||||
s2 := op.Save(o)
|
||||
op.Offset(f32.Pt(0, 0)).Add(o)
|
||||
c2.Add(o)
|
||||
s2.Load()
|
||||
}, 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, 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))
|
||||
p := builder.End()
|
||||
clip.Outline{
|
||||
Path: p,
|
||||
}.Op().Add(o)
|
||||
paint.Fill(o, 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 pollute 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, 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, 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 TestDeferredPaint(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
state := op.Save(o)
|
||||
clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o)
|
||||
paint.ColorOp{Color: color.NRGBA{A: 0xff, G: 0xff}}.Add(o)
|
||||
paint.PaintOp{}.Add(o)
|
||||
|
||||
op.Affine(f32.Affine2D{}.Offset(f32.Pt(20, 20))).Add(o)
|
||||
m := op.Record(o)
|
||||
clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o)
|
||||
paint.ColorOp{Color: color.NRGBA{A: 0xff, R: 0xff, G: 0xff}}.Add(o)
|
||||
paint.PaintOp{}.Add(o)
|
||||
paintMacro := m.Stop()
|
||||
op.Defer(o, paintMacro)
|
||||
|
||||
state.Load()
|
||||
op.Affine(f32.Affine2D{}.Offset(f32.Pt(10, 10))).Add(o)
|
||||
clip.Rect(image.Rect(0, 0, 80, 80)).Op().Add(o)
|
||||
paint.ColorOp{Color: color.NRGBA{A: 0xff, B: 0xff}}.Add(o)
|
||||
paint.PaintOp{}.Add(o)
|
||||
}, func(r result) {
|
||||
})
|
||||
}
|
||||
|
||||
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))
|
||||
p := builder.End()
|
||||
clip.Outline{Path: p}.Op().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.Save(ops)
|
||||
c1.Add(ops)
|
||||
stack1.Load()
|
||||
|
||||
stack2 := op.Save(ops)
|
||||
op.Offset(f32.Pt(0, 50)).Add(ops)
|
||||
c2.Add(ops)
|
||||
stack2.Load()
|
||||
}, 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.Save(o)
|
||||
op.Offset(f32.Pt(0, off)).Add(o)
|
||||
txt.Add(o)
|
||||
paint.PaintOp{}.Add(o)
|
||||
s.Load()
|
||||
}
|
||||
|
||||
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.NRGBA
|
||||
}
|
||||
|
||||
var gradients = []Gradient{
|
||||
{From: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}},
|
||||
{From: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}},
|
||||
{From: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}},
|
||||
{From: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}},
|
||||
{From: color.NRGBA{R: 0x19, G: 0xFF, B: 0xFF, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}},
|
||||
{From: color.NRGBA{R: 0xFF, G: 0xFF, B: 0x19, A: 0xFF}, To: color.NRGBA{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.Save(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.Load()
|
||||
gr = gr.Add(f32.Pt(0, gradienth))
|
||||
}
|
||||
}, func(r result) {
|
||||
gr := pixelAligned
|
||||
for _, g := range gradients {
|
||||
from := f32color.LinearFromSRGB(g.From)
|
||||
to := f32color.LinearFromSRGB(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), f32color.NRGBAToRGBA(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: black,
|
||||
Stop2: f32.Pt(0, 0),
|
||||
Color2: red,
|
||||
}.Add(ops)
|
||||
st := op.Save(ops)
|
||||
clip.Rect(image.Rect(0, 0, 64, 64)).Add(ops)
|
||||
paint.PaintOp{}.Add(ops)
|
||||
st.Load()
|
||||
|
||||
paint.LinearGradientOp{
|
||||
Stop1: f32.Pt(64, 64),
|
||||
Color1: white,
|
||||
Stop2: f32.Pt(128, 0),
|
||||
Color2: green,
|
||||
}.Add(ops)
|
||||
st = op.Save(ops)
|
||||
clip.Rect(image.Rect(64, 0, 128, 64)).Add(ops)
|
||||
paint.PaintOp{}.Add(ops)
|
||||
st.Load()
|
||||
|
||||
paint.LinearGradientOp{
|
||||
Stop1: f32.Pt(64, 64),
|
||||
Color1: black,
|
||||
Stop2: f32.Pt(128, 128),
|
||||
Color2: blue,
|
||||
}.Add(ops)
|
||||
st = op.Save(ops)
|
||||
clip.Rect(image.Rect(64, 64, 128, 128)).Add(ops)
|
||||
paint.PaintOp{}.Add(ops)
|
||||
st.Load()
|
||||
|
||||
paint.LinearGradientOp{
|
||||
Stop1: f32.Pt(64, 64),
|
||||
Color1: white,
|
||||
Stop2: f32.Pt(0, 128),
|
||||
Color2: magenta,
|
||||
}.Add(ops)
|
||||
st = op.Save(ops)
|
||||
clip.Rect(image.Rect(0, 64, 64, 128)).Add(ops)
|
||||
paint.PaintOp{}.Add(ops)
|
||||
st.Load()
|
||||
}, 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,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package rendertest
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/image/colornames"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
)
|
||||
|
||||
func TestPaintOffset(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
op.Offset(f32.Pt(10, 20)).Add(o)
|
||||
paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op())
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.White)
|
||||
r.expect(59, 30, colornames.Red)
|
||||
r.expect(60, 30, colornames.White)
|
||||
r.expect(10, 70, colornames.White)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaintRotate(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
a := f32.Affine2D{}.Rotate(f32.Pt(40, 40), -math.Pi/8)
|
||||
op.Affine(a).Add(o)
|
||||
paint.FillShape(o, red, clip.Rect(image.Rect(20, 20, 60, 60)).Op())
|
||||
}, func(r result) {
|
||||
r.expect(40, 40, colornames.Red)
|
||||
r.expect(50, 19, colornames.Red)
|
||||
r.expect(59, 19, colornames.White)
|
||||
r.expect(21, 21, colornames.White)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPaintShear(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
a := f32.Affine2D{}.Shear(f32.Point{}, math.Pi/4, 0)
|
||||
op.Affine(a).Add(o)
|
||||
paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 40, 40)).Op())
|
||||
}, func(r result) {
|
||||
r.expect(10, 30, colornames.White)
|
||||
})
|
||||
}
|
||||
|
||||
func TestClipPaintOffset(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
clip.RRect{Rect: f32.Rect(10, 10, 30, 30)}.Add(o)
|
||||
op.Offset(f32.Pt(20, 20)).Add(o)
|
||||
paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 100, 100)).Op())
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.White)
|
||||
r.expect(19, 19, colornames.White)
|
||||
r.expect(20, 20, colornames.Red)
|
||||
r.expect(30, 30, colornames.White)
|
||||
})
|
||||
}
|
||||
|
||||
func TestClipOffset(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
op.Offset(f32.Pt(20, 20)).Add(o)
|
||||
clip.RRect{Rect: f32.Rect(10, 10, 30, 30)}.Add(o)
|
||||
paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 100, 100)).Op())
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.White)
|
||||
r.expect(29, 29, colornames.White)
|
||||
r.expect(30, 30, colornames.Red)
|
||||
r.expect(49, 49, colornames.Red)
|
||||
r.expect(50, 50, colornames.White)
|
||||
})
|
||||
}
|
||||
|
||||
func TestClipScale(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
a := f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(2, 2)).Offset(f32.Pt(10, 10))
|
||||
op.Affine(a).Add(o)
|
||||
clip.RRect{Rect: f32.Rect(10, 10, 20, 20)}.Add(o)
|
||||
paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 1000, 1000)).Op())
|
||||
}, func(r result) {
|
||||
r.expect(19+10, 19+10, colornames.White)
|
||||
r.expect(20+10, 20+10, colornames.Red)
|
||||
r.expect(39+10, 39+10, colornames.Red)
|
||||
r.expect(40+10, 40+10, colornames.White)
|
||||
})
|
||||
}
|
||||
|
||||
func TestClipRotate(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
op.Affine(f32.Affine2D{}.Rotate(f32.Pt(40, 40), -math.Pi/4)).Add(o)
|
||||
clip.RRect{Rect: f32.Rect(30, 30, 50, 50)}.Add(o)
|
||||
paint.FillShape(o, red, clip.Rect(image.Rect(0, 40, 100, 100)).Op())
|
||||
}, func(r result) {
|
||||
r.expect(39, 39, colornames.White)
|
||||
r.expect(41, 41, colornames.Red)
|
||||
r.expect(50, 50, colornames.White)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOffsetTexture(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
op.Offset(f32.Pt(15, 15)).Add(o)
|
||||
squares.Add(o)
|
||||
scale(50.0/512, 50.0/512).Add(o)
|
||||
paint.PaintOp{}.Add(o)
|
||||
}, func(r result) {
|
||||
r.expect(14, 20, colornames.White)
|
||||
r.expect(66, 20, colornames.White)
|
||||
r.expect(16, 64, colornames.Green)
|
||||
r.expect(64, 16, colornames.Green)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOffsetScaleTexture(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
op.Offset(f32.Pt(15, 15)).Add(o)
|
||||
squares.Add(o)
|
||||
op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(2, 1))).Add(o)
|
||||
scale(50.0/512, 50.0/512).Add(o)
|
||||
paint.PaintOp{}.Add(o)
|
||||
}, func(r result) {
|
||||
r.expect(114, 64, colornames.Blue)
|
||||
r.expect(116, 64, colornames.White)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRotateTexture(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
defer op.Save(o).Load()
|
||||
squares.Add(o)
|
||||
a := f32.Affine2D{}.Offset(f32.Pt(30, 30)).Rotate(f32.Pt(40, 40), math.Pi/4)
|
||||
op.Affine(a).Add(o)
|
||||
scale(20.0/512, 20.0/512).Add(o)
|
||||
paint.PaintOp{}.Add(o)
|
||||
}, func(r result) {
|
||||
r.expect(40, 40-12, colornames.Blue)
|
||||
r.expect(40+12, 40, colornames.Green)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRotateClipTexture(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
squares.Add(o)
|
||||
a := f32.Affine2D{}.Rotate(f32.Pt(40, 40), math.Pi/8)
|
||||
op.Affine(a).Add(o)
|
||||
clip.RRect{Rect: f32.Rect(30, 30, 50, 50)}.Add(o)
|
||||
op.Affine(f32.Affine2D{}.Offset(f32.Pt(10, 10))).Add(o)
|
||||
scale(60.0/512, 60.0/512).Add(o)
|
||||
paint.PaintOp{}.Add(o)
|
||||
}, func(r result) {
|
||||
r.expect(0, 0, colornames.White)
|
||||
r.expect(37, 39, colornames.Green)
|
||||
r.expect(36, 39, colornames.Green)
|
||||
r.expect(35, 39, colornames.Green)
|
||||
r.expect(34, 39, colornames.Green)
|
||||
r.expect(33, 39, colornames.Green)
|
||||
})
|
||||
}
|
||||
|
||||
func TestComplicatedTransform(t *testing.T) {
|
||||
run(t, func(o *op.Ops) {
|
||||
squares.Add(o)
|
||||
|
||||
clip.RRect{Rect: f32.Rect(0, 0, 100, 100), SE: 50, SW: 50, NW: 50, NE: 50}.Add(o)
|
||||
|
||||
a := f32.Affine2D{}.Shear(f32.Point{}, math.Pi/4, 0)
|
||||
op.Affine(a).Add(o)
|
||||
clip.RRect{Rect: f32.Rect(0, 0, 50, 40)}.Add(o)
|
||||
|
||||
scale(50.0/512, 50.0/512).Add(o)
|
||||
paint.PaintOp{}.Add(o)
|
||||
}, func(r result) {
|
||||
r.expect(20, 5, colornames.White)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTransformOrder(t *testing.T) {
|
||||
// check the ordering of operations bot in affine and in gpu stack.
|
||||
run(t, func(o *op.Ops) {
|
||||
a := f32.Affine2D{}.Offset(f32.Pt(64, 64))
|
||||
op.Affine(a).Add(o)
|
||||
|
||||
b := f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(8, 8))
|
||||
op.Affine(b).Add(o)
|
||||
|
||||
c := f32.Affine2D{}.Offset(f32.Pt(-10, -10)).Scale(f32.Point{}, f32.Pt(0.5, 0.5))
|
||||
op.Affine(c).Add(o)
|
||||
paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 20, 20)).Op())
|
||||
}, func(r result) {
|
||||
// centered and with radius 40
|
||||
r.expect(64-41, 64, colornames.White)
|
||||
r.expect(64-39, 64, colornames.Red)
|
||||
r.expect(64+39, 64, colornames.Red)
|
||||
r.expect(64+41, 64, colornames.White)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package rendertest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/image/colornames"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/gpu/headless"
|
||||
"gioui.org/internal/f32color"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/paint"
|
||||
)
|
||||
|
||||
var (
|
||||
dumpImages = flag.Bool("saveimages", false, "save test images")
|
||||
squares paint.ImageOp
|
||||
)
|
||||
|
||||
var (
|
||||
red = f32color.RGBAToNRGBA(colornames.Red)
|
||||
green = f32color.RGBAToNRGBA(colornames.Green)
|
||||
blue = f32color.RGBAToNRGBA(colornames.Blue)
|
||||
magenta = f32color.RGBAToNRGBA(colornames.Magenta)
|
||||
black = f32color.RGBAToNRGBA(colornames.Black)
|
||||
white = f32color.RGBAToNRGBA(colornames.White)
|
||||
)
|
||||
|
||||
func init() {
|
||||
// build the texture we use for testing
|
||||
size := 512
|
||||
sub := size / 4
|
||||
im := image.NewNRGBA(image.Rect(0, 0, size, size))
|
||||
c1, c2 := image.NewUniform(colornames.Green), image.NewUniform(colornames.Blue)
|
||||
for r := 0; r < 4; r++ {
|
||||
for c := 0; c < 4; c++ {
|
||||
c1, c2 = c2, c1
|
||||
draw.Draw(im, image.Rect(r*sub, c*sub, r*sub+sub, c*sub+sub), c1, image.Point{}, draw.Over)
|
||||
}
|
||||
c1, c2 = c2, c1
|
||||
}
|
||||
squares = paint.NewImageOp(im)
|
||||
}
|
||||
|
||||
func drawImage(t *testing.T, size int, ops *op.Ops, draw func(o *op.Ops)) (im *image.RGBA, err error) {
|
||||
sz := image.Point{X: size, Y: size}
|
||||
w := newWindow(t, sz.X, sz.Y)
|
||||
draw(ops)
|
||||
if err := w.Frame(ops); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return w.Screenshot()
|
||||
}
|
||||
|
||||
func run(t *testing.T, f func(o *op.Ops), c func(r result)) {
|
||||
// draw a few times and check that it is correct each time, to
|
||||
// ensure any caching effects still generate the correct images.
|
||||
var img *image.RGBA
|
||||
var err error
|
||||
ops := new(op.Ops)
|
||||
for i := 0; i < 3; i++ {
|
||||
ops.Reset()
|
||||
img, err = drawImage(t, 128, ops, f)
|
||||
if err != nil {
|
||||
t.Error("error rendering:", err)
|
||||
return
|
||||
}
|
||||
// check for a reference image and make sure we are identical.
|
||||
if !verifyRef(t, img, 0) {
|
||||
name := fmt.Sprintf("%s-%d-bad.png", t.Name(), i)
|
||||
if err := saveImage(name, img); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
c(result{t: t, img: img})
|
||||
}
|
||||
|
||||
if *dumpImages {
|
||||
if err := saveImage(t.Name()+".png", img); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func frame(f func(o *op.Ops), c func(r result)) frameT {
|
||||
return frameT{f: f, c: c}
|
||||
}
|
||||
|
||||
type frameT struct {
|
||||
f func(o *op.Ops)
|
||||
c func(r result)
|
||||
}
|
||||
|
||||
// multiRun is used to run test cases over multiple frames, typically
|
||||
// to test caching interactions.
|
||||
func multiRun(t *testing.T, frames ...frameT) {
|
||||
// draw a few times and check that it is correct each time, to
|
||||
// ensure any caching effects still generate the correct images.
|
||||
var img *image.RGBA
|
||||
var err error
|
||||
sz := image.Point{X: 128, Y: 128}
|
||||
w := newWindow(t, sz.X, sz.Y)
|
||||
ops := new(op.Ops)
|
||||
for i := range frames {
|
||||
ops.Reset()
|
||||
frames[i].f(ops)
|
||||
if err := w.Frame(ops); err != nil {
|
||||
t.Errorf("rendering failed: %v", err)
|
||||
continue
|
||||
}
|
||||
img, err = w.Screenshot()
|
||||
if err != nil {
|
||||
t.Errorf("screenshot failed: %v", err)
|
||||
continue
|
||||
}
|
||||
// Check for a reference image and make sure they are identical.
|
||||
ok := verifyRef(t, img, i)
|
||||
if frames[i].c != nil {
|
||||
frames[i].c(result{t: t, img: img})
|
||||
}
|
||||
if *dumpImages || !ok {
|
||||
name := t.Name() + ".png"
|
||||
if i != 0 {
|
||||
name = t.Name() + "_" + strconv.Itoa(i) + ".png"
|
||||
}
|
||||
if err := saveImage(name, img); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func verifyRef(t *testing.T, img *image.RGBA, frame int) (ok bool) {
|
||||
// ensure identical to ref data
|
||||
path := filepath.Join("refs", t.Name()+".png")
|
||||
if frame != 0 {
|
||||
path = filepath.Join("refs", t.Name()+"_"+strconv.Itoa(frame)+".png")
|
||||
}
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Error("could not open ref:", err)
|
||||
return
|
||||
}
|
||||
r, err := png.Decode(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
t.Error("could not decode ref:", err)
|
||||
return
|
||||
}
|
||||
ref, ok := r.(*image.RGBA)
|
||||
if !ok {
|
||||
t.Errorf("image is a %T, expected *image.RGBA", r)
|
||||
return
|
||||
}
|
||||
if len(ref.Pix) != len(img.Pix) {
|
||||
t.Error("not equal to ref (len)")
|
||||
return false
|
||||
}
|
||||
bnd := img.Bounds()
|
||||
for x := bnd.Min.X; x < bnd.Max.X; x++ {
|
||||
for y := bnd.Min.Y; y < bnd.Max.Y; y++ {
|
||||
c1, c2 := ref.RGBAAt(x, y), img.RGBAAt(x, y)
|
||||
if !colorsClose(c1, c2) {
|
||||
t.Error("not equal to ref at", x, y, " ", c1, c2)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func colorsClose(c1, c2 color.RGBA) bool {
|
||||
const delta = 0.01 // magic value obtained from experimentation.
|
||||
return yiqEqApprox(c1, c2, delta)
|
||||
}
|
||||
|
||||
// yiqEqApprox compares the colors of 2 pixels, in the NTSC YIQ color space,
|
||||
// as described in:
|
||||
//
|
||||
// Measuring perceived color difference using YIQ NTSC
|
||||
// transmission color space in mobile applications.
|
||||
// Yuriy Kotsarenko, Fernando Ramos.
|
||||
//
|
||||
// An electronic version is available at:
|
||||
//
|
||||
// - http://www.progmat.uaem.mx:8080/artVol2Num2/Articulo3Vol2Num2.pdf
|
||||
func yiqEqApprox(c1, c2 color.RGBA, d2 float64) bool {
|
||||
const max = 35215.0 // difference between 2 maximally different pixels.
|
||||
|
||||
var (
|
||||
r1 = float64(c1.R)
|
||||
g1 = float64(c1.G)
|
||||
b1 = float64(c1.B)
|
||||
|
||||
r2 = float64(c2.R)
|
||||
g2 = float64(c2.G)
|
||||
b2 = float64(c2.B)
|
||||
|
||||
y1 = r1*0.29889531 + g1*0.58662247 + b1*0.11448223
|
||||
i1 = r1*0.59597799 - g1*0.27417610 - b1*0.32180189
|
||||
q1 = r1*0.21147017 - g1*0.52261711 + b1*0.31114694
|
||||
|
||||
y2 = r2*0.29889531 + g2*0.58662247 + b2*0.11448223
|
||||
i2 = r2*0.59597799 - g2*0.27417610 - b2*0.32180189
|
||||
q2 = r2*0.21147017 - g2*0.52261711 + b2*0.31114694
|
||||
|
||||
y = y1 - y2
|
||||
i = i1 - i2
|
||||
q = q1 - q2
|
||||
|
||||
diff = 0.5053*y*y + 0.299*i*i + 0.1957*q*q
|
||||
)
|
||||
return diff <= max*d2
|
||||
}
|
||||
|
||||
func (r result) expect(x, y int, col color.RGBA) {
|
||||
if r.img == nil {
|
||||
return
|
||||
}
|
||||
c := r.img.RGBAAt(x, y)
|
||||
if !colorsClose(c, col) {
|
||||
r.t.Error("expected ", col, " at ", "(", x, ",", y, ") but got ", c)
|
||||
}
|
||||
}
|
||||
|
||||
type result struct {
|
||||
t *testing.T
|
||||
img *image.RGBA
|
||||
}
|
||||
|
||||
func saveImage(file string, img image.Image) error {
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(file, buf.Bytes(), 0666)
|
||||
}
|
||||
|
||||
func newWindow(t testing.TB, width, height int) *headless.Window {
|
||||
w, err := headless.NewWindow(width, height)
|
||||
if err != nil {
|
||||
t.Skipf("failed to create headless window, skipping: %v", err)
|
||||
}
|
||||
t.Cleanup(w.Release)
|
||||
return w
|
||||
}
|
||||
|
||||
func scale(sx, sy float32) op.TransformOp {
|
||||
return op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(sx, sy)))
|
||||
}
|
||||