op/paint: add nearest neighbor scaling

This adds support for nearest neighbor filtering,
which can be useful in quite a few scenarios.

  img := paint.NewImageOp(m)
  img.Filter = paint.FilterNearest
  img.Add(gtx.Ops)

Fixes: https://todo.sr.ht/~eliasnaur/gio/414
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
This commit is contained in:
Egon Elbre
2023-11-15 12:02:08 +02:00
committed by Elias Naur
parent 23b6f06e3e
commit 5fa94ff67b
6 changed files with 113 additions and 4 deletions
+32 -3
View File
@@ -186,10 +186,16 @@ type material struct {
uvTrans f32.Affine2D
}
const (
filterLinear = 0
filterNearest = 1
)
// imageOpData is the shadow of paint.ImageOp.
type imageOpData struct {
src *image.RGBA
handle interface{}
filter byte
}
type linearGradientOpData struct {
@@ -207,6 +213,7 @@ func decodeImageOp(data []byte, refs []interface{}) imageOpData {
return imageOpData{
src: refs[0].(*image.RGBA),
handle: handle,
filter: data[1],
}
}
@@ -454,19 +461,41 @@ func (g *gpu) Profile() string {
}
func (r *renderer) texHandle(cache *resourceCache, data imageOpData) driver.Texture {
type cachekey struct {
filter byte
handle any
}
key := cachekey{
filter: data.filter,
handle: data.handle,
}
var tex *texture
t, exists := cache.get(data.handle)
t, exists := cache.get(key)
if !exists {
t = &texture{
src: data.src,
}
cache.put(data.handle, t)
cache.put(key, t)
}
tex = t.(*texture)
if tex.tex != nil {
return tex.tex
}
handle, err := r.ctx.NewTexture(driver.TextureFormatSRGBA, data.src.Bounds().Dx(), data.src.Bounds().Dy(), driver.FilterLinearMipmapLinear, driver.FilterLinear, driver.BufferBindingTexture)
var minFilter, magFilter driver.TextureFilter
switch data.filter {
case filterLinear:
minFilter, magFilter = driver.FilterLinearMipmapLinear, driver.FilterLinear
case filterNearest:
minFilter, magFilter = driver.FilterNearest, driver.FilterNearest
}
handle, err := r.ctx.NewTexture(driver.TextureFormatSRGBA,
data.src.Bounds().Dx(), data.src.Bounds().Dy(),
minFilter, magFilter,
driver.BufferBindingTexture,
)
if err != nil {
panic(err)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

+67
View File
@@ -359,6 +359,73 @@ func TestImageRGBA(t *testing.T) {
})
}
func TestImageRGBA_ScaleLinear(t *testing.T) {
run(t, func(o *op.Ops) {
w := newWindow(t, 128, 128)
defer clip.Rect{Max: image.Pt(128, 128)}.Push(o).Pop()
op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(64, 64))).Add(o)
im := image.NewRGBA(image.Rect(0, 0, 2, 2))
im.Set(0, 0, colornames.Red)
im.Set(1, 0, colornames.Green)
im.Set(0, 1, colornames.White)
im.Set(1, 1, colornames.Black)
op := paint.NewImageOp(im)
op.Filter = paint.FilterLinear
op.Add(o)
paint.PaintOp{}.Add(o)
if err := w.Frame(o); err != nil {
t.Error(err)
}
}, func(r result) {
r.expect(0, 0, colornames.Red)
r.expect(8, 8, colornames.Red)
// TODO: this currently seems to do srgb scaling
// instead of linear rgb scaling,
r.expect(64-4, 0, color.RGBA{R: 197, G: 87, B: 0, A: 255})
r.expect(64+4, 0, color.RGBA{R: 175, G: 98, B: 0, A: 255})
r.expect(127, 0, colornames.Green)
r.expect(127-8, 8, colornames.Green)
})
}
func TestImageRGBA_ScaleNearest(t *testing.T) {
run(t, func(o *op.Ops) {
w := newWindow(t, 128, 128)
op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(64, 64))).Add(o)
im := image.NewRGBA(image.Rect(0, 0, 2, 2))
im.Set(0, 0, colornames.Red)
im.Set(1, 0, colornames.Green)
im.Set(0, 1, colornames.White)
im.Set(1, 1, colornames.Black)
op := paint.NewImageOp(im)
op.Filter = paint.FilterNearest
op.Add(o)
paint.PaintOp{}.Add(o)
if err := w.Frame(o); err != nil {
t.Error(err)
}
}, func(r result) {
r.expect(0, 0, colornames.Red)
r.expect(8, 8, colornames.Red)
r.expect(64-4, 0, colornames.Red)
r.expect(64+4, 0, colornames.Green)
r.expect(127, 0, colornames.Green)
r.expect(127-8, 8, colornames.Green)
})
}
func TestGapsInPath(t *testing.T) {
ops := new(op.Ops)
var p clip.Path