diff --git a/image.go b/image.go index f8ff68b40..67ce6f11f 100644 --- a/image.go +++ b/image.go @@ -116,7 +116,7 @@ func (m *mipmap) disposeMipmaps() { // Image represents a rectangle set of pixels. // The pixel format is alpha-premultiplied RGBA. -// Image implements image.Image. +// Image implements image.Image and draw.Image. // // Functions of Image never returns error as of 1.5.0-alpha, and error values are always nil. type Image struct { @@ -131,6 +131,8 @@ type Image struct { bounds *image.Rectangle original *Image + pixelsToSet map[int]color.RGBA + filter Filter } @@ -180,6 +182,8 @@ func (i *Image) Fill(clr color.Color) error { panic("render to a subimage is not implemented") } + i.resolvePixelsToSet(false) + r16, g16, b16, a16 := clr.RGBA() r, g, b, a := uint8(r16>>8), uint8(g16>>8), uint8(b16>>8), uint8(a16>>8) i.mipmap.original().Fill(r, g, b, a) @@ -243,6 +247,9 @@ func (i *Image) drawImage(img *Image, options *DrawImageOptions) { panic("render to a subimage is not implemented") } + img.resolvePixelsToSet(true) + i.resolvePixelsToSet(true) + // Calculate vertices before locking because the user can do anything in // options.ImageParts interface without deadlock (e.g. Call Image functions). if options == nil { @@ -428,6 +435,9 @@ func (i *Image) DrawTriangles(vertices []Vertex, indices []uint16, img *Image, o panic("render to a subimage is not implemented") } + img.resolvePixelsToSet(true) + i.resolvePixelsToSet(true) + if len(indices)%3 != 0 { panic("ebiten: len(indices) % 3 must be 0") } @@ -531,9 +541,76 @@ func (i *Image) At(x, y int) color.Color { if i.bounds != nil && !image.Pt(x, y).In(*i.bounds) { return color.RGBA{} } + i.resolvePixelsToSet(true) return i.mipmap.original().At(x, y) } +// Set sets the color at (x, y). +// +// Set loads pixels from GPU to system memory if necessary, which means that Set can be slow. +// +// Set can't be called before the main loop (ebiten.Run) starts. +// +// If the image is disposed, Set does nothing. +func (i *Image) Set(x, y int, clr color.Color) { + i.copyCheck() + if i.isDisposed() { + return + } + if i.bounds != nil && !image.Pt(x, y).In(*i.bounds) { + return + } + if i.isSubimage() { + i = i.original + } + + if i.pixelsToSet == nil { + i.pixelsToSet = map[int]color.RGBA{} + } + r, g, b, a := clr.RGBA() + w, _ := i.Size() + i.pixelsToSet[x+y*w] = color.RGBA{ + byte(r >> 8), + byte(g >> 8), + byte(b >> 8), + byte(a >> 8), + } +} + +func (img *Image) resolvePixelsToSet(draw bool) { + if img.isSubimage() { + img = img.original + } + + if img.pixelsToSet == nil { + return + } + + if !draw { + img.pixelsToSet = nil + return + } + + w, h := img.Size() + pix := make([]byte, 4*w*h) + idx := 0 + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + c, ok := img.pixelsToSet[idx] + if !ok { + c = img.mipmap.original().At(i, j) + } + pix[4*idx] = c.R + pix[4*idx+1] = c.G + pix[4*idx+2] = c.B + pix[4*idx+3] = c.A + idx++ + } + } + img.ReplacePixels(pix) + img.pixelsToSet = nil +} + // Dispose disposes the image data. After disposing, most of image functions do nothing and returns meaningless values. // // Dispose is useful to save memory. @@ -549,6 +626,7 @@ func (i *Image) Dispose() error { if !i.isSubimage() { i.mipmap.dispose() } + i.resolvePixelsToSet(false) runtime.SetFinalizer(i, nil) return nil } @@ -573,6 +651,7 @@ func (i *Image) ReplacePixels(p []byte) error { if i.isSubimage() { panic("render to a subimage is not implemented") } + i.resolvePixelsToSet(false) s := i.Bounds().Size() if l := 4 * s.X * s.Y; len(p) != l { panic(fmt.Sprintf("ebiten: len(p) was %d but must be %d", len(p), l)) diff --git a/image_test.go b/image_test.go index e2589fefc..582a672bc 100644 --- a/image_test.go +++ b/image_test.go @@ -1340,7 +1340,7 @@ func TestImageAddressRepeat(t *testing.T) { } } -func TestReplacePixelsAfterClear(t *testing.T) { +func TestImageReplacePixelsAfterClear(t *testing.T) { const w, h = 256, 256 img, _ := NewImage(w, h, FilterDefault) img.ReplacePixels(make([]byte, 4*w*h)) @@ -1353,3 +1353,97 @@ func TestReplacePixelsAfterClear(t *testing.T) { // The test passes if this doesn't crash. } + +func TestImageSet(t *testing.T) { + type Pt struct { + X, Y int + } + + const w, h = 16, 16 + img, _ := NewImage(w, h, FilterDefault) + colors := map[Pt]color.RGBA{ + {1, 2}: {3, 4, 5, 6}, + {7, 8}: {9, 10, 11, 12}, + {13, 14}: {15, 16, 17, 18}, + } + + for p, c := range colors { + img.Set(p.X, p.Y, c) + } + + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + got := img.At(i, j).(color.RGBA) + var want color.RGBA + if c, ok := colors[Pt{i, j}]; ok { + want = c + } + if got != want { + t.Errorf("img.At(%d, %d): got: %v, want: %v", i, j, got, want) + } + } + } +} + +func TestImageSetAndDraw(t *testing.T) { + type Pt struct { + X, Y int + } + + const w, h = 16, 16 + src, _ := NewImage(w, h, FilterDefault) + dst, _ := NewImage(w, h, FilterDefault) + colors := map[Pt]color.RGBA{ + {1, 2}: {3, 4, 5, 6}, + {7, 8}: {9, 10, 11, 12}, + {13, 14}: {15, 16, 17, 18}, + } + for p, c := range colors { + src.Set(p.X, p.Y, c) + dst.Set(p.X+1, p.Y+1, c) + } + + dst.DrawImage(src, nil) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + got := dst.At(i, j).(color.RGBA) + var want color.RGBA + if c, ok := colors[Pt{i, j}]; ok { + want = c + } + if c, ok := colors[Pt{i - 1, j - 1}]; ok { + want = c + } + if got != want { + t.Errorf("img.At(%d, %d): got: %v, want: %v", i, j, got, want) + } + } + } + + src.Clear() + dst.Clear() + for p, c := range colors { + src.Set(p.X, p.Y, c) + dst.Set(p.X+1, p.Y+1, c) + } + op := &DrawImageOptions{} + op.GeoM.Translate(2, 2) + dst.DrawImage(src.SubImage(image.Rect(2, 2, w-2, h-2)).(*Image), op) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + got := dst.At(i, j).(color.RGBA) + var want color.RGBA + if 2 <= i && 2 <= j && i < w-2 && j < h-2 { + if c, ok := colors[Pt{i, j}]; ok { + want = c + } + } + if c, ok := colors[Pt{i - 1, j - 1}]; ok { + want = c + } + if got != want { + t.Errorf("img.At(%d, %d): got: %v, want: %v", i, j, got, want) + } + } + } +}