graphics: Experimental: (*image.Image).SubImage

Fixes #722
This commit is contained in:
Hajime Hoshi 2018-10-23 23:27:27 +09:00
parent 41877bba0b
commit 710f56531f
14 changed files with 155 additions and 92 deletions

View File

@ -56,7 +56,6 @@ func drawDebugText(rt *ebiten.Image, str string, ox, oy int, shadow bool) {
x := 0
y := 0
w, _ := debugPrintTextImage.Size()
var r image.Rectangle
for _, c := range str {
const (
cw = assets.CharWidth
@ -70,15 +69,10 @@ func drawDebugText(rt *ebiten.Image, str string, ox, oy int, shadow bool) {
n := w / cw
sx := (int(c) % n) * cw
sy := (int(c) / n) * ch
r.Min.X = sx
r.Min.Y = sy
r.Max.X = sx + cw
r.Max.Y = sy + ch
op.SourceRect = &r
op.GeoM.Reset()
op.GeoM.Translate(float64(x), float64(y))
op.GeoM.Translate(float64(ox+1), float64(oy))
_ = rt.DrawImage(debugPrintTextImage, op)
_ = rt.DrawImage(debugPrintTextImage.SubImage(image.Rect(sx, sy, sx+cw, sy+ch)).(*ebiten.Image), op)
x += cw
}
}

View File

@ -218,9 +218,7 @@ func drawGroundImage(screen *ebiten.Image, ground *ebiten.Image) {
op.GeoM.Scale(1/z, 8) // 8 is an arbitrary number not to make empty lines.
op.GeoM.Translate(float64(pw)/2, float64(j)/z)
src := image.Rect(0, j, gw, j+1)
op.SourceRect = &src
perspectiveGroundImage.DrawImage(ground, op)
perspectiveGroundImage.DrawImage(ground.SubImage(image.Rect(0, j, gw, j+1)).(*ebiten.Image), op)
}
perspectiveGroundImage.DrawImage(fogImage, nil)

View File

@ -54,9 +54,7 @@ func update(screen *ebiten.Image) error {
op.GeoM.Translate(screenWidth/2, screenHeight/2)
i := (count / 5) % frameNum
sx, sy := frameOX+i*frameWidth, frameOY
r := image.Rect(sx, sy, sx+frameWidth, sy+frameHeight)
op.SourceRect = &r
screen.DrawImage(runnerImage, op)
screen.DrawImage(runnerImage.SubImage(image.Rect(sx, sy, sx+frameWidth, sy+frameHeight)).(*ebiten.Image), op)
return nil
}

View File

@ -175,9 +175,7 @@ func drawBlock(r *ebiten.Image, block BlockType, x, y int, clr ebiten.ColorM) {
op.GeoM.Translate(float64(x), float64(y))
srcX := (int(block) - 1) * blockWidth
src := image.Rect(srcX, 0, srcX+blockWidth, blockHeight)
op.SourceRect = &src
r.DrawImage(imageBlocks, op)
r.DrawImage(imageBlocks.SubImage(image.Rect(srcX, 0, srcX+blockWidth, blockHeight)).(*ebiten.Image), op)
}
func (p *Piece) InitialPosition() (int, int) {

View File

@ -345,9 +345,7 @@ func (g *Game) drawTiles(screen *ebiten.Image) {
op.GeoM.Reset()
op.GeoM.Translate(float64(i*tileSize-floorMod(g.cameraX, tileSize)),
float64((ny-1)*tileSize-floorMod(g.cameraY, tileSize)))
r := image.Rect(0, 0, tileSize, tileSize)
op.SourceRect = &r
screen.DrawImage(tilesImage, op)
screen.DrawImage(tilesImage.SubImage(image.Rect(0, 0, tileSize, tileSize)).(*ebiten.Image), op)
// pipe
if tileY, ok := g.pipeAt(floorDiv(g.cameraX, tileSize) + i); ok {
@ -357,27 +355,25 @@ func (g *Game) drawTiles(screen *ebiten.Image) {
op.GeoM.Translate(float64(i*tileSize-floorMod(g.cameraX, tileSize)),
float64(j*tileSize-floorMod(g.cameraY, tileSize)))
op.GeoM.Translate(0, tileSize)
var r image.Rectangle
if j == tileY-1 {
r := image.Rect(pipeTileSrcX, pipeTileSrcY, pipeTileSrcX+tileSize*2, pipeTileSrcY+tileSize)
op.SourceRect = &r
r = image.Rect(pipeTileSrcX, pipeTileSrcY, pipeTileSrcX+tileSize*2, pipeTileSrcY+tileSize)
} else {
r := image.Rect(pipeTileSrcX, pipeTileSrcY+tileSize, pipeTileSrcX+tileSize*2, pipeTileSrcY+tileSize*2)
op.SourceRect = &r
r = image.Rect(pipeTileSrcX, pipeTileSrcY+tileSize, pipeTileSrcX+tileSize*2, pipeTileSrcY+tileSize*2)
}
screen.DrawImage(tilesImage, op)
screen.DrawImage(tilesImage.SubImage(r).(*ebiten.Image), op)
}
for j := tileY + pipeGapY; j < screenHeight/tileSize-1; j++ {
op.GeoM.Reset()
op.GeoM.Translate(float64(i*tileSize-floorMod(g.cameraX, tileSize)),
float64(j*tileSize-floorMod(g.cameraY, tileSize)))
var r image.Rectangle
if j == tileY+pipeGapY {
r := image.Rect(pipeTileSrcX, pipeTileSrcY, pipeTileSrcX+pipeWidth, pipeTileSrcY+tileSize)
op.SourceRect = &r
r = image.Rect(pipeTileSrcX, pipeTileSrcY, pipeTileSrcX+pipeWidth, pipeTileSrcY+tileSize)
} else {
r := image.Rect(pipeTileSrcX, pipeTileSrcY+tileSize, pipeTileSrcX+pipeWidth, pipeTileSrcY+tileSize+tileSize)
op.SourceRect = &r
r = image.Rect(pipeTileSrcX, pipeTileSrcY+tileSize, pipeTileSrcX+pipeWidth, pipeTileSrcY+tileSize+tileSize)
}
screen.DrawImage(tilesImage, op)
screen.DrawImage(tilesImage.SubImage(r).(*ebiten.Image), op)
}
}
}

View File

@ -78,8 +78,7 @@ func update(screen *ebiten.Image) error {
}
op.GeoM.Translate(float64(r.Min.X), float64(r.Min.Y))
op.GeoM.Translate(offsetX, offsetY)
op.SourceRect = &r
screen.DrawImage(keyboardImage, op)
screen.DrawImage(keyboardImage.SubImage(r).(*ebiten.Image), op)
}
keyStrs := []string{}

View File

@ -69,6 +69,7 @@ Press C to clip the images.
Scale: %0.2f`, s)
ebitenutil.DebugPrint(screen, msg)
clippedGophersImage := gophersImage.SubImage(image.Rect(10, 10, 100, 100)).(*ebiten.Image)
for i, f := range []ebiten.Filter{ebiten.FilterNearest, ebiten.FilterLinear} {
w, h := gophersImage.Size()
@ -82,10 +83,10 @@ Scale: %0.2f`, s)
op.GeoM.Translate(32+float64(i*w)*s+float64(i*4), 64)
op.Filter = f
if clip {
r := image.Rect(10, 10, 100, 100)
op.SourceRect = &r
screen.DrawImage(clippedGophersImage, op)
} else {
screen.DrawImage(gophersImage, op)
}
screen.DrawImage(gophersImage, op)
}
return nil

