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 uvTrans f32.Affine2D
} }
const (
filterLinear = 0
filterNearest = 1
)
// imageOpData is the shadow of paint.ImageOp. // imageOpData is the shadow of paint.ImageOp.
type imageOpData struct { type imageOpData struct {
src *image.RGBA src *image.RGBA
handle interface{} handle interface{}
filter byte
} }
type linearGradientOpData struct { type linearGradientOpData struct {
@@ -207,6 +213,7 @@ func decodeImageOp(data []byte, refs []interface{}) imageOpData {
return imageOpData{ return imageOpData{
src: refs[0].(*image.RGBA), src: refs[0].(*image.RGBA),
handle: handle, handle: handle,
filter: data[1],
} }
} }
@@ -454,19 +461,41 @@ func (g *gpu) Profile() string {
} }
func (r *renderer) texHandle(cache *resourceCache, data imageOpData) driver.Texture { 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 var tex *texture
t, exists := cache.get(data.handle) t, exists := cache.get(key)
if !exists { if !exists {
t = &texture{ t = &texture{
src: data.src, src: data.src,
} }
cache.put(data.handle, t) cache.put(key, t)
} }
tex = t.(*texture) tex = t.(*texture)
if tex.tex != nil { if tex.tex != nil {
return tex.tex 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 { if err != nil {
panic(err) 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) { func TestGapsInPath(t *testing.T) {
ops := new(op.Ops) ops := new(op.Ops)
var p clip.Path var p clip.Path
+1 -1
View File
@@ -142,7 +142,7 @@ const (
TypePushOpacityLen = 1 + 4 TypePushOpacityLen = 1 + 4
TypePopOpacityLen = 1 TypePopOpacityLen = 1
TypeRedrawLen = 1 + 8 TypeRedrawLen = 1 + 8
TypeImageLen = 1 TypeImageLen = 1 + 1
TypePaintLen = 1 TypePaintLen = 1
TypeColorLen = 1 + 4 TypeColorLen = 1 + 4
TypeLinearGradientLen = 1 + 8*2 + 4*2 TypeLinearGradientLen = 1 + 8*2 + 4*2
+13
View File
@@ -15,8 +15,20 @@ import (
"gioui.org/op/clip" "gioui.org/op/clip"
) )
// ImageFilter is the scaling filter for images.
type ImageFilter byte
const (
// FilterLinear uses linear interpolation for scaling.
FilterLinear ImageFilter = iota
// FilterNearest uses nearest neighbor interpolation for scaling.
FilterNearest
)
// ImageOp sets the brush to an image. // ImageOp sets the brush to an image.
type ImageOp struct { type ImageOp struct {
Filter ImageFilter
uniform bool uniform bool
color color.NRGBA color color.NRGBA
src *image.RGBA src *image.RGBA
@@ -103,6 +115,7 @@ func (i ImageOp) Add(o *op.Ops) {
} }
data := ops.Write2(&o.Internal, ops.TypeImageLen, i.src, i.handle) data := ops.Write2(&o.Internal, ops.TypeImageLen, i.src, i.handle)
data[0] = byte(ops.TypeImage) data[0] = byte(ops.TypeImage)
data[1] = byte(i.Filter)
} }
func (c ColorOp) Add(o *op.Ops) { func (c ColorOp) Add(o *op.Ops) {