ebiten: add NewImageFromImageWithOptions

Closes #2013
Closes #2017
Closes #2124
This commit is contained in:
Hajime Hoshi 2022-06-15 11:52:34 +09:00
parent aee2e67242
commit acd70d6e34
2 changed files with 111 additions and 7 deletions

View File

@ -815,6 +815,8 @@ type NewImageOptions struct {
// If DrawImage is called on a new image created by NewImageOptions,
// for example, the center of scaling and rotating is (0, 0), that might not be a left-upper position.
//
// If options is nil, the default setting is used.
//
// NewImageWithOptions should be called only when necessary.
// For example, you should avoid to call NewImageWithOptions every Update or Draw call.
// Reusing the same image by Clear is much more efficient than creating a new image.
@ -859,19 +861,62 @@ func newImage(bounds image.Rectangle, imageType atlas.ImageType) *Image {
//
// NewImageFromImage panics if RunGame already finishes.
//
// The returned image's origin is always (0, 0). The source's bounds are not respected.
// The returned image's left-upper position is always (0, 0). The source's bounds are not respected.
func NewImageFromImage(source image.Image) *Image {
if isRunGameEnded() {
panic(fmt.Sprintf("ebiten: NewImage cannot be called after RunGame finishes"))
return NewImageFromImageWithOptions(source, nil)
}
// NewImageFromImageOptions represents options for NewImageFromImage.
type NewImageFromImageOptions struct {
// Unmanaged represents whether the image is unmanaged or not.
// The default (zero) value is false, that means the image is managed.
//
// An unmanged image is never on an internal automatic texture atlas.
// A regular image is a part of an internal texture atlas, and locating them is done automatically in Ebitengine.
// NewUnmanagedImage is useful when you want finer controls over the image for performance and memory reasons.
Unmanaged bool
// PreserveBounds represents whether the new image's bounds are the same as the given image.
// The default (zero) value is false, that means the new image's left-upper position is adjusted to (0, 0).
PreserveBounds bool
}
// NewImageFromImageWithOptions creates a new image with the given image (source) with the given options.
//
// If source's width or height is less than 1 or more than device-dependent maximum size, NewImageFromImageWithOptions panics.
//
// If options is nil, the default setting is used.
//
// NewImageFromImageWithOptions should be called only when necessary.
// For example, you should avoid to call NewImageFromImageWithOptions every Update or Draw call.
// Reusing the same image by Clear and ReplacePixels is much more efficient than creating a new image.
//
// NewImageFromImageWithOptions panics if RunGame already finishes.
func NewImageFromImageWithOptions(source image.Image, options *NewImageFromImageOptions) *Image {
if options == nil {
options = &NewImageFromImageOptions{}
}
size := source.Bounds().Size()
i := NewImage(size.X, size.Y)
var r image.Rectangle
if options.PreserveBounds {
r = source.Bounds()
} else {
size := source.Bounds().Size()
r = image.Rect(0, 0, size.X, size.Y)
}
i := NewImageWithOptions(r, &NewImageOptions{
Unmanaged: options.Unmanaged,
})
// If the given image is an Ebitengine image, use DrawImage instead of reading pixels from the source.
// This works even before the game loop runs.
if source, ok := source.(*Image); ok {
i.DrawImage(source, nil)
op := &DrawImageOptions{}
if options.PreserveBounds {
b := source.Bounds()
op.GeoM.Translate(float64(b.Min.X), float64(b.Min.Y))
}
i.DrawImage(source, op)
return i
}

View File

@ -3045,7 +3045,7 @@ func TestImageOptionsNegativeBoundsDrawImage(t *testing.T) {
want = color.RGBA{0xff, 0xff, 0xff, 0xff}
}
if got != want {
t.Errorf("img.At(%d, %d): got: %v, want: %v", i, j, got, want)
t.Errorf("dst.At(%d, %d): got: %v, want: %v", i, j, got, want)
}
}
}
@ -3115,6 +3115,65 @@ func TestImageOptionsNegativeBoundsDrawTriangles(t *testing.T) {
if -2 <= i && i < 2 && -3 <= j && j < 3 {
want = color.RGBA{0xff, 0xff, 0xff, 0xff}
}
if got != want {
t.Errorf("dst.At(%d, %d): got: %v, want: %v", i, j, got, want)
}
}
}
}
func TestImageFromImageOptions(t *testing.T) {
r := image.Rect(-2, -3, 4, 5)
pix := make([]byte, 4*r.Dx()*r.Dy())
for i := range pix {
pix[i] = 0xff
}
src := &image.RGBA{
Pix: pix,
Stride: 4 * 2,
Rect: r,
}
op := &ebiten.NewImageFromImageOptions{
PreserveBounds: true,
}
img := ebiten.NewImageFromImageWithOptions(src, op)
if got, want := img.Bounds(), r; got != want {
t.Errorf("got: %v, want: %v", got, want)
}
for j := r.Min.Y; j < r.Max.Y; j++ {
for i := r.Min.X; i < r.Max.X; i++ {
got := img.At(i, j)
want := color.RGBA{0xff, 0xff, 0xff, 0xff}
if got != want {
t.Errorf("img.At(%d, %d): got: %v, want: %v", i, j, got, want)
}
}
}
}
func TestImageFromEbitenImageOptions(t *testing.T) {
r := image.Rect(-2, -3, 4, 5)
src := ebiten.NewImageWithOptions(r, nil)
pix := make([]byte, 4*r.Dx()*r.Dy())
for i := range pix {
pix[i] = 0xff
}
src.ReplacePixels(pix)
op := &ebiten.NewImageFromImageOptions{
PreserveBounds: true,
}
img := ebiten.NewImageFromImageWithOptions(src, op)
if got, want := img.Bounds(), r; got != want {
t.Errorf("got: %v, want: %v", got, want)
}
for j := r.Min.Y; j < r.Max.Y; j++ {
for i := r.Min.X; i < r.Max.X; i++ {
got := img.At(i, j)
want := color.RGBA{0xff, 0xff, 0xff, 0xff}
if got != want {
t.Errorf("img.At(%d, %d): got: %v, want: %v", i, j, got, want)
}