View File

@ -58,9 +58,7 @@ func update(screen *ebiten.Image) error {
// Move the image's center to the screen's center.
op.GeoM.Translate(screenWidth/2, screenHeight/2)
r := image.Rect(0, i, w, i+1)
op.SourceRect = &r
screen.DrawImage(gophersImage, op)
screen.DrawImage(gophersImage.SubImage(image.Rect(0, i, w, i+1)).(*ebiten.Image), op)
}
return nil
}

View File

@ -119,9 +119,7 @@ func update(screen *ebiten.Image) error {
sx := (t % tileXNum) * tileSize
sy := (t / tileXNum) * tileSize
r := image.Rect(sx, sy, sx+tileSize, sy+tileSize)
op.SourceRect = &r
screen.DrawImage(tilesImage, op)
screen.DrawImage(tilesImage.SubImage(image.Rect(sx, sy, sx+tileSize, sy+tileSize)).(*ebiten.Image), op)
}
}

View File

@ -154,9 +154,7 @@ func drawNinePatches(dst *ebiten.Image, dstRect image.Rectangle, srcRect image.R
op.GeoM.Scale(float64(dw)/float64(sw), float64(dh)/float64(sh))
op.GeoM.Translate(float64(dx), float64(dy))
op.GeoM.Translate(float64(dstX), float64(dstY))
r := image.Rect(sx, sy, sx+sw, sy+sh)
op.SourceRect = &r
dst.DrawImage(uiImage, op)
dst.DrawImage(uiImage.SubImage(image.Rect(sx, sy, sx+sw, sy+sh)).(*ebiten.Image), op)
}
}
}

