go.mobile/sprite/portable: support masks in affine transform

LGTM=nigeltao
R=nigeltao
CC=golang-codereviews
https://golang.org/cl/166100043
This commit is contained in:
David Crawshaw 2014-11-04 08:51:09 -05:00
Родитель 3e603f3a38
Коммит df04a5b449
4 изменённых файлов: 202 добавлений и 62 удалений

Просмотреть файл

@ -25,7 +25,7 @@ import (
//
// will produce a dst that is half the size of src. To perform a
// traditional affine transform, use the inverse of the affine matrix.
func affine(dst *image.RGBA, src *image.RGBA, a *f32.Affine, op draw.Op) {
func affine(dst *image.RGBA, src *image.RGBA, a *f32.Affine, mask image.Image, op draw.Op) {
srcb := src.Bounds()
b := dst.Bounds()
@ -40,40 +40,48 @@ func affine(dst *image.RGBA, src *image.RGBA, a *f32.Affine, op draw.Op) {
continue
}
c := bilinear(src, sx, sy)
off := (y-dst.Rect.Min.Y)*dst.Stride + (x-dst.Rect.Min.X)*4
if op == draw.Over {
// This logic comes from drawCopyOver in the image/draw package.
sr := uint32(c.R) * 0x101
sg := uint32(c.G) * 0x101
sb := uint32(c.B) * 0x101
sa := uint32(c.A) * 0x101
// m is the maximum color value returned by image.Color.RGBA.
const m = 1<<16 - 1
ma := uint32(m)
if mask != nil {
_, _, _, ma = bilinear(mask, sx, sy).RGBA()
}
c := bilinearRGBA(src, sx, sy)
off := (y-dst.Rect.Min.Y)*dst.Stride + (x-dst.Rect.Min.X)*4
// c.R, c.G, c.B, c.A, dr, dg, db, and da are all 8-bit color at the moment,
// ranging in [0,255]. We work in 16-bit color, and so would normally do:
// dr |= dr << 8
// and similarly for the other values, but instead we multiply by 0x101
// to shift these to 16-bit colors, ranging in [0,65535].
// This yields the same result, but is fewer arithmetic operations.
//
// This logic comes from drawCopyOver in the image/draw package.
sr := uint32(c.R) * 0x101
sg := uint32(c.G) * 0x101
sb := uint32(c.B) * 0x101
sa := uint32(c.A) * 0x101
if op == draw.Over {
dr := uint32(dst.Pix[off+0])
dg := uint32(dst.Pix[off+1])
db := uint32(dst.Pix[off+2])
da := uint32(dst.Pix[off+3])
// m is the maximum color value returned by image.Color.RGBA.
const m = 1<<16 - 1
a := m - (sa * ma / m)
a *= 0x101
// dr, dg, db and da are all 8-bit color at the moment, ranging in [0,255].
// We work in 16-bit color, and so would normally do:
// dr |= dr << 8
// and similarly for dg, db and da, but instead we multiply a
// (which is a 16-bit color, ranging in [0,65535]) by 0x101.
// This yields the same result, but is fewer arithmetic operations.
a := (m - sa) * 0x101
dst.Pix[off+0] = uint8((dr*a/m + sr) >> 8)
dst.Pix[off+1] = uint8((dg*a/m + sg) >> 8)
dst.Pix[off+2] = uint8((db*a/m + sb) >> 8)
dst.Pix[off+3] = uint8((da*a/m + sa) >> 8)
dst.Pix[off+0] = uint8((dr*a + sr*ma) / m >> 8)
dst.Pix[off+1] = uint8((dg*a + sg*ma) / m >> 8)
dst.Pix[off+2] = uint8((db*a + sb*ma) / m >> 8)
dst.Pix[off+3] = uint8((da*a + sa*ma) / m >> 8)
} else {
dst.Pix[off+0] = c.R
dst.Pix[off+1] = c.G
dst.Pix[off+2] = c.B
dst.Pix[off+3] = c.A
dst.Pix[off+0] = uint8(sr * ma / m >> 8)
dst.Pix[off+1] = uint8(sg * ma / m >> 8)
dst.Pix[off+2] = uint8(sb * ma / m >> 8)
dst.Pix[off+3] = uint8(sa * ma / m >> 8)
}
}
}

Просмотреть файл

@ -59,7 +59,7 @@ func TestAffine(t *testing.T) {
a.Scale(&a, 40/float32(src.Rect.Dx()), 20/float32(src.Rect.Dy()))
a.Inverse(&a)
affine(got, src, &a, draw.Over)
affine(got, src, &a, nil, draw.Over)
ptTopLeft := geom.Point{0, 24}
ptBottomRight := geom.Point{12 + 32, 16}
@ -87,27 +87,72 @@ func TestAffine(t *testing.T) {
}
if !imageEq(got, want) {
// Write out the image we got.
f, err = ioutil.TempFile("", "testpattern-window-got")
gotPath, err := writeTempPNG("testpattern-window-got", got)
if err != nil {
t.Fatal(err)
}
f.Close()
gotPath := f.Name() + ".png"
f, err = os.Create(gotPath)
if err != nil {
t.Fatal(err)
}
if err := png.Encode(f, got); err != nil {
t.Fatal(err)
}
if err := f.Close(); err != nil {
t.Fatal(err)
}
t.Errorf("got\n%s\nwant\n%s", gotPath, wantPath)
}
}
func TestAffineMask(t *testing.T) {
f, err := os.Open("../../testdata/testpattern.png")
if err != nil {
t.Fatal(err)
}
defer f.Close()
srcOrig, _, err := image.Decode(f)
if err != nil {
t.Fatal(err)
}
b := srcOrig.Bounds()
src := image.NewRGBA(b)
draw.Draw(src, src.Rect, srcOrig, b.Min, draw.Src)
mask := image.NewAlpha(b)
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
mask.Set(x, y, color.Alpha{A: uint8(x - b.Min.X)})
}
}
want := image.NewRGBA(b)
draw.DrawMask(want, want.Rect, src, b.Min, mask, b.Min, draw.Src)
a := new(f32.Affine)
a.Identity()
got := image.NewRGBA(b)
affine(got, src, a, mask, draw.Src)
if !imageEq(got, want) {
gotPath, err := writeTempPNG("testpattern-mask-got", got)
if err != nil {
t.Fatal(err)
}
wantPath, err := writeTempPNG("testpattern-mask-want", want)
if err != nil {
t.Fatal(err)
}
t.Errorf("got\n%s\nwant\n%s", gotPath, wantPath)
}
}
func writeTempPNG(prefix string, m image.Image) (string, error) {
f, err := ioutil.TempFile("", prefix+"-")
if err != nil {
return "", err
}
f.Close()
path := f.Name() + ".png"
f, err = os.Create(path)
if err != nil {
return "", err
}
if err := png.Encode(f, m); err != nil {
f.Close()
return "", err
}
return path, f.Close()
}
func drawCross(m *image.RGBA, x, y int) {
c := color.RGBA{0xff, 0, 0, 0xff} // red
m.SetRGBA(x+0, y-2, c)

Просмотреть файл

@ -10,14 +10,61 @@ import (
"math"
)
func bilinear(src *image.RGBA, x, y float32) color.RGBA {
func bilinear(src image.Image, x, y float32) color.Color {
switch src := src.(type) {
case *image.RGBA:
return bilinearRGBA(src, x, y)
case *image.Alpha:
return bilinearAlpha(src, x, y)
default:
return bilinearGeneral(src, x, y)
}
}
func bilinearGeneral(src image.Image, x, y float32) color.RGBA64 {
p := findLinearSrc(src.Bounds(), x, y)
// Array offsets for the surrounding pixels.
off00 := offRGBA(src, p.low.X, p.low.Y)
off01 := offRGBA(src, p.high.X, p.low.Y)
off10 := offRGBA(src, p.low.X, p.high.Y)
off11 := offRGBA(src, p.high.X, p.high.Y)
r00, g00, b00, a00 := src.At(p.low.X, p.low.Y).RGBA()
r01, g01, b01, a01 := src.At(p.high.X, p.low.Y).RGBA()
r10, g10, b10, a10 := src.At(p.low.X, p.high.Y).RGBA()
r11, g11, b11, a11 := src.At(p.high.X, p.high.Y).RGBA()
fr := float32(r00) * p.frac00
fg := float32(g00) * p.frac00
fb := float32(b00) * p.frac00
fa := float32(a00) * p.frac00
fr += float32(r01) * p.frac01
fg += float32(g01) * p.frac01
fb += float32(b01) * p.frac01
fa += float32(a01) * p.frac01
fr += float32(r10) * p.frac10
fg += float32(g10) * p.frac10
fb += float32(b10) * p.frac10
fa += float32(a10) * p.frac10
fr += float32(r11) * p.frac11
fg += float32(g11) * p.frac11
fb += float32(b11) * p.frac11
fa += float32(a11) * p.frac11
return color.RGBA64{
R: uint16(fr + 0.5),
G: uint16(fg + 0.5),
B: uint16(fb + 0.5),
A: uint16(fa + 0.5),
}
}
func bilinearRGBA(src *image.RGBA, x, y float32) color.RGBA {
p := findLinearSrc(src.Bounds(), x, y)
// Slice offsets for the surrounding pixels.
off00 := src.PixOffset(p.low.X, p.low.Y)
off01 := src.PixOffset(p.high.X, p.low.Y)
off10 := src.PixOffset(p.low.X, p.high.Y)
off11 := src.PixOffset(p.high.X, p.high.Y)
fr := float32(src.Pix[off00+0]) * p.frac00
fg := float32(src.Pix[off00+1]) * p.frac00
@ -47,6 +94,23 @@ func bilinear(src *image.RGBA, x, y float32) color.RGBA {
}
}
func bilinearAlpha(src *image.Alpha, x, y float32) color.Alpha {
p := findLinearSrc(src.Bounds(), x, y)
// Slice offsets for the surrounding pixels.
off00 := src.PixOffset(p.low.X, p.low.Y)
off01 := src.PixOffset(p.high.X, p.low.Y)
off10 := src.PixOffset(p.low.X, p.high.Y)
off11 := src.PixOffset(p.high.X, p.high.Y)
fa := float32(src.Pix[off00]) * p.frac00
fa += float32(src.Pix[off01]) * p.frac01
fa += float32(src.Pix[off10]) * p.frac10
fa += float32(src.Pix[off11]) * p.frac11
return color.Alpha{A: uint8(fa + 0.5)}
}
type bilinearSrc struct {
// Top-left and bottom-right interpolation sources
low, high image.Point
@ -128,7 +192,3 @@ func findLinearSrc(b image.Rectangle, sx, sy float32) bilinearSrc {
return p
}
func offRGBA(src *image.RGBA, x, y int) int {
return (y-src.Rect.Min.Y)*src.Stride + (x-src.Rect.Min.X)*4
}

Просмотреть файл

@ -84,14 +84,40 @@ func TestBilinear(t *testing.T) {
for _, p := range interpTests {
src := p.newSrc()
// Fast path.
c := bilinear(src, p.x, p.y)
if c.R != c.G || c.R != c.B || c.A != 0xff {
t.Errorf("expect channels to match, got %v", c)
c0 := bilinearGeneral(src, p.x, p.y)
c0R, c0G, c0B, c0A := c0.RGBA()
r := uint8(c0R >> 8)
g := uint8(c0G >> 8)
b := uint8(c0B >> 8)
a := uint8(c0A >> 8)
if r != g || r != b || a != 0xff {
t.Errorf("expect channels to match, got %v", c0)
continue
}
if c.R != p.expect {
t.Errorf("%s: got 0x%02x want 0x%02x", p.desc, c.R, p.expect)
if r != p.expect {
t.Errorf("%s: got 0x%02x want 0x%02x", p.desc, r, p.expect)
continue
}
// fast path for *image.RGBA
c1 := bilinearRGBA(src, p.x, p.y)
if r != c1.R || g != c1.G || b != c1.B || a != c1.A {
t.Errorf("%s: RGBA fast path mismatch got %v want %v", p.desc, c1, c0)
continue
}
// fast path for *image.Alpha
alpha := image.NewAlpha(src.Bounds())
for y := src.Bounds().Min.Y; y < src.Bounds().Max.Y; y++ {
for x := src.Bounds().Min.X; x < src.Bounds().Max.X; x++ {
r, _, _, _ := src.At(x, y).RGBA()
alpha.Set(x, y, color.Alpha{A: uint8(r >> 8)})
}
}
c2 := bilinearAlpha(alpha, p.x, p.y)
if c2.A != r {
t.Errorf("%s: Alpha fast path mismatch got %v want %v", p.desc, c2, c0)
continue
}
}
@ -109,7 +135,7 @@ func TestBilinearSubImage(t *testing.T) {
tests := []struct {
x, y float32
want uint8
want uint32
}{
{1, 1, 0x11},
{3, 1, 0x22},
@ -119,9 +145,10 @@ func TestBilinearSubImage(t *testing.T) {
}
for _, p := range tests {
c := bilinear(src1, p.x, p.y)
if c.R != p.want {
t.Errorf("(%.0f, %.0f): got 0x%02x want 0x%02x", p.x, p.y, c.R, p.want)
r, _, _, _ := bilinear(src1, p.x, p.y).RGBA()
r >>= 8
if r != p.want {
t.Errorf("(%.0f, %.0f): got 0x%02x want 0x%02x", p.x, p.y, r, p.want)
}
}
}