Skip to content

Commit

Permalink
op/paint: add nearest neighbor scaling
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
egonelbre authored and eliasnaur committed Nov 15, 2023
1 parent 23b6f06 commit 5fa94ff
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 4 deletions.
35 changes: 32 additions & 3 deletions gpu/gpu.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -207,6 +213,7 @@ func decodeImageOp(data []byte, refs []interface{}) imageOpData {
return imageOpData{
src: refs[0].(*image.RGBA),
handle: handle,
filter: data[1],
}
}

Expand Down Expand Up @@ -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)
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
67 changes: 67 additions & 0 deletions gpu/internal/rendertest/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion internal/ops/ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ const (
TypePushOpacityLen = 1 + 4
TypePopOpacityLen = 1
TypeRedrawLen = 1 + 8
TypeImageLen = 1
TypeImageLen = 1 + 1
TypePaintLen = 1
TypeColorLen = 1 + 4
TypeLinearGradientLen = 1 + 8*2 + 4*2
Expand Down
13 changes: 13 additions & 0 deletions op/paint/paint.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,20 @@ import (
"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.
type ImageOp struct {
Filter ImageFilter

uniform bool
color color.NRGBA
src *image.RGBA
Expand Down Expand Up @@ -103,6 +115,7 @@ func (i ImageOp) Add(o *op.Ops) {
}
data := ops.Write2(&o.Internal, ops.TypeImageLen, i.src, i.handle)
data[0] = byte(ops.TypeImage)
data[1] = byte(i.Filter)
}

func (c ColorOp) Add(o *op.Ops) {
Expand Down

0 comments on commit 5fa94ff

Please sign in to comment.