128
image.go
View File

@ -141,6 +141,9 @@ type Image struct {
// The level 0 image is a regular image and higher-level images are used for mipmap.
mipmap *mipmap
bounds *image.Rectangle
original *Image
filter Filter
}
@ -159,6 +162,10 @@ func (i *Image) isDisposed() bool {
return i.mipmap.isDisposed()
}
func (i *Image) isSubimage() bool {
return i.bounds != nil
}
// Clear resets the pixels of the image into 0.
//
// When the image is disposed, Clear does nothing.
@ -169,6 +176,12 @@ func (i *Image) Clear() error {
if i.isDisposed() {
return nil
}
// TODO: Implement this.
if i.isSubimage() {
panic("render to a subimage is not implemented")
}
i.fill(0, 0, 0, 0)
return nil
}
@ -183,6 +196,12 @@ func (i *Image) Fill(clr color.Color) error {
if i.isDisposed() {
return nil
}
// TODO: Implement this.
if i.isSubimage() {
panic("render to a subimage is not implemented")
}
r, g, b, a := clr.RGBA()
i.fill(uint8(r>>8), uint8(g>>8), uint8(b>>8), uint8(a>>8))
return nil
@ -278,6 +297,11 @@ func (i *Image) drawImage(img *Image, options *DrawImageOptions) {
return
}
// TODO: Implement this.
if i.isSubimage() {
panic("render to a subimage is not implemented")
}
// Calculate vertices before locking because the user can do anything in
// options.ImageParts interface without deadlock (e.g. Call Image functions).
if options == nil {
@ -300,21 +324,32 @@ func (i *Image) drawImage(img *Image, options *DrawImageOptions) {
ColorM: options.ColorM,
CompositeMode: options.CompositeMode,
}
r := image.Rect(sx0, sy0, sx1, sy1)
op.SourceRect = &r
op.GeoM.Scale(
float64(dx1-dx0)/float64(sx1-sx0),
float64(dy1-dy0)/float64(sy1-sy0))
op.GeoM.Translate(float64(dx0), float64(dy0))
op.GeoM.Concat(options.GeoM)
i.DrawImage(img, op)
i.DrawImage(img.SubImage(image.Rect(sx0, sy0, sx1, sy1)).(*Image), op)
}
return
}
w, h := img.Size()
sx0, sy0, sx1, sy1 := 0, 0, w, h
if r := options.SourceRect; r != nil {
// SourceRect is deprecated. This implementation is for backward compatibility.
if img.bounds != nil || options.SourceRect != nil {
r := img.bounds
if r == nil {
r = options.SourceRect
} else if options.SourceRect != nil {
r2 := r.Intersect(*options.SourceRect)
r = &r2
}
if r.Empty() {
return
}
sx0 = r.Min.X
sy0 = r.Min.Y
if sx1 > r.Max.X {
@ -324,6 +359,7 @@ func (i *Image) drawImage(img *Image, options *DrawImageOptions) {
sy1 = r.Max.Y
}
}
geom := &options.GeoM
if sx0 < 0 || sy0 < 0 {
dx := 0.0
@ -460,6 +496,11 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o
return
}
// TODO: Implement this.
if i.isSubimage() {
panic("render to a subimage is not implemented")
}
if len(indices)%3 != 0 {
panic("ebiten: len(indices) % 3 must be 0")
}
@ -487,10 +528,51 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o
i.disposeMipmaps()
}
// SubImage returns an image representing the portion of the image p visible through r. The returned value shares pixels with the original image.
//
// The returned value is always *ebiten.Image.
//
// If the image is disposed, SubImage returns nil.
//
// In the current Ebiten implementation, SubImage is available only as a rendering source.
func (i *Image) SubImage(r image.Rectangle) image.Image {
i.copyCheck()
if i.isDisposed() {
return nil
}
img := &Image{
mipmap: i.mipmap,
filter: i.filter,
}
// Keep the original image's reference not to dispose that by GC.
if i.isSubimage() {
img.original = i.original
} else {
img.original = i
}
img.addr = img
runtime.SetFinalizer(img, (*Image).Dispose)
r = r.Intersect(img.Bounds())
// Need to check Empty explicitly. See the standard image package implementations.
if r.Empty() {
img.bounds = &image.ZR
} else {
img.bounds = &r
}
return img
}
// Bounds returns the bounds of the image.
func (i *Image) Bounds() image.Rectangle {
w, h := i.Size()
return image.Rect(0, 0, w, h)
if i.bounds == nil {
w, h := i.Size()
return image.Rect(0, 0, w, h)
}
return *i.bounds
}
// ColorModel returns the color model of the image.
@ -512,6 +594,9 @@ func (i *Image) At(x, y int) color.Color {
if i.isDisposed() {
return color.RGBA{}
}
if i.bounds != nil && !image.Pt(x, y).In(*i.bounds) {
return color.RGBA{}
}
return i.mipmap.original().At(x, y)
}
@ -527,7 +612,9 @@ func (i *Image) Dispose() error {
if i.isDisposed() {
return nil
}
i.mipmap.dispose()
if !i.isSubimage() {
i.mipmap.dispose()
}
runtime.SetFinalizer(i, nil)
return nil
}
@ -548,6 +635,10 @@ func (i *Image) ReplacePixels(p []byte) error {
if i.isDisposed() {
return nil
}
// TODO: Implement this.
if i.isSubimage() {
panic("render to a subimage is not implemented")
}
i.mipmap.original().ReplacePixels(p)
i.disposeMipmaps()
return nil
@ -555,22 +646,6 @@ func (i *Image) ReplacePixels(p []byte) error {
// A DrawImageOptions represents options to render an image on an image.
type DrawImageOptions struct {
// SourceRect is the region of the source image to draw.
// If SourceRect is nil, whole image is used.
//
// It is assured that texels out of the SourceRect are never used.
//
// Calling DrawImage copies the content of SourceRect pointer. This means that
// even if the SourceRect value is modified after passed to DrawImage,
// the result of DrawImage doen't change.
//
// op := &ebiten.DrawImageOptions{}
// r := image.Rect(0, 0, 100, 100)
// op.SourceRect = &r
// dst.DrawImage(src, op)
// r.Min.X = 10 // This doesn't affect the previous DrawImage.
SourceRect *image.Rectangle
// GeoM is a geometry matrix to draw.
// The default (zero) value is identify, which draws the image at (0, 0).
GeoM GeoM
@ -595,11 +670,14 @@ type DrawImageOptions struct {
// Otherwise, Filter specified at DrawImageOptions is used.
Filter Filter
// Deprecated (as of 1.5.0-alpha): Use SourceRect instead.
// Deprecated (as of 1.5.0-alpha): Use SubImage instead.
ImageParts ImageParts
// Deprecated (as of 1.1.0-alpha): Use SourceRect instead.
// Deprecated (as of 1.1.0-alpha): Use SubImage instead.
Parts []ImagePart
// Deprecated (as of 1.9.0-alpha): Use SubImage instead.
SourceRect *image.Rectangle
}
// NewImage returns an empty image.

View File

@ -555,21 +555,20 @@ func TestImageEdge(t *testing.T) {
angles = append(angles, float64(a)/4096*2*math.Pi)
}
img0Sub := img0.SubImage(image.Rect(img0OffsetWidth, img0InnerHeight, img0Width-img0OffsetWidth, img0Height-img0InnerHeight)).(*Image)
for _, s := range []float64{1, 0.5, 0.25} {
for _, f := range []Filter{FilterNearest, FilterLinear} {
for _, a := range angles {
img1.Clear()
op := &DrawImageOptions{}
r := image.Rect(img0OffsetWidth, img0InnerHeight, img0Width-img0OffsetWidth, img0Height-img0InnerHeight)
op.SourceRect = &r
w, h := img0.Size()
op.GeoM.Translate(-float64(w)/2, -float64(h)/2)
op.GeoM.Scale(s, s)
op.GeoM.Rotate(a)
op.GeoM.Translate(img1Width/2, img1Height/2)
op.Filter = f
img1.DrawImage(img0, op)
img1.DrawImage(img0Sub, op)
for j := 0; j < img1Height; j++ {
for i := 0; i < img1Width; i++ {
c := img1.At(i, j)
@ -641,10 +640,8 @@ func TestImageLinear(t *testing.T) {
op := &DrawImageOptions{}
op.GeoM.Translate(8, 8)
op.GeoM.Scale(2, 2)
r := image.Rect(8, 8, 24, 24)
op.SourceRect = &r
op.Filter = FilterLinear
dst.DrawImage(src, op)
dst.DrawImage(src.SubImage(image.Rect(8, 8, 24, 24)).(*Image), op)
for j := 0; j < 64; j++ {
for i := 0; i < 64; i++ {
@ -683,11 +680,10 @@ func TestImageOutside(t *testing.T) {
op := &DrawImageOptions{}
op.GeoM.Translate(0, 0)
op.SourceRect = &image.Rectangle{
dst.DrawImage(src.SubImage(image.Rectangle{
Min: image.Pt(c.X, c.Y),
Max: image.Pt(c.X+c.Width, c.Y+c.Height),
}
dst.DrawImage(src, op)
}).(*Image), op)
for j := 0; j < 4; j++ {
for i := 0; i < 4; i++ {
@ -709,12 +705,12 @@ func TestImageOutsideUpperLeft(t *testing.T) {
op := &DrawImageOptions{}
op.GeoM.Rotate(math.Pi / 4)
r := image.Rect(-4, -4, 8, 8)
op.SourceRect = &r
dst1.DrawImage(src, op)
dst1.DrawImage(src.SubImage(image.Rect(-4, -4, 8, 8)).(*Image), op)
op = &DrawImageOptions{}
op.GeoM.Translate(4, 4)
// The outside part of the source rect is just ignored.
// This behavior was changed as of 1.9.0-alpha.
// op.GeoM.Translate(4, 4)
op.GeoM.Rotate(math.Pi / 4)
dst2.DrawImage(src, op)
@ -829,9 +825,7 @@ func TestImageStretch(t *testing.T) {
img1.Clear()
op := &DrawImageOptions{}
op.GeoM.Scale(1, float64(i)/16)
r := image.Rect(0, 0, 16, 16)
op.SourceRect = &r
img1.DrawImage(img0, op)
img1.DrawImage(img0.SubImage(image.Rect(0, 0, 16, 16)).(*Image), op)
for j := -1; j <= 1; j++ {
got := img1.At(0, i+j).(color.RGBA)
want := color.RGBA{}
@ -1023,3 +1017,20 @@ func TestImageMiamapAndDrawTriangle(t *testing.T) {
}
}
}
func TestImageSubImageAt(t *testing.T) {
img, _ := NewImage(16, 16, FilterDefault)
img.Fill(color.RGBA{0xff, 0, 0, 0xff})
got := img.SubImage(image.Rect(1, 1, 16, 16)).At(0, 0).(color.RGBA)
want := color.RGBA{}
if got != want {
t.Errorf("got: %v, want: %v", got, want)
}
got = img.SubImage(image.Rect(1, 1, 16, 16)).At(1, 1).(color.RGBA)
want = color.RGBA{0xff, 0, 0, 0xff}
if got != want {
t.Errorf("got: %v, want: %v", got, want)
}
}

View File

@ -18,13 +18,13 @@ import (
"image"
)
// An ImagePart is deprecated (as of 1.1.0-alpha): Use SourceRect instead.
// An ImagePart is deprecated (as of 1.1.0-alpha): Use SubImage instead.
type ImagePart struct {
Dst image.Rectangle
Src image.Rectangle
}
// An ImageParts is deprecated (as of 1.5.0-alpha): Use SourceRect instead.
// An ImageParts is deprecated (as of 1.5.0-alpha): Use SubImage instead.
type ImageParts interface {
Len() int
Dst(i int) (x0, y0, x1, y1 int)

View File

@ -61,12 +61,8 @@ func drawGlyph(dst *ebiten.Image, face font.Face, r rune, img *glyphImage, x, y
b := getGlyphBounds(face, r)
op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(fixed26_6ToFloat64(x+b.Min.X), fixed26_6ToFloat64(y+b.Min.Y))
op.ColorM = clr
re := image.Rect(img.x, img.y, img.x+img.width, img.y+img.height)
op.SourceRect = &re
_ = dst.DrawImage(img.image, op)
_ = dst.DrawImage(img.image.SubImage(image.Rect(img.x, img.y, img.x+img.width, img.y+img.height)).(*ebiten.Image), op)
}
